From ed2eae1afa8aa90d4c9581cf978bceb1d72d9532 Mon Sep 17 00:00:00 2001 From: hwj Date: Mon, 9 Feb 2026 16:35:01 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E8=8F=9C=E5=8D=95=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E6=B7=BB=E5=8A=A0=E8=8B=B1=E6=96=87=E9=80=82=E9=85=8D?= =?UTF-8?q?/=E5=A4=A7=E5=B1=8F=E7=AE=A1=E7=90=86-=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E4=B8=8B=E6=8B=89=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/iot/device/index.ts | 2 +- src/api/system/menu/index.ts | 1 + src/locales/en.ts | 2 + src/locales/zh-CN.ts | 2 + src/utils/routerHelper.ts | 5 +- src/views/report/dashboardList/index.vue | 243 +++++++++++++++--- .../report/dashboardPage/dashboard1/index.vue | 14 +- src/views/system/menu/MenuForm.vue | 5 + 8 files changed, 228 insertions(+), 46 deletions(-) diff --git a/src/api/iot/device/index.ts b/src/api/iot/device/index.ts index d28ebe7e..e313378e 100644 --- a/src/api/iot/device/index.ts +++ b/src/api/iot/device/index.ts @@ -103,7 +103,7 @@ export const DeviceApi = { }, // 批量获取设备属性列表 - getDeviceAttributeBatchList: async (params: { deviceIds: string; orgId?: number | string }) => { + getDeviceAttributeBatchList: async (params: { goviewId: number | string; orgId?: number | string }) => { return await request.get({ url: `/iot/device/device-attribute/batchList`, params }) }, // 修改物联设备 diff --git a/src/api/system/menu/index.ts b/src/api/system/menu/index.ts index 5a806682..b912b67a 100644 --- a/src/api/system/menu/index.ts +++ b/src/api/system/menu/index.ts @@ -3,6 +3,7 @@ import request from '@/config/axios' export interface MenuVO { id: number name: string + enName?: string permission: string type: number sort: number diff --git a/src/locales/en.ts b/src/locales/en.ts index 1abfadc4..f727d94e 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -818,6 +818,7 @@ export default { }, Menu: { name: 'Menu Name', + enName: 'Menu English Name', parent: 'Parent Menu', type: 'Menu Type', icon: 'Menu Icon', @@ -834,6 +835,7 @@ export default { searchNamePlaceholder: 'Please input menu name', searchStatusPlaceholder: 'Please select menu status', namePlaceholder: 'Please input menu name', + enNamePlaceholder: 'Please input menu English name', pathPlaceholder: 'Please input route path', componentPlaceholder: 'For example: system/user/index', componentNamePlaceholder: 'For example: SystemUser', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 617886ec..8ab66d7a 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -822,6 +822,7 @@ export default { }, Menu: { name: '菜单名称', + enName: '菜单英文名称', parent: '上级菜单', type: '菜单类型', icon: '菜单图标', @@ -838,6 +839,7 @@ export default { searchNamePlaceholder: '请输入菜单名称', searchStatusPlaceholder: '请选择菜单状态', namePlaceholder: '请输入菜单名称', + enNamePlaceholder: '请输入菜单英文名称', pathPlaceholder: '请输入路由地址', componentPlaceholder: '例如说:system/user/index', componentNamePlaceholder: '例如说:SystemUser', diff --git a/src/utils/routerHelper.ts b/src/utils/routerHelper.ts index b65f93a0..05860e96 100644 --- a/src/utils/routerHelper.ts +++ b/src/utils/routerHelper.ts @@ -64,10 +64,13 @@ export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormal export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => { const res: AppRouteRecordRaw[] = [] const modulesRoutesKeys = Object.keys(modules) + const { locale } = useI18n() for (const route of routes) { + const useEnTitle = String(locale.value).toLowerCase().startsWith('en') + const title = useEnTitle && (route as any).enName ? (route as any).enName : route.name // 1. 生成 meta 菜单元数据 const meta = { - title: route.name, + title, icon: route.icon, hidden: !route.visible, noCache: !route.keepAlive, diff --git a/src/views/report/dashboardList/index.vue b/src/views/report/dashboardList/index.vue index 35b35764..07dbe56e 100644 --- a/src/views/report/dashboardList/index.vue +++ b/src/views/report/dashboardList/index.vue @@ -129,6 +129,7 @@ :title="dialogMode === 'create' ? '新增数据大屏' : '编辑数据大屏'" width="600px" draggable + @closed="handleCreateDialogClosed" > @@ -157,24 +158,66 @@ /> - - - +
+
+ + + + + + + + + +
+ + 添加设备 + +
([]) -const selectedDeviceIds = ref([]) +const deviceAttrSelections = ref<{ deviceId?: number | string; attributeIds: number[] }[]>([ + { deviceId: undefined, attributeIds: [] } +]) +const deviceAttributeOptionsMap = ref>({}) const getList = async () => { loading.value = true @@ -324,8 +371,27 @@ const handlePreview = (item: DashboardItem) => { } const path = route.startsWith('/') ? route : `/${route}` const queryParams = new URLSearchParams() + if (item.id) queryParams.append('goviewId', String(item.id)) if (item.orgId) queryParams.append('orgId', String(item.orgId)) - if (item.type === '1' && item.deviceIds) queryParams.append('deviceIds', item.deviceIds) + if (item.type === '1') { + let deviceIdsStr = '' + if (Array.isArray(item.deviceIdsList) && item.deviceIdsList.length) { + deviceIdsStr = item.deviceIdsList + .map((g) => g.deviceId) + .filter((id) => id !== undefined && id !== null) + .join(',') + } else if (Array.isArray(item.deviceIds)) { + deviceIdsStr = item.deviceIds + .map((g: any) => g?.deviceId) + .filter((id: any) => id !== undefined && id !== null) + .join(',') + } else if (typeof item.deviceIds === 'string') { + deviceIdsStr = item.deviceIds + } + if (deviceIdsStr && deviceIdsStr.trim()) { + queryParams.append('deviceIds', deviceIdsStr.trim()) + } + } const queryString = queryParams.toString() const url = router.resolve(path + (queryString ? `?${queryString}` : '')).href window.open(url, '_blank') @@ -338,11 +404,16 @@ const resetCreateForm = () => { createForm.type = '' createForm.orgId = undefined createForm.orgName = '' - createForm.deviceIds = '' + createForm.deviceIdsList = [] createForm.indexImage = '' createForm.route = '' createForm.content = '' - selectedDeviceIds.value = [] + deviceAttrSelections.value = [{ deviceId: undefined, attributeIds: [] }] +} + +const handleCreateDialogClosed = () => { + resetCreateForm() + createFormRef.value?.resetFields?.() } const openCreateDialog = () => { @@ -352,6 +423,38 @@ const openCreateDialog = () => { createDialogVisible.value = true } +const normalizeDeviceIdsList = (val: any): { deviceId: number; attributesIds: number[] }[] => { + if (!val) return [] + if (Array.isArray(val)) { + return val + .map((v: any) => ({ + deviceId: Number(v?.deviceId), + attributesIds: Array.isArray(v?.attributesIds) + ? v.attributesIds.map((id: any) => Number(id)).filter((id: number) => !Number.isNaN(id)) + : [] + })) + .filter((v) => v.deviceId && !Number.isNaN(v.deviceId)) + } + if (typeof val === 'string') { + const trimmed = val.trim() + if (!trimmed) return [] + if (trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed) + if (Array.isArray(parsed)) { + return normalizeDeviceIdsList(parsed) + } + } catch {} + } + const ids = trimmed + .split(',') + .map((s) => Number(s.trim())) + .filter((id) => !Number.isNaN(id)) + return ids.map((id) => ({ deviceId: id, attributesIds: [] })) + } + return [] +} + const openEditDialog = (item: DashboardItem) => { dialogMode.value = 'edit' editingId.value = item.id @@ -361,13 +464,26 @@ const openEditDialog = (item: DashboardItem) => { createForm.type = item.type || '' createForm.orgId = item.orgId || undefined createForm.orgName = item.orgName || '' - createForm.deviceIds = item.deviceIds || '' + const fromNew = Array.isArray(item.deviceIdsList) && item.deviceIdsList.length + ? normalizeDeviceIdsList(item.deviceIdsList) + : [] + const fromLegacy = !fromNew.length ? normalizeDeviceIdsList(item.deviceIds) : [] + createForm.deviceIdsList = fromNew.length ? fromNew : fromLegacy createForm.indexImage = item.indexImage || '' createForm.route = item.route || '' createForm.content = item.content || '' - selectedDeviceIds.value = createForm.deviceIds - ? createForm.deviceIds.split(',').filter((v) => v) - : [] + deviceAttrSelections.value = + createForm.deviceIdsList && createForm.deviceIdsList.length + ? createForm.deviceIdsList.slice(0, 8).map((g) => ({ + deviceId: g.deviceId, + attributeIds: Array.isArray(g.attributesIds) ? g.attributesIds.slice() : [] + })) + : [{ deviceId: undefined, attributeIds: [] }] + deviceAttrSelections.value.forEach((g) => { + if (g.deviceId) { + loadDeviceAttributes(g.deviceId) + } + }) createDialogVisible.value = true } @@ -384,17 +500,22 @@ const submitDialog = async () => { return } if (createForm.type === '1') { - if (!selectedDeviceIds.value.length) { - message.error('设备不能为空') - return - } - if (selectedDeviceIds.value.length !== 8) { - message.error('设备必须选择8个') + const groups = deviceAttrSelections.value + .map((g) => ({ + deviceId: g.deviceId, + attributesIds: (g.attributeIds || []).filter((v) => v !== undefined && v !== null) + })) + .filter((g) => g.deviceId && g.attributesIds.length) + if (!groups.length) { + message.error('请至少配置一组设备和点位') return } - createForm.deviceIds = selectedDeviceIds.value.join(',') + createForm.deviceIdsList = groups.map((g) => ({ + deviceId: Number(g.deviceId), + attributesIds: g.attributesIds.map((id: any) => Number(id)) + })) } else { - createForm.deviceIds = '' + createForm.deviceIdsList = [] } if (createForm.orgId) { const org = findOrgNode(organizationTree.value || [], createForm.orgId) @@ -475,6 +596,39 @@ const loadDeviceList = async () => { deviceList.value = await DeviceApi.getDeviceList() } +const loadDeviceAttributes = async (deviceId: number | string) => { + const key = String(deviceId) + if (!key) return + if (deviceAttributeOptionsMap.value[key]) return + const data = await request.get({ + url: '/iot/device/device-attribute/page', + params: { + pageNo: 1, + pageSize: 100, + deviceId + } + }) + deviceAttributeOptionsMap.value[key] = data?.list || [] +} + +const handleDeviceChange = async (value: number | string, index: number) => { + const group = deviceAttrSelections.value[index] + if (!group) return + group.attributeIds = [] + if (!value) return + await loadDeviceAttributes(value) +} + +const addDeviceAttrGroup = () => { + if (deviceAttrSelections.value.length >= 8) return + deviceAttrSelections.value.push({ deviceId: undefined, attributeIds: [] }) +} + +const removeDeviceAttrGroup = (index: number) => { + if (deviceAttrSelections.value.length <= 1) return + deviceAttrSelections.value.splice(index, 1) +} + onMounted(() => { getList() loadOrganizationTree() @@ -554,4 +708,21 @@ onMounted(() => { .dashboard-card-menu { min-width: 120px; } + +.dashboard-device-group-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.dashboard-device-group { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.dashboard-device-remove-btn { + flex-shrink: 0; +} diff --git a/src/views/report/dashboardPage/dashboard1/index.vue b/src/views/report/dashboardPage/dashboard1/index.vue index 56099e1c..c01749fa 100644 --- a/src/views/report/dashboardPage/dashboard1/index.vue +++ b/src/views/report/dashboardPage/dashboard1/index.vue @@ -98,7 +98,7 @@ import EnergyMonitor from './components/EnergyMonitor.vue' import ProductionTrend from './components/ProductionTrend.vue' const route = useRoute() -const deviceIds = route.query.deviceIds as string +const goviewId = route.query.goviewId as string const orgId = route.query.orgId interface DeviceAttribute { @@ -128,13 +128,11 @@ const getBottomLeft = (index: number) => { } const loadDeviceAttributes = async () => { - if (!deviceIds) return - try { - const res = await DeviceApi.getDeviceAttributeBatchList({ deviceIds, orgId }) - const list = (res && Array.isArray(res) ? res : []) as any[] - - // 假设返回的 list 顺序对应传入的 deviceIds 顺序,或者根据 deviceId 匹配 - // 这里简单按顺序分配前4个给 top,后4个给 bottom + if (!goviewId) return + try { + const res = await DeviceApi.getDeviceAttributeBatchList({ goviewId, orgId }) + const list = (res && Array.isArray(res) ? res : []) as any[] + const cards: DeviceCardData[] = list.map((d: any) => { return { deviceName: d.deviceName || 'Device', diff --git a/src/views/system/menu/MenuForm.vue b/src/views/system/menu/MenuForm.vue index d61bd3dd..db1a42be 100644 --- a/src/views/system/menu/MenuForm.vue +++ b/src/views/system/menu/MenuForm.vue @@ -21,6 +21,9 @@ + + + { formData.value = { id: undefined, name: '', + enName: '', permission: '', type: SystemMenuTypeEnum.DIR, sort: Number(undefined),