Bläddra i källkod

!560 【新增功能】添加扫描枪、门店店员绑定功能
Merge pull request !560 from 痴货/jh-smq

芋道源码 10 månader sedan
förälder
incheckning
ca9a389ae8

+ 10 - 0
src/api/mall/trade/delivery/pickUpStore/index.ts

@@ -44,3 +44,13 @@ export const updateDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) =>
 export const deleteDeliveryPickUpStore = async (id: number) => {
   return await request.delete({ url: '/trade/delivery/pick-up-store/delete?id=' + id })
 }
+
+//绑定自提店员
+export const bindStoreStaffId = async (data: any) => {
+  return await request.post({ url: '/trade/delivery/pick-up-store/bind', data })
+}
+
+//查询门店绑定情况
+export const getDeliveryPickUpStoreStaff = async (id: number) => {
+  return await request.get({ url: '/trade/delivery/pick-up-store/get-store-staff?id=' + id })
+}

+ 102 - 6
src/views/mall/trade/delivery/pickUpOrder/index.vue

@@ -26,9 +26,8 @@
         <el-select
           v-model="queryParams.pickUpStoreId"
           class="!w-280px"
-          clearable
-          multiple
           placeholder="全部"
+          @change="handleQuery"
         >
           <el-option
             v-for="item in pickUpStoreList"
@@ -73,10 +72,16 @@
           <Icon class="mr-5px" icon="ep:refresh" />
           重置
         </el-button>
-        <el-button @click="handlePickup" type="success" plain v-hasPermi="['trade:order:pick-up']">
+        <el-button @click="handlePickup" type="success" plain v-hasPermi="['trade:order:pick-up']" :disabled="isUse">
           <Icon class="mr-5px" icon="ep:check" />
           核销
         </el-button>
+        <el-button type="primary" @click="connectToSerialPort" :disabled="serialPort || isUse">
+          连接扫描枪
+        </el-button>
+        <el-button type="danger" @click="cutPort" :disabled="!serialPort || isUse">
+          断开扫描枪
+        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -216,6 +221,12 @@ import { DeliveryTypeEnum } from '@/utils/constants'
 import { TradeOrderSummaryRespVO } from '@/api/mall/trade/order'
 import { DeliveryPickUpStoreVO } from '@/api/mall/trade/delivery/pickUpStore'
 import OrderPickUpForm from '@/views/mall/trade/order/form/OrderPickUpForm.vue'
+import { ref, onMounted } from "vue";
+const message = useMessage() // 消息弹窗
+
+const port = ref("");
+const ports = ref([]);
+const reader = ref("");
 
 defineOptions({ name: 'PickUpOrder' })
 
@@ -227,6 +238,8 @@ const total = ref(2)
 const list = ref<TradeOrderApi.OrderVO[]>([])
 // 搜索的表单
 const queryFormRef = ref<FormInstance>()
+const serialPort = ref(false)
+const isUse = ref(true)
 // 初始表单参数
 const INIT_QUERY_PARAMS = {
   // 页数
@@ -240,6 +253,7 @@ const INIT_QUERY_PARAMS = {
   // 自提门店
   pickUpStoreId: undefined
 }
+
 // 表单搜索
 const queryParams = ref({ ...INIT_QUERY_PARAMS })
 // 订单搜索类型 queryParam
@@ -294,6 +308,9 @@ const handleQuery = async () => {
 const resetQuery = () => {
   queryFormRef.value?.resetFields()
   queryParams.value = { ...INIT_QUERY_PARAMS }
+  if(pickUpStoreList.value.length > 0) {
+    queryParams.value.pickUpStoreId = pickUpStoreList.value[0].id
+  }
   handleQuery()
 }
 
@@ -309,10 +326,89 @@ const handlePickup = () => {
   pickUpForm.value.open()
 }
 
+/** 连接扫码枪 */
+const connectToSerialPort = async () => {
+  try {
+    // 判断浏览器支持串口通信
+    if ("serial" in navigator && navigator.serial != null && typeof navigator.serial === 'object' && "requestPort" in navigator.serial) {
+      // 提示用户选择一个串口
+      port.value = await navigator.serial.requestPort();
+    } else {
+      message.error("浏览器不支持扫码枪连接,请更换浏览器重试")
+      return
+    }
+
+    // 获取用户之前授予该网站访问权限的所有串口。
+    ports.value = await navigator.serial.getPorts();
+
+    // console.log(port.value, ports.value);
+    console.log(port.value);
+    // 等待串口打开
+    await port.value.open({ baudRate: 9600 , dataBits: 8 , stopBits: 2});
+
+    // console.log(typeof port.value);
+    message.success("成功连接扫码枪")
+    serialPort.value = true;
+    // readData(port.value);
+    readData();
+  } catch (error) {
+    // 处理连接串口出错的情况
+    console.log("Error connecting to serial port:", error);
+  }
+};
+
+/** 监听扫码枪输入 */
+const readData = async () => {
+  reader.value = port.value.readable.getReader();
+  let data = ""; //扫码数据
+  // 监听来自串口的数据
+  while (true) {
+    const { value, done } = await reader.value.read();
+    if (done) {
+      // 允许稍后关闭串口
+      reader.value.releaseLock();
+      break;
+    }
+    // 获取发送的数据
+    const serialData = new TextDecoder().decode(value);
+    data = `${data}${serialData}`;
+    if (serialData.includes("\r")) {
+      //读取结束
+      let codeData = data.replace("\r","");
+      data = ""; //清空下次读取不会叠加
+      console.log(`二维码数据:${codeData}`);
+      //处理拿到数据逻辑
+      pickUpForm.value.open(codeData)
+    }
+  }
+};
+
+/** 断开扫码枪 */
+const cutPort = async () => {
+  if (port.value !== "") {
+    await reader.value.cancel();
+    await port.value.close();
+    port.value = "";
+    console.log("断开扫码枪连接");
+    message.success("已成功断开扫码枪连接")
+    serialPort.value = false;
+  } else {
+    message.warning("请先连接或打开扫码枪")
+  }
+};
+
 /** 初始化 **/
-onMounted(() => {
-  getList()
-  getPickUpStoreList()
+onMounted(async () => {
+  await getPickUpStoreList()
+  if(pickUpStoreList.value.length > 0){
+    queryParams.value.pickUpStoreId = pickUpStoreList.value[0].id
+    isUse.value = false
+    await getList()
+  }else{
+    message.error("当前登录人没绑定任何自提点")
+    loading.value = false
+    isUse.value = true
+  }
 })
 </script>
 <style lang="scss" scoped>

+ 161 - 0
src/views/mall/trade/delivery/pickUpStore/DeliveryPickUpStoreBindForm.vue

@@ -0,0 +1,161 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="20%">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="门店名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入门店名称" :disabled="true"/>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="门店店员" prop="storeStaffIds">
+            <el-button type="primary" @click="storeStaffTableSelect.open()">选择店员</el-button>
+          </el-form-item>
+          <!-- 店员列表 -->
+          <ContentWrap v-if="usersList.length > 0">
+            <el-table :data="usersList">
+              <el-table-column label="编号" align="center" prop="id" />
+              <el-table-column
+                label="用户昵称"
+                align="center"
+                prop="nickname"
+                :show-overflow-tooltip="true"
+              />
+              <el-table-column label="状态" align="center" key="status">
+                <template #default="scope">
+                  <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="操作">
+                <template #default="scope">
+                  <el-button
+                    v-hasPermi="['trade:delivery:pick-up-store:delete']"
+                    link
+                    type="danger"
+                    @click="handleDelete(scope.row.id)"
+                  >
+                    删除
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </ContentWrap>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+  <StoreStaffTableSelect ref="storeStaffTableSelect" @change="changeStoreStaff"/>
+</template>
+<script setup lang="ts">
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import StoreStaffTableSelect from './components/StoreStaffTableSelect.vue'
+import {DICT_TYPE} from "@/utils/dict";
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  name: '',
+  storeStaffIds: [],
+  storeStaffs: [],
+})
+const formRules = reactive({
+  name: [{ required: true, message: '门店名称不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+const storeStaffTableSelect = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = '绑定自提门店员工'
+  resetForm()
+  formLoading.value = true
+  try {
+    await getList(id)
+  } finally {
+    formLoading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const submitForm = async () => {
+  formData.value.storeStaffIds = usersList.value.map(item => item.id) as [];
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    await DeliveryPickUpStoreApi.bindStoreStaffId(data)
+    message.success("绑定成功")
+    dialogVisible.value = false
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    const index = usersList.value.findIndex(item => {
+      if (item.id == id){
+        return true;
+      }
+    })
+    usersList.value.splice(index, 1);
+    //await DeliveryPickUpStoreStaffApi.deleteDeliveryPickUpStoreStaff(id,formData.value.id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    //await getList(formData.value.id)
+  } catch {}
+}
+
+/**
+ * 查询自提点员工绑定关系
+ */
+const getList = async (id: number) => {
+  formData.value = await DeliveryPickUpStoreApi.getDeliveryPickUpStoreStaff(id)
+  if(formData.value.storeStaffs){
+    usersList.value = formData.value.storeStaffs;
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: '',
+    storeStaffIds: [],
+    storeStaffs: [],
+  }
+  formRef.value?.resetFields()
+}
+
+const usersList = ref([])
+const changeStoreStaff = (checkedUsers : []) => {
+  usersList.value = checkedUsers
+}
+</script>

+ 270 - 0
src/views/mall/trade/delivery/pickUpStore/components/StoreStaffTableSelect.vue

@@ -0,0 +1,270 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="60%">
+    <el-row :gutter="20">
+      <!-- 左侧部门树 -->
+      <el-col :span="4" :xs="24">
+        <ContentWrap class="h-1/1">
+          <DeptTree @node-click="handleDeptNodeClick" />
+        </ContentWrap>
+      </el-col>
+      <el-col :span="20" :xs="24">
+        <!-- 搜索 -->
+        <ContentWrap>
+          <el-form
+            class="-mb-15px"
+            :model="queryParams"
+            ref="queryFormRef"
+            :inline="true"
+            label-width="68px"
+          >
+            <el-form-item label="用户名称" prop="username">
+              <el-input
+                v-model="queryParams.username"
+                placeholder="请输入用户名称"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item label="手机号码" prop="mobile">
+              <el-input
+                v-model="queryParams.mobile"
+                placeholder="请输入手机号码"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select
+                v-model="queryParams.status"
+                placeholder="用户状态"
+                clearable
+                class="!w-240px"
+              >
+                <el-option
+                  v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="创建时间" prop="createTime">
+              <el-date-picker
+                v-model="queryParams.createTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="datetimerange"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
+              <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
+            </el-form-item>
+          </el-form>
+        </ContentWrap>
+        <ContentWrap>
+          <el-table v-loading="loading" :data="list">
+            <el-table-column width="55">
+              <template #header>
+                <el-checkbox
+                  v-model="isCheckAll"
+                  :indeterminate="isIndeterminate"
+                  @change="handleCheckAll"
+                />
+              </template>
+              <template #default="{ row }">
+                <el-checkbox
+                  v-model="checkedStatus[row.id]"
+                  @change="(checked: boolean) => handleCheckOne(checked, row, true)"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="用户编号" align="center" key="id" prop="id" />
+            <el-table-column
+              label="用户名称"
+              align="center"
+              prop="username"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              label="用户昵称"
+              align="center"
+              prop="nickname"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column
+              label="部门"
+              align="center"
+              key="deptName"
+              prop="deptName"
+              :show-overflow-tooltip="true"
+            />
+            <el-table-column label="手机号码" align="center" prop="mobile" width="120" />
+            <el-table-column label="状态" key="status">
+              <template #default="scope">
+                <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+              </template>
+            </el-table-column>
+            <el-table-column
+              label="创建时间"
+              align="center"
+              prop="createTime"
+              :formatter="dateFormatter"
+              width="180"
+            />
+          </el-table>
+          <Pagination
+            :total="total"
+            v-model:page="queryParams.pageNo"
+            v-model:limit="queryParams.pageSize"
+            @pagination="getList"
+          />
+        </ContentWrap>
+      </el-col>
+    </el-row>
+    <template #footer>
+      <el-button type="primary" @click="handleEmitChange">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as UserApi from '@/api/system/user'
+import DeptTree from '@/views/system/user/DeptTree.vue'
+
+// 是否全选
+const isCheckAll = ref(false)
+// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
+const isIndeterminate = ref(false)
+// 选中的活动
+const checkedUsers = ref([])
+// 选中状态:key为用户ID,value为是否选中
+const checkedStatus = ref<Record<string, boolean>>({})
+
+
+const dialogTitle = "选择店员"
+const dialogVisible = ref(false)
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  username: undefined,
+  mobile: undefined,
+  status: undefined,
+  deptId: undefined,
+  roleId: 5,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await UserApi.getUserPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 处理部门被点击 */
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  await getList()
+}
+
+/** 打开弹窗 */
+const open = async () => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    await getList()
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 全选/全不选 */
+const handleCheckAll = (checked: boolean) => {
+  isCheckAll.value = checked
+  isIndeterminate.value = false
+
+  list.value.forEach((combinationActivity) => handleCheckOne(checked, combinationActivity, false))
+}
+
+/**
+ * 选中一行
+ * @param checked 是否选中
+ * @param combinationActivity 活动
+ * @param isCalcCheckAll 是否计算全选
+ */
+const handleCheckOne = (
+  checked: boolean,
+  combinationActivity,
+  isCalcCheckAll: boolean
+) => {
+  if (checked) {
+    checkedUsers.value.push(combinationActivity as never)
+    checkedStatus.value[combinationActivity.id] = true
+  } else {
+    const index = findCheckedIndex(combinationActivity)
+    if (index > -1) {
+      checkedUsers.value.splice(index, 1)
+      checkedStatus.value[combinationActivity.id] = false
+      isCheckAll.value = false
+    }
+  }
+
+  // 计算全选框状态
+  if (isCalcCheckAll) {
+    calculateIsCheckAll()
+  }
+}
+
+// 查找活动在已选中活动列表中的索引
+const findCheckedIndex = (user) =>
+  checkedUsers.value.findIndex((item) => item.id === user.id)
+
+// 计算全选框状态
+const calculateIsCheckAll = () => {
+  isCheckAll.value = list.value.every((user) => checkedStatus.value[user.id])
+  // 计算中间状态:不是全部选中 && 任意一个选中
+  isIndeterminate.value =
+    !isCheckAll.value && list.value.some((user) => checkedStatus.value[user.id])
+}
+
+/** 多选完成 */
+const handleEmitChange = () => {
+  // 关闭弹窗
+  dialogVisible.value = false
+  emits("change", [...checkedUsers.value])
+}
+
+/** 确认选择时的触发事件 */
+const emits = defineEmits<{
+  change: [CombinationActivityApi: any]
+}>()
+</script>

+ 16 - 0
src/views/mall/trade/delivery/pickUpStore/index.vue

@@ -103,6 +103,14 @@
           >
             编辑
           </el-button>
+          <el-button
+            v-hasPermi="['trade:delivery:pick-up-store:bind']"
+            link
+            type="primary"
+            @click="openFormBind(scope.row.id)"
+          >
+            绑定店员
+          </el-button>
           <el-button
             v-hasPermi="['trade:delivery:pick-up-store:delete']"
             link
@@ -117,10 +125,13 @@
   </ContentWrap>
   <!-- 表单弹窗:添加/修改 -->
   <DeliveryPickUpStoreForm ref="formRef" @success="getList" />
+  <!-- 表单弹窗:绑定店员 -->
+  <DeliveryPickUpStoreBindForm ref="formBindRef"/>
 </template>
 <script lang="ts" name="DeliveryPickUpStore" setup>
 import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
 import DeliveryPickUpStoreForm from './PickUpStoreForm.vue'
+import DeliveryPickUpStoreBindForm from './DeliveryPickUpStoreBindForm.vue'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 
@@ -146,6 +157,11 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
+const formBindRef = ref()
+const openFormBind = (id?: number) => {
+  formBindRef.value.open(id)
+}
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {

+ 14 - 6
src/views/mall/trade/order/form/OrderPickUpForm.vue

@@ -13,7 +13,7 @@
       </el-form-item>
     </el-form>
     <template #footer>
-      <el-button type="primary" :disabled="formLoading" @click="getOrderByPickUpVerifyCode">
+      <el-button type="primary" :disabled="formLoading" @click="getOrderByPickUpVerifyCodeClick">
         查询
       </el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
@@ -52,9 +52,14 @@ const formRef = ref() // 表单 Ref
 const orderDetails = ref<OrderVO>({})
 
 /** 打开弹窗 */
-const open = async () => {
+const open = async (pickUpVerifyCode: string) => {
   resetForm()
-  dialogVisible.value = true
+  if(pickUpVerifyCode != null){
+    formData.value.pickUpVerifyCode = pickUpVerifyCode;
+    await getOrderByPickUpVerifyCode()
+  }else{
+    dialogVisible.value = true
+  }
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -83,18 +88,21 @@ const resetForm = () => {
   formRef.value?.resetFields()
 }
 
-/** 查询核销码对应的订单 */
-const getOrderByPickUpVerifyCode = async () => {
+const getOrderByPickUpVerifyCodeClick = async () => {
   // 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
+  await getOrderByPickUpVerifyCode()
+}
 
+/** 查询核销码对应的订单 */
+const getOrderByPickUpVerifyCode = async () => {
   formLoading.value = true
   const data = await TradeOrderApi.getOrderByPickUpVerifyCode(formData.value.pickUpVerifyCode)
   formLoading.value = false
   if (data?.deliveryType !== DeliveryTypeEnum.PICK_UP.type) {
-    message.error('请输入正确的核销码')
+    message.error('未查询到订单')
     return
   }
   if (data?.status !== TradeOrderStatusEnum.UNDELIVERED.status) {