index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. <template>
  2. <doc-alert title="【交易】交易订单" url="https://doc.iocoder.cn/mall/trade-order/" />
  3. <doc-alert title="【交易】购物车" url="https://doc.iocoder.cn/mall/trade-cart/" />
  4. <!-- 搜索 -->
  5. <ContentWrap>
  6. <el-form
  7. ref="queryFormRef"
  8. :inline="true"
  9. :model="queryParams"
  10. class="-mb-15px"
  11. label-width="68px"
  12. >
  13. <el-form-item label="创建时间" prop="createTime">
  14. <el-date-picker
  15. v-model="queryParams.createTime"
  16. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  17. class="!w-280px"
  18. end-placeholder="自定义时间"
  19. start-placeholder="自定义时间"
  20. type="daterange"
  21. value-format="YYYY-MM-DD HH:mm:ss"
  22. />
  23. </el-form-item>
  24. <el-form-item label="自提门店" prop="pickUpStoreId">
  25. <el-select
  26. v-model="queryParams.pickUpStoreId"
  27. class="!w-280px"
  28. placeholder="全部"
  29. @change="handleQuery"
  30. >
  31. <el-option
  32. v-for="item in pickUpStoreList"
  33. :key="item.id"
  34. :label="item.name"
  35. :value="item.id"
  36. />
  37. </el-select>
  38. </el-form-item>
  39. <el-form-item label="聚合搜索">
  40. <el-input
  41. v-show="true"
  42. v-model="queryParams[queryType.queryParam]"
  43. class="!w-280px"
  44. clearable
  45. placeholder="请输入"
  46. :type="queryType.queryParam === 'userId' ? 'number' : 'text'"
  47. >
  48. <template #prepend>
  49. <el-select
  50. v-model="queryType.queryParam"
  51. class="!w-110px"
  52. placeholder="全部"
  53. @change="inputChangeSelect"
  54. >
  55. <el-option
  56. v-for="dict in dynamicSearchList"
  57. :key="dict.value"
  58. :label="dict.label"
  59. :value="dict.value"
  60. />
  61. </el-select>
  62. </template>
  63. </el-input>
  64. </el-form-item>
  65. <el-form-item>
  66. <el-button @click="handleQuery">
  67. <Icon class="mr-5px" icon="ep:search" />
  68. 搜索
  69. </el-button>
  70. <el-button @click="resetQuery">
  71. <Icon class="mr-5px" icon="ep:refresh" />
  72. 重置
  73. </el-button>
  74. <el-button
  75. @click="handlePickup"
  76. type="success"
  77. plain
  78. v-hasPermi="['trade:order:pick-up']"
  79. :disabled="isUse"
  80. >
  81. <Icon class="mr-5px" icon="ep:check" />
  82. 核销
  83. </el-button>
  84. <el-button type="primary" @click="connectToSerialPort" :disabled="serialPort || isUse">
  85. 连接扫描枪
  86. </el-button>
  87. <el-button type="danger" @click="cutPort" :disabled="!serialPort || isUse">
  88. 断开扫描枪
  89. </el-button>
  90. </el-form-item>
  91. </el-form>
  92. </ContentWrap>
  93. <!-- 统计卡片 -->
  94. <el-row :gutter="16" class="summary">
  95. <el-col :sm="6" :xs="12" v-loading="loading">
  96. <SummaryCard
  97. title="订单数量"
  98. icon="icon-park-outline:transaction-order"
  99. icon-color="bg-blue-100"
  100. icon-bg-color="text-blue-500"
  101. :value="summary?.orderCount || 0"
  102. />
  103. </el-col>
  104. <el-col :sm="6" :xs="12" v-loading="loading">
  105. <SummaryCard
  106. title="订单金额"
  107. icon="streamline:money-cash-file-dollar-common-money-currency-cash-file"
  108. icon-color="bg-purple-100"
  109. icon-bg-color="text-purple-500"
  110. prefix="¥"
  111. :decimals="2"
  112. :value="fenToYuan(summary?.orderPayPrice || 0)"
  113. />
  114. </el-col>
  115. <el-col :sm="6" :xs="12" v-loading="loading">
  116. <SummaryCard
  117. title="退款单数"
  118. icon="heroicons:receipt-refund"
  119. icon-color="bg-yellow-100"
  120. icon-bg-color="text-yellow-500"
  121. :value="summary?.afterSaleCount || 0"
  122. />
  123. </el-col>
  124. <el-col :sm="6" :xs="12" v-loading="loading">
  125. <SummaryCard
  126. title="退款金额"
  127. icon="ri:refund-2-line"
  128. icon-color="bg-green-100"
  129. icon-bg-color="text-green-500"
  130. prefix="¥"
  131. :decimals="2"
  132. :value="fenToYuan(summary?.afterSalePrice || 0)"
  133. />
  134. </el-col>
  135. </el-row>
  136. <!-- 列表 -->
  137. <ContentWrap>
  138. <el-table v-loading="loading" :data="list">
  139. <el-table-column label="订单号" align="center" prop="no" min-width="180" />
  140. <el-table-column label="用户信息" align="center" prop="user.nickname" min-width="80" />
  141. <el-table-column
  142. label="推荐人信息"
  143. align="center"
  144. prop="brokerageUser.nickname"
  145. min-width="100"
  146. />
  147. <el-table-column label="商品信息" align="center" prop="spuName" min-width="300">
  148. <template #default="{ row }">
  149. <div class="flex items-center" v-for="item in row.items" :key="item.id">
  150. <el-image
  151. :src="item.picUrl"
  152. class="mr-10px h-30px w-30px flex-shrink-0"
  153. :preview-src-list="[item.picUrl]"
  154. preview-teleported
  155. />
  156. <span class="mr-10px">{{ item.spuName }}</span>
  157. <div class="flex flex-col flex-wrap gap-1">
  158. <el-tag
  159. v-for="property in item.properties"
  160. :key="property.propertyId"
  161. class="mr-10px"
  162. >
  163. {{ property.propertyName }}: {{ property.valueName }}
  164. </el-tag>
  165. <span>{{ floatToFixed2(item.price) }} 元 x {{ item.count }}</span>
  166. </div>
  167. </div>
  168. </template>
  169. </el-table-column>
  170. <el-table-column
  171. label="实付金额(元)"
  172. align="center"
  173. prop="payPrice"
  174. min-width="110"
  175. :formatter="fenToYuanFormat"
  176. />
  177. <el-table-column label="核销员" align="center" prop="storeStaffName" min-width="70" />
  178. <el-table-column label="核销门店" align="center" prop="pickUpStoreId" min-width="80">
  179. <template #default="{ row }">
  180. {{ pickUpStoreList.find((p) => p.id === row.pickUpStoreId)?.name }}
  181. </template>
  182. </el-table-column>
  183. <el-table-column label="支付状态" align="center" prop="payStatus" min-width="80">
  184. <template #default="{ row }">
  185. <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.payStatus || false" />
  186. </template>
  187. </el-table-column>
  188. <el-table-column align="center" label="订单状态" prop="status" width="120">
  189. <template #default="{ row }">
  190. <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="row.status" />
  191. </template>
  192. </el-table-column>
  193. <el-table-column
  194. label="下单时间"
  195. align="center"
  196. prop="createTime"
  197. min-width="170"
  198. :formatter="dateFormatter"
  199. />
  200. </el-table>
  201. <!-- 分页 -->
  202. <Pagination
  203. v-model:limit="queryParams.pageSize"
  204. v-model:page="queryParams.pageNo"
  205. :total="total"
  206. @pagination="getList"
  207. />
  208. </ContentWrap>
  209. <!-- 各种操作的弹窗 -->
  210. <OrderPickUpForm ref="pickUpForm" @success="getList" />
  211. </template>
  212. <script lang="ts" setup>
  213. import type { FormInstance } from 'element-plus'
  214. import * as TradeOrderApi from '@/api/mall/trade/order'
  215. import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
  216. import { DICT_TYPE } from '@/utils/dict'
  217. import { fenToYuan, floatToFixed2 } from '@/utils'
  218. import { fenToYuanFormat } from '@/utils/formatter'
  219. import SummaryCard from '@/components/SummaryCard/index.vue'
  220. import { dateFormatter } from '@/utils/formatTime'
  221. import { DeliveryTypeEnum } from '@/utils/constants'
  222. import { TradeOrderSummaryRespVO } from '@/api/mall/trade/order'
  223. import { DeliveryPickUpStoreVO } from '@/api/mall/trade/delivery/pickUpStore'
  224. import OrderPickUpForm from '@/views/mall/trade/order/form/OrderPickUpForm.vue'
  225. import { ref, onMounted } from 'vue'
  226. import { useUserStore } from '@/store/modules/user'
  227. const message = useMessage() // 消息弹窗
  228. const port = ref('')
  229. const ports = ref([])
  230. const reader = ref('')
  231. defineOptions({ name: 'PickUpOrder' })
  232. const loading = ref(true) // 列表的加载中
  233. const total = ref(2) // 列表的总页数
  234. const list = ref<TradeOrderApi.OrderVO[]>([]) // 列表的数据
  235. const queryFormRef = ref<FormInstance>() // 搜索的表单
  236. const INIT_QUERY_PARAMS = {
  237. // 页数
  238. pageNo: 1,
  239. // 每页显示数量
  240. pageSize: 10,
  241. // 创建时间
  242. createTime: undefined,
  243. // 配送方式
  244. deliveryType: DeliveryTypeEnum.PICK_UP.type,
  245. // 自提门店
  246. pickUpStoreId: -1
  247. } // 初始表单参数
  248. const queryParams = ref({ ...INIT_QUERY_PARAMS }) // 表单搜索
  249. const queryType = reactive({ queryParam: 'no' }) // 订单搜索类型 queryParam
  250. const summary = ref<TradeOrderSummaryRespVO>() // 订单统计数据
  251. const serialPort = ref(false) // 是否连接扫码枪
  252. const isUse = ref(true) // 是否可核销
  253. // 订单聚合搜索 select 类型配置(动态搜索)
  254. const dynamicSearchList = ref([
  255. { value: 'no', label: '订单号' },
  256. { value: 'userId', label: '用户UID' },
  257. { value: 'userNickname', label: '用户昵称' },
  258. { value: 'userMobile', label: '用户电话' }
  259. ])
  260. /**
  261. * 聚合搜索切换查询对象时触发
  262. * @param val
  263. */
  264. const inputChangeSelect = (val: string) => {
  265. dynamicSearchList.value
  266. .filter((item) => item.value !== val)
  267. ?.forEach((item) => {
  268. // 清除集合搜索无用属性
  269. if (queryParams.value.hasOwnProperty(item.value)) {
  270. delete queryParams.value[item.value]
  271. }
  272. })
  273. }
  274. /** 查询列表 */
  275. const getList = async () => {
  276. loading.value = true
  277. try {
  278. // 统计
  279. summary.value = await TradeOrderApi.getOrderSummary(unref(queryParams))
  280. // 分页
  281. const data = await TradeOrderApi.getOrderPage(unref(queryParams))
  282. list.value = data.list
  283. total.value = data.total
  284. } finally {
  285. loading.value = false
  286. }
  287. }
  288. /** 搜索按钮操作 */
  289. const handleQuery = async () => {
  290. queryParams.value.pageNo = 1
  291. await getList()
  292. }
  293. /** 重置按钮操作 */
  294. const resetQuery = () => {
  295. queryFormRef.value?.resetFields()
  296. queryParams.value = { ...INIT_QUERY_PARAMS }
  297. if (pickUpStoreList.value.length > 0) {
  298. queryParams.value.pickUpStoreId = pickUpStoreList.value[0].id
  299. }
  300. handleQuery()
  301. }
  302. /** 自提门店精简列表 */
  303. const pickUpStoreList = ref<DeliveryPickUpStoreVO[]>([])
  304. const getPickUpStoreList = async () => {
  305. pickUpStoreList.value = await PickUpStoreApi.getSimpleDeliveryPickUpStoreList()
  306. // 移除自己无法核销的门店
  307. const userId = useUserStore().getUser.id
  308. pickUpStoreList.value = pickUpStoreList.value.filter((item) =>
  309. item.verifyUserIds?.includes(userId)
  310. )
  311. }
  312. /** 显示核销表单 */
  313. const pickUpForm = ref()
  314. const handlePickup = () => {
  315. pickUpForm.value.open()
  316. }
  317. /** 连接扫码枪 */
  318. const connectToSerialPort = async () => {
  319. try {
  320. // 判断浏览器支持串口通信
  321. if (
  322. 'serial' in navigator &&
  323. navigator.serial != null &&
  324. typeof navigator.serial === 'object' &&
  325. 'requestPort' in navigator.serial
  326. ) {
  327. // 提示用户选择一个串口
  328. port.value = await navigator.serial.requestPort()
  329. } else {
  330. message.error('浏览器不支持扫码枪连接,请更换浏览器重试')
  331. return
  332. }
  333. // 获取用户之前授予该网站访问权限的所有串口。
  334. ports.value = await navigator.serial.getPorts()
  335. // console.log(port.value, ports.value);
  336. // console.log(port.value)
  337. // 等待串口打开
  338. await port.value.open({ baudRate: 9600, dataBits: 8, stopBits: 2 })
  339. // console.log(typeof port.value);
  340. message.success('成功连接扫码枪')
  341. serialPort.value = true
  342. // readData(port.value);
  343. readData()
  344. } catch (error) {
  345. // 处理连接串口出错的情况
  346. console.log('Error connecting to serial port:', error)
  347. }
  348. }
  349. /** 监听扫码枪输入 */
  350. const readData = async () => {
  351. reader.value = port.value.readable.getReader()
  352. let data = '' //扫码数据
  353. // 监听来自串口的数据
  354. while (true) {
  355. const { value, done } = await reader.value.read()
  356. if (done) {
  357. // 允许稍后关闭串口
  358. reader.value.releaseLock()
  359. break
  360. }
  361. // 获取发送的数据
  362. const serialData = new TextDecoder().decode(value)
  363. data = `${data}${serialData}`
  364. if (serialData.includes('\r')) {
  365. //读取结束
  366. let codeData = data.replace('\r', '')
  367. data = '' //清空下次读取不会叠加
  368. console.log(`二维码数据:${codeData}`)
  369. //处理拿到数据逻辑
  370. pickUpForm.value.open(codeData)
  371. }
  372. }
  373. }
  374. /** 断开扫码枪 */
  375. const cutPort = async () => {
  376. if (port.value !== '') {
  377. await reader.value.cancel()
  378. await port.value.close()
  379. port.value = ''
  380. console.log('断开扫码枪连接')
  381. message.success('已成功断开扫码枪连接')
  382. serialPort.value = false
  383. } else {
  384. message.warning('请先连接或打开扫码枪')
  385. }
  386. }
  387. /** 初始化 **/
  388. onMounted(async () => {
  389. await getPickUpStoreList()
  390. if (pickUpStoreList.value.length === 0) {
  391. message.error('当前登录人没绑定任何自提点')
  392. loading.value = false
  393. isUse.value = true
  394. return
  395. }
  396. // 查询
  397. queryParams.value.pickUpStoreId = pickUpStoreList.value[0].id
  398. isUse.value = false
  399. await getList()
  400. })
  401. </script>
  402. <style lang="scss" scoped>
  403. :deep(.order-table-col > .cell) {
  404. padding: 0;
  405. }
  406. .summary {
  407. .el-col {
  408. margin-bottom: 1rem;
  409. }
  410. }
  411. </style>