hook.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import dayjs from "dayjs";
  2. import { message } from "@/utils/message";
  3. import { addDialog } from "@/components/ReDialog";
  4. import type { PaginationProps } from "@pureadmin/table";
  5. import { deviceDetection } from "@pureadmin/utils";
  6. import {
  7. getDeviceList,
  8. openDoor,
  9. setTemperature,
  10. setVolume,
  11. getDoorRecords,
  12. type DoorRecordItem
  13. } from "@/api/device";
  14. import { getEnabledShops } from "@/api/shop";
  15. import { type Ref, ref, toRaw, reactive, onMounted } from "vue";
  16. import {
  17. ElForm,
  18. ElFormItem,
  19. ElInput,
  20. ElInputNumber,
  21. ElSelect,
  22. ElOption,
  23. ElMessageBox
  24. } from "element-plus";
  25. import type { DeviceItem, DeviceSearchForm } from "./types";
  26. export function useDevice(tableRef: Ref) {
  27. const form = reactive<DeviceSearchForm>({
  28. deviceId: "",
  29. shopId: "",
  30. status: "",
  31. storeName: ""
  32. });
  33. const formRef = ref();
  34. const ruleFormRef = ref();
  35. const dataList = ref([]);
  36. const loading = ref(true);
  37. const shopOptions = ref([]);
  38. // 操作按钮的加载状态
  39. const operatingIds = ref<Set<number>>(new Set());
  40. // 开关门记录相关
  41. const dialogVisible = ref(false);
  42. const recordLoading = ref(false);
  43. const recordList = ref<DoorRecordItem[]>([]);
  44. const currentDeviceId = ref<string>("");
  45. const recordPagination = reactive<PaginationProps>({
  46. total: 0,
  47. pageSize: 10,
  48. currentPage: 1,
  49. background: true
  50. });
  51. const pagination = reactive<PaginationProps>({
  52. total: 0,
  53. pageSize: 10,
  54. currentPage: 1,
  55. background: true
  56. });
  57. const columns: TableColumnList = [
  58. {
  59. label: "设备ID",
  60. prop: "deviceId",
  61. minWidth: 120
  62. },
  63. {
  64. label: "设备名称",
  65. prop: "deviceName",
  66. minWidth: 120
  67. },
  68. {
  69. label: "所属门店",
  70. prop: "shopName",
  71. minWidth: 120
  72. },
  73. {
  74. label: "货柜名称",
  75. prop: "storeName",
  76. minWidth: 100
  77. },
  78. {
  79. label: "温度(℃)",
  80. prop: "temperature",
  81. minWidth: 80
  82. },
  83. {
  84. label: "音量",
  85. prop: "volume",
  86. minWidth: 60
  87. },
  88. {
  89. label: "门状态",
  90. prop: "doorStatus",
  91. minWidth: 80,
  92. cellRenderer: ({ row }) => (
  93. <el-tag type={row.doorStatus === 1 ? "warning" : "success"}>
  94. {row.doorStatus === 1 ? "已开门" : "已关门"}
  95. </el-tag>
  96. )
  97. },
  98. {
  99. label: "状态",
  100. prop: "status",
  101. minWidth: 80,
  102. cellRenderer: ({ row }) => (
  103. <el-tag type={row.status === 1 ? "success" : "danger"}>
  104. {row.status === 1 ? "在线" : "离线"}
  105. </el-tag>
  106. )
  107. },
  108. {
  109. label: "最后在线时间",
  110. prop: "lastOnlineTime",
  111. minWidth: 160,
  112. formatter: ({ lastOnlineTime }) =>
  113. lastOnlineTime ? dayjs(lastOnlineTime).format("YYYY-MM-DD HH:mm:ss") : "-"
  114. },
  115. {
  116. label: "操作",
  117. fixed: "right",
  118. width: 200,
  119. slot: "operation"
  120. }
  121. ];
  122. // 搜索
  123. async function onSearch() {
  124. loading.value = true;
  125. try {
  126. const searchParams: any = {
  127. page: pagination.currentPage,
  128. pageSize: pagination.pageSize
  129. };
  130. // 只添加非空的搜索条件
  131. if (form.deviceId) searchParams.deviceId = form.deviceId;
  132. if (form.shopId) searchParams.shopId = form.shopId;
  133. if (form.status) searchParams.status = form.status;
  134. if (form.storeName) searchParams.storeName = form.storeName;
  135. const { data } = await getDeviceList(searchParams);
  136. dataList.value = data.list;
  137. pagination.total = data.total;
  138. } catch (error) {
  139. console.error("获取设备列表失败:", error);
  140. dataList.value = [];
  141. pagination.total = 0;
  142. } finally {
  143. setTimeout(() => {
  144. loading.value = false;
  145. }, 300);
  146. }
  147. }
  148. // 重置表单
  149. const resetForm = formEl => {
  150. if (!formEl) return;
  151. formEl.resetFields();
  152. pagination.currentPage = 1;
  153. onSearch();
  154. };
  155. // 分页
  156. function handleSizeChange(val: number) {
  157. pagination.pageSize = val;
  158. onSearch();
  159. }
  160. function handleCurrentChange(val: number) {
  161. pagination.currentPage = val;
  162. onSearch();
  163. }
  164. // 远程开门
  165. async function handleOpenDoor(row: DeviceItem) {
  166. try {
  167. const deviceName = row.deviceName || row.storeName || `设备${row.deviceId}`;
  168. await ElMessageBox.confirm(`确认要远程开启 ${deviceName} 的门吗?`, "系统提示", {
  169. confirmButtonText: "确定",
  170. cancelButtonText: "取消",
  171. type: "warning"
  172. });
  173. // 添加 loading 状态
  174. operatingIds.value.add(row.id);
  175. // 传递 doorIndex 参数,默认为 A 门
  176. const res = await openDoor(row.id, { doorIndex: "A" });
  177. if (res.code === 0) {
  178. message(`已发送开门指令到设备 ${deviceName}`, { type: "success" });
  179. // 开门成功后刷新列表,更新门状态
  180. await onSearch();
  181. } else {
  182. message(res.message || "开门失败", { type: "error" });
  183. }
  184. } catch (error) {
  185. // 用户取消或请求失败
  186. if (error !== "cancel") {
  187. console.error("开门失败:", error);
  188. message("操作失败", { type: "error" });
  189. }
  190. } finally {
  191. // 移除 loading 状态
  192. operatingIds.value.delete(row.id);
  193. }
  194. }
  195. // 设置温度
  196. const tempForm = reactive({ temperature: 0 });
  197. function handleSetTemperature(row: DeviceItem) {
  198. tempForm.temperature = row.temperature;
  199. addDialog({
  200. title: `设置 ${row.deviceName} 的温度`,
  201. width: "30%",
  202. draggable: true,
  203. closeOnClickModal: false,
  204. fullscreen: deviceDetection(),
  205. contentRenderer: () => (
  206. <ElForm ref={ruleFormRef} model={tempForm} label-width="80px">
  207. <ElFormItem
  208. label="温度 (℃)"
  209. prop="temperature"
  210. rules={[
  211. { required: true, message: "请输入温度", trigger: "blur" },
  212. {
  213. type: "number",
  214. min: -30,
  215. max: 30,
  216. message: "温度范围为 -30℃ 到 30℃",
  217. trigger: "blur"
  218. }
  219. ]}
  220. >
  221. <ElInputNumber
  222. v-model={tempForm.temperature}
  223. min={-30}
  224. max={30}
  225. step={0.5}
  226. precision={1}
  227. class="w-full!"
  228. placeholder="请输入温度值"
  229. />
  230. </ElFormItem>
  231. </ElForm>
  232. ),
  233. beforeSure: async (done) => {
  234. try {
  235. // 添加 loading 状态
  236. operatingIds.value.add(row.id);
  237. const res = await setTemperature(row.id, { temperature: tempForm.temperature });
  238. if (res.code === 0) {
  239. message(`已设置温度为 ${tempForm.temperature}℃`, { type: "success" });
  240. done();
  241. // 温度设置成功后刷新列表,更新温度显示
  242. await onSearch();
  243. } else {
  244. message(res.message || "设置失败", { type: "error" });
  245. }
  246. } catch (error) {
  247. console.error("设置温度失败:", error);
  248. message("设置失败", { type: "error" });
  249. } finally {
  250. // 移除 loading 状态
  251. operatingIds.value.delete(row.id);
  252. }
  253. }
  254. });
  255. }
  256. // 设置音量
  257. const volumeForm = reactive({ volume: 0 });
  258. function handleSetVolume(row: DeviceItem) {
  259. volumeForm.volume = row.volume;
  260. addDialog({
  261. title: `设置 ${row.deviceName} 的音量`,
  262. width: "30%",
  263. draggable: true,
  264. closeOnClickModal: false,
  265. fullscreen: deviceDetection(),
  266. contentRenderer: () => (
  267. <ElForm ref={ruleFormRef} model={volumeForm} label-width="80px">
  268. <ElFormItem
  269. label="音量"
  270. prop="volume"
  271. rules={[
  272. { required: true, message: "请输入音量", trigger: "blur" },
  273. {
  274. type: "number",
  275. min: 0,
  276. max: 100,
  277. message: "音量范围为 0 到 100",
  278. trigger: "blur"
  279. }
  280. ]}
  281. >
  282. <ElInputNumber
  283. v-model={volumeForm.volume}
  284. min={0}
  285. max={100}
  286. step={5}
  287. class="w-full!"
  288. placeholder="请输入音量值 (0-100)"
  289. />
  290. </ElFormItem>
  291. </ElForm>
  292. ),
  293. beforeSure: async (done) => {
  294. try {
  295. // 添加 loading 状态
  296. operatingIds.value.add(row.id);
  297. const res = await setVolume(row.id, { volume: volumeForm.volume });
  298. if (res.code === 0) {
  299. message(`已设置音量为 ${volumeForm.volume}`, { type: "success" });
  300. done();
  301. // 音量设置成功后刷新列表,更新音量显示
  302. await onSearch();
  303. } else {
  304. message(res.message || "设置失败", { type: "error" });
  305. }
  306. } catch (error) {
  307. console.error("设置音量失败:", error);
  308. message("设置失败", { type: "error" });
  309. } finally {
  310. // 移除 loading 状态
  311. operatingIds.value.delete(row.id);
  312. }
  313. }
  314. });
  315. }
  316. const recordColumns: TableColumnList = [
  317. {
  318. label: "活动 ID",
  319. prop: "activityId",
  320. minWidth: 180
  321. },
  322. {
  323. label: "用户 ID",
  324. prop: "userId",
  325. minWidth: 100
  326. },
  327. {
  328. label: "门索引",
  329. prop: "doorIndex",
  330. minWidth: 80
  331. },
  332. {
  333. label: "类型",
  334. prop: "openType",
  335. minWidth: 80,
  336. cellRenderer: ({ row }) => (
  337. <el-tag type={row.openType === "IN" ? "warning" : "info"}>
  338. {row.openType === "IN" ? "上货" : "消费"}
  339. </el-tag>
  340. )
  341. },
  342. {
  343. label: "门状态",
  344. prop: "doorStatus",
  345. minWidth: 100,
  346. cellRenderer: ({ row }) => (
  347. <el-tag
  348. type={
  349. row.doorStatus === "OPENED" ? "danger" :
  350. row.doorStatus === "CLOSED" ? "success" : "info"
  351. }
  352. >
  353. {
  354. row.doorStatus === "OPENED" ? "已开门" :
  355. row.doorStatus === "CLOSED" ? "已关门" : "异常"
  356. }
  357. </el-tag>
  358. )
  359. },
  360. {
  361. label: "是否有消费",
  362. prop: "nobuy",
  363. minWidth: 100,
  364. cellRenderer: ({ row }) => (
  365. <el-tag type={row.nobuy === 0 ? "success" : "info"}>
  366. {row.nobuy === 0 ? "有消费" : "无消费"}
  367. </el-tag>
  368. )
  369. },
  370. {
  371. label: "开门时间",
  372. prop: "openTime",
  373. minWidth: 160,
  374. formatter: ({ openTime }) => dayjs(openTime).format("YYYY-MM-DD HH:mm:ss")
  375. },
  376. {
  377. label: "关门时间",
  378. prop: "closeTime",
  379. minWidth: 160,
  380. formatter: ({ closeTime }) =>
  381. closeTime ? dayjs(closeTime).format("YYYY-MM-DD HH:mm:ss") : "-"
  382. },
  383. {
  384. label: "持续时长 (秒)",
  385. prop: "duration",
  386. minWidth: 100
  387. },
  388. {
  389. label: "来源",
  390. prop: "source",
  391. minWidth: 100,
  392. cellRenderer: ({ row }) => (
  393. <el-tag type={row.source === "MINIAPP" ? "primary" : "info"} size="small">
  394. {row.source === "MINIAPP" ? "小程序" : row.source === "ADMIN" ? "管理后台" : row.source || "-"}
  395. </el-tag>
  396. )
  397. }
  398. ];
  399. // 查询开关门记录
  400. async function fetchDoorRecords() {
  401. if (!currentDeviceId.value) return;
  402. recordLoading.value = true;
  403. try {
  404. const res = await getDoorRecords(currentDeviceId.value, {
  405. page: recordPagination.currentPage,
  406. pageSize: recordPagination.pageSize
  407. });
  408. if (res.code === 0 && res.data) {
  409. recordList.value = res.data.list || [];
  410. recordPagination.total = res.data.total || 0;
  411. }
  412. } catch (error) {
  413. console.error("查询开关门记录失败:", error);
  414. message("查询失败", { type: "error" });
  415. } finally {
  416. recordLoading.value = false;
  417. }
  418. }
  419. // 显示开关门记录弹窗
  420. function showDoorRecords(row: DeviceItem) {
  421. currentDeviceId.value = row.deviceId;
  422. recordPagination.currentPage = 1;
  423. recordPagination.pageSize = 10;
  424. dialogVisible.value = true;
  425. fetchDoorRecords();
  426. }
  427. // 记录分页大小变化
  428. function handleRecordSizeChange(val: number) {
  429. recordPagination.pageSize = val;
  430. recordPagination.currentPage = 1;
  431. fetchDoorRecords();
  432. }
  433. // 记录页码变化
  434. function handleRecordCurrentChange(val: number) {
  435. recordPagination.currentPage = val;
  436. fetchDoorRecords();
  437. }
  438. async function fetchShopOptions() {
  439. try {
  440. const { data } = await getEnabledShops();
  441. shopOptions.value = data || [];
  442. } catch (error) {
  443. console.error("获取门店列表失败:", error);
  444. }
  445. }
  446. onMounted(async () => {
  447. await fetchShopOptions();
  448. onSearch();
  449. });
  450. return {
  451. form,
  452. loading,
  453. columns,
  454. dataList,
  455. pagination,
  456. shopOptions,
  457. onSearch,
  458. resetForm,
  459. handleOpenDoor,
  460. handleSetTemperature,
  461. handleSetVolume,
  462. handleSizeChange,
  463. handleCurrentChange,
  464. operatingIds,
  465. // 开关门记录相关
  466. dialogVisible,
  467. recordLoading,
  468. recordList,
  469. recordColumns,
  470. recordPagination,
  471. showDoorRecords,
  472. handleRecordSizeChange,
  473. handleRecordCurrentChange
  474. };
  475. }