diff --git a/apps/web-antd/src/views/ops/area/index.vue b/apps/web-antd/src/views/ops/area/index.vue index 669de5a4e..48ea565ab 100644 --- a/apps/web-antd/src/views/ops/area/index.vue +++ b/apps/web-antd/src/views/ops/area/index.vue @@ -2,24 +2,31 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { OpsAreaApi } from '#/api/ops/area'; -import { ref } from 'vue'; +import { nextTick, ref } from 'vue'; -import { - confirm as confirmModal, - Page, - useVbenDrawer, - useVbenModal, -} from '@vben/common-ui'; +import { confirm as confirmModal, Page, useVbenModal } from '@vben/common-ui'; +import { downloadFileFromBlob } from '@vben/utils'; -import { message } from 'ant-design-vue'; +import { message, Switch, Tag } from 'ant-design-vue'; +import JSZip from 'jszip'; +import QRCodeLib from 'qrcode'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { deleteArea, getAreaTree, updateArea } from '#/api/ops/area'; import { $t } from '#/locales'; -import { useGridColumns, useGridFormSchema } from './data'; -import DeviceBindDrawer from './modules/device-bind-drawer.vue'; +import { + AREA_LEVEL_OPTIONS, + AREA_LEVEL_TAG_COLORS, + AREA_TYPE_OPTIONS, + AREA_TYPE_TAG_COLORS, + FUNCTION_TYPE_OPTIONS, + FUNCTION_TYPE_TAG_COLORS, + useGridColumns, + useGridFormSchema, +} from './data'; import Form from './modules/form.vue'; +import QrcodeModal from './modules/qrcode-modal.vue'; defineOptions({ name: 'OpsBusArea' }); @@ -28,20 +35,23 @@ const [FormModal, formModalApi] = useVbenModal({ destroyOnClose: true, }); -const [DeviceBindDrawerComp, deviceBindDrawerApi] = useVbenDrawer({ - connectedComponent: DeviceBindDrawer, +const [QrcodeModalComp, qrcodeModalApi] = useVbenModal({ + connectedComponent: QrcodeModal, destroyOnClose: true, }); /** 树形展开/收缩 */ const isExpanded = ref(true); -function handleExpand() { +async function handleExpand() { isExpanded.value = !isExpanded.value; - gridApi.grid?.setAllTreeExpand(isExpanded.value); + await gridApi.grid?.setAllTreeExpand(isExpanded.value); } -function handleRefresh() { - gridApi.query(); +/** 刷新表格 */ +async function handleRefresh() { + await gridApi.query(); + await nextTick(); + await gridApi.grid?.setAllTreeExpand(isExpanded.value); } function handleCreate() { @@ -56,6 +66,123 @@ function handleEdit(row: OpsAreaApi.BusArea) { formModalApi.setData(row).open(); } +/** + * 基于扁平列表(带 parentId)构建 id → 全路径名称 映射 + * getData() 返回扁平数据(transform: true),通过 parentId 向上回溯拼接全名 + */ +function buildFullNameMap(list: OpsAreaApi.BusArea[]): Map { + const nodeMap = new Map(); + for (const item of list) { + if (item.id !== null && item.id !== undefined) + nodeMap.set(item.id, item); + } + + const cache = new Map(); + + const visiting = new Set(); + + function resolve(id: number): string { + if (cache.has(id)) return cache.get(id)!; + if (visiting.has(id)) return ''; // 防止循环引用导致无限递归 + const node = nodeMap.get(id); + if (!node) return ''; + visiting.add(id); + const parentId = node.parentId; + const parentName = + parentId !== null && + parentId !== undefined && + parentId !== 0 && + nodeMap.has(parentId) + ? resolve(parentId) + : ''; + visiting.delete(id); + const fullName = parentName + ? `${parentName}/${node.areaName}` + : node.areaName; + cache.set(id, fullName); + return fullName; + } + + for (const item of list) { + if (item.id !== null && item.id !== undefined) resolve(item.id); + } + return cache; +} + +function handleQrcode(row: OpsAreaApi.BusArea) { + const data = getGridData(); + const fullNameMap = buildFullNameMap(data); + const fullName = fullNameMap.get(row.id!) || row.areaName; + qrcodeModalApi + .setData({ + id: row.id, + areaName: row.areaName, + areaCode: row.areaCode, + fullName, + }) + .open(); +} + +/** 获取当前表格扁平数据 */ +function getGridData(): OpsAreaApi.BusArea[] { + return (gridApi.grid?.getData() as OpsAreaApi.BusArea[] | undefined) || []; +} + +interface FlatAreaItem extends OpsAreaApi.BusArea { + fullName: string; +} + +/** 批量导出二维码 ZIP */ +async function handleBatchExportQrcode() { + const data = getGridData(); + if (data.length === 0) { + message.warning('暂无数据可导出'); + return; + } + const fullNameMap = buildFullNameMap(data); + const flatList: FlatAreaItem[] = data.map((item) => ({ + ...item, + fullName: fullNameMap.get(item.id!) || item.areaName, + })); + if (flatList.length === 0) { + message.warning('暂无数据可导出'); + return; + } + + const hideLoading = message.loading({ + content: `正在生成 ${flatList.length} 个二维码...`, + duration: 0, + }); + + try { + const zip = new JSZip(); + for (const item of flatList) { + const qrContent = JSON.stringify({ + type: 'AREA', + id: item.id, + code: item.areaCode, + name: item.fullName, + }); + const dataUrl = await QRCodeLib.toDataURL(qrContent, { + width: 300, + margin: 2, + }); + // 去掉 data:image/png;base64, 前缀 + const base64Data = dataUrl.split(',')[1]; + if (!base64Data) continue; + const fileName = `${item.areaName}_${item.areaCode || 'nocode'}.png`; + zip.file(fileName, base64Data, { base64: true }); + } + const blob = await zip.generateAsync({ type: 'blob' }); + downloadFileFromBlob({ fileName: '区域二维码.zip', source: blob }); + message.success('导出成功'); + } catch { + message.error('导出失败,请重试'); + } finally { + hideLoading(); + } +} + async function handleDelete(row: OpsAreaApi.BusArea) { const hideLoading = message.loading({ content: $t('ui.actionMessage.deleting', [row.areaName]), @@ -64,7 +191,7 @@ async function handleDelete(row: OpsAreaApi.BusArea) { try { await deleteArea(row.id!); message.success($t('ui.actionMessage.deleteSuccess', [row.areaName])); - handleRefresh(); + await handleRefresh(); } catch (error: any) { const msg = error?.message || error?.data?.msg || '删除失败'; message.error(msg); @@ -73,14 +200,17 @@ async function handleDelete(row: OpsAreaApi.BusArea) { } } +/** 状态开关切换(带确认弹窗) */ async function handleToggleActive(row: OpsAreaApi.BusArea) { const next = !row.isActive; - if (!next) { - try { - await confirmModal('确认停用该区域吗?停用后相关工单策略可能受影响。'); - } catch { - return; - } + try { + await confirmModal( + next + ? `确认启用区域【${row.areaName}】吗?` + : `确认停用区域【${row.areaName}】吗?停用后相关工单策略可能受影响。`, + ); + } catch { + return; } const hideLoading = message.loading({ content: next ? '启用中...' : '停用中...', @@ -89,7 +219,7 @@ async function handleToggleActive(row: OpsAreaApi.BusArea) { try { await updateArea({ ...row, isActive: next }); message.success(next ? '已启用' : '已停用'); - handleRefresh(); + await handleRefresh(); } catch (error: any) { const msg = error?.message || error?.data?.msg || '操作失败'; message.error(msg); @@ -98,15 +228,6 @@ async function handleToggleActive(row: OpsAreaApi.BusArea) { } } -function handleBindDevice(row: OpsAreaApi.BusArea) { - deviceBindDrawerApi.setData(row).open(); -} - -const hasChildren = (row: OpsAreaApi.BusArea) => { - const c = (row as any).children; - return Array.isArray(c) && c.length > 0; -}; - const [Grid, gridApi] = useVbenVxeGrid({ formOptions: { schema: useGridFormSchema(), @@ -117,15 +238,13 @@ const [Grid, gridApi] = useVbenVxeGrid({ pagerConfig: { enabled: false }, proxyConfig: { ajax: { - query: async (_: any, formValues: Record) => { + query: async (_params: any, formValues: Record) => { const params: OpsAreaApi.AreaTreeQuery = {}; if (formValues?.name) params.name = formValues.name; if (formValues?.areaType) params.areaType = formValues.areaType; if (formValues?.isActive !== undefined && formValues?.isActive !== '') params.isActive = formValues.isActive; - const data = await getAreaTree(params); - const list = Array.isArray(data) ? data : ((data as any)?.list ?? []); - return { list, total: list.length }; + return await getAreaTree(params); }, }, }, @@ -141,19 +260,26 @@ const [Grid, gridApi] = useVbenVxeGrid({ parentField: 'parentId', rowField: 'id', transform: true, - expandAll: true, reserve: true, }, } as VxeTableGridOptions, + gridEvents: { + cellClick: ({ row, column, triggerTreeNode }: any) => { + if (triggerTreeNode) return; + // 状态开关列和操作列不触发展开 + if (column.field === 'isActive' || !column.field) return; + gridApi.grid?.toggleTreeExpand(row); + }, + }, });