From 011111655f7d491405e5af9c89088f7ac37bf98b Mon Sep 17 00:00:00 2001 From: fengpu <18691763612@163.com> Date: Mon, 6 Jul 2026 14:07:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=90=E7=BB=B4=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E8=AF=A6=E6=83=85/=E6=89=AB=E7=A0=81/=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E6=B5=81=E7=A8=8B=E6=89=93=E7=A3=A8=20+=20=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E7=BB=9F=E8=AE=A1=E9=A1=B5=E4=B8=8E=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 运维小程序:workbench/workOrder/scan/inspect/me 各 Tab 详情与上报流程交互完善,新增 OpsBottomActions 公共底部操作组件与 datetime 工具 - 设备中心:asset/statistics 统计页对接 TableCard 升级,规范 listPage.less / global.less 与 list-page-spec 文档 - TableCard 组件能力扩展,配合统计页与运维页面复用 Co-Authored-By: Claude Opus 4.7 --- src/assets/styles/listPage.less | 45 +++- src/axios/handlers/miniprogram/ops/scan.ts | 4 +- .../handlers/miniprogram/ops/workOrder.ts | 5 +- src/components/OpsBottomActions.vue | 38 +++ src/components/TableCard.vue | 116 +++++++-- src/docs/list-page-spec.md | 40 ++++ src/layouts/MiniprogramLayout.vue | 9 + src/layouts/components/AppTabBar.vue | 41 +++- src/meta/nav.ts | 7 +- .../asset/statistics/init/usePage.ts | 18 +- .../asset/statistics/init/useTable.ts | 70 ++++-- .../asset/statistics/statistics.vue | 44 ++-- .../asset/statistics/types/index.ts | 2 + src/pages/miniprogram/ops/inspect/Home.vue | 81 ++++--- src/pages/miniprogram/ops/inspect/detail.vue | 151 +++++++----- src/pages/miniprogram/ops/me/Home.vue | 7 +- .../miniprogram/ops/me/messageCenter.vue | 55 +++-- src/pages/miniprogram/ops/scan/implRecord.vue | 4 +- src/pages/miniprogram/ops/scan/init.vue | 4 +- src/pages/miniprogram/ops/scan/monitor.vue | 221 +++++++++-------- src/pages/miniprogram/ops/scan/ota.vue | 30 ++- src/pages/miniprogram/ops/scan/repair.vue | 4 +- src/pages/miniprogram/ops/scan/sceneMenu.vue | 102 +++++--- src/pages/miniprogram/ops/workOrder/Home.vue | 120 +++++----- .../miniprogram/ops/workOrder/constants.ts | 53 +++++ .../miniprogram/ops/workOrder/detail.vue | 222 +++++++++++------- .../ops/workOrder/repairReport.vue | 4 +- src/pages/miniprogram/ops/workbench/Home.vue | 209 +++++++++++------ .../miniprogram/ops/workbench/alarmDetail.vue | 146 ++++++------ .../miniprogram/ops/workbench/alarmList.vue | 147 ++++++------ .../miniprogram/ops/workbench/constants.ts | 71 ++++++ src/styles/global.less | 39 +++ src/utils/datetime.ts | 61 +++++ 33 files changed, 1467 insertions(+), 703 deletions(-) create mode 100644 src/components/OpsBottomActions.vue create mode 100644 src/pages/miniprogram/ops/workOrder/constants.ts create mode 100644 src/pages/miniprogram/ops/workbench/constants.ts create mode 100644 src/utils/datetime.ts diff --git a/src/assets/styles/listPage.less b/src/assets/styles/listPage.less index 49a893c..330665f 100644 --- a/src/assets/styles/listPage.less +++ b/src/assets/styles/listPage.less @@ -33,13 +33,56 @@ .listPageTable { flex: 1; min-height: 0; + overflow: hidden; > .ant-col { height: 100%; } - .ant-table-wrapper { + // 兜底:直接用 a-table(不经 TableCard)的页面同样走完整高度链 + .ant-table-wrapper, + .table { + width: 100%; height: 100%; + + :deep(.ant-spin-nested-loading) { + height: 100%; + } + + :deep(.ant-spin-container) { + height: 100%; + display: flex; + flex-direction: column; + } + + :deep(.ant-table-container) { + height: 100%; + display: flex; + flex-direction: column; + } + + :deep(.ant-table-body) { + position: relative; + flex: 1; + min-height: 0; + + table { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + } + } + + :deep(.ant-table) { + flex: 1; + min-height: 0; + } + + :deep(.ant-pagination) { + margin: 16px 0 0; + } } } diff --git a/src/axios/handlers/miniprogram/ops/scan.ts b/src/axios/handlers/miniprogram/ops/scan.ts index 58bcdf5..786b861 100644 --- a/src/axios/handlers/miniprogram/ops/scan.ts +++ b/src/axios/handlers/miniprogram/ops/scan.ts @@ -176,8 +176,8 @@ registerMocks([ url: '/mp/ops/scan/repair', method: 'POST', handler: (ctx): ApiResponse => { - const _form = getParams(ctx.params) - void _form + // mock 不持久化维修记录,仅校验表单字段符合 RepairForm 契约 + getParams(ctx.params) return ok('维修上报成功', null) }, }, diff --git a/src/axios/handlers/miniprogram/ops/workOrder.ts b/src/axios/handlers/miniprogram/ops/workOrder.ts index 04ae544..89d56c9 100644 --- a/src/axios/handlers/miniprogram/ops/workOrder.ts +++ b/src/axios/handlers/miniprogram/ops/workOrder.ts @@ -188,9 +188,8 @@ registerMocks([ url: '/mp/ops/work-order/repair-report', method: 'POST', handler: (ctx): ApiResponse => { - const _form = getParams(ctx.params) - // 原型阶段仅记录日志,不实际更新工单状态 - void _form + // 原型阶段不实际更新工单状态,仅校验表单字段符合 RepairReportForm 契约 + getParams(ctx.params) return ok('维修上报成功', null) }, }, diff --git a/src/components/OpsBottomActions.vue b/src/components/OpsBottomActions.vue new file mode 100644 index 0000000..0f9109f --- /dev/null +++ b/src/components/OpsBottomActions.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/components/TableCard.vue b/src/components/TableCard.vue index 956ed9b..5d0e19d 100644 --- a/src/components/TableCard.vue +++ b/src/components/TableCard.vue @@ -10,24 +10,27 @@ - - - - - + +
+ + + + +
@@ -74,7 +77,7 @@ const props = withDefaults(defineProps<{ dataSource?: Record[] /** 用法 B:分页配置(false 关闭) */ pagination?: TablePaginationConfig | false - /** 横向 / 纵向滚动 */ + /** 横向 / 纵向滚动(默认 y:100% 让表体在容器内自滚,底部横向滚动条固定在视口下方) */ scroll?: TableProps['scroll'] /** 行 key */ rowKey?: string @@ -87,7 +90,7 @@ const props = withDefaults(defineProps<{ columns: undefined, dataSource: undefined, pagination: undefined, - scroll: () => ({ x: 'max-content' }), + scroll: () => ({ y: '100%', x: 'max-content' }), rowKey: 'id', size: 'middle', loading: false, @@ -120,6 +123,9 @@ const resolvedPagination = computed(() => { return props.pagination ?? false }) +/** 解析 scroll:外部传入则尊重,否则用默认 { y: '100%', x: 'max-content' } */ +const resolvedScroll = computed(() => props.scroll) + /** @change 事件透传(接 createDataSourceChange 返回值) */ const onChange = ( pagination: TablePaginationConfig, @@ -134,7 +140,19 @@ defineOptions({ inheritAttrs: false }) diff --git a/src/docs/list-page-spec.md b/src/docs/list-page-spec.md index 56b8790..65d41e5 100644 --- a/src/docs/list-page-spec.md +++ b/src/docs/list-page-spec.md @@ -537,6 +537,46 @@ const { - ❌ 省略 `button-style="solid"`(默认 outline 风格不符合规范) - ❌ 把 radio-group 塞进 FilterBar 的 inline 表单内(视觉错位,应单独成行) +### 7.5 高度链约束(强制 / 全局规范) + +> 列表页底部横向滚动条必须固定在视口下方,无论数据多少都不上下漂浮。 + +**根因机制**:参考 platform-vue-tenant `apps/main/src/assets/styles/listPage.less`,让 a-table 的 `ant-table-body` 在容器内自己滚动(flex:1 + position:relative + table absolute 撑满),滚动条就会贴在 ant-table-body 底部 = 分页栏上方 = 视口下方。 + +**完整高度链(TableCard 已内置,业务侧只需保证外层容器)**: + +``` +.xxx-page (业务外层) height:100% + flex column +└─ TableCard / .table-card height:100% + flex column + └─ ant-card-body height:100% + flex column + └─ .table-card__table flex:1 + overflow:hidden + └─ .table (a-table) height:100% + └─ ant-spin-nested-loading → ant-spin-container → ant-table-container 全部 height:100% + flex column + └─ ant-table-body flex:1 + position:relative + └─ table position:absolute 撑满 +``` + +**业务侧模板强制**: + +```vue + +``` + +**a-table 的 scroll 默认值**:TableCard 已默认 `{ y: '100%', x: 'max-content' }`,业务侧无需手动指定;除非有特殊诉求(如关闭纵向滚动),否则不要覆盖。 + +**禁止行为**: +- ❌ 业务外层容器省略 `height: 100%` 或改用 `min-height`(高度链断裂,TableCard 取不到父高度) +- ❌ 业务外层容器缺少 `display: flex; flex-direction: column`(多个子元素如 StatCard / FilterBar / TableCard 会堆叠而非 flex 分配) +- ❌ 在 TableCard 外层再包一层不必要的容器(同样会断裂高度链) +- ❌ 给 a-table 单独传 `:scroll="{ x: 'max-content' }"` 覆盖默认 y 值(会回到旧行为) + ## 八、数据展示规范 | 数据类型 | 展示格式 | diff --git a/src/layouts/MiniprogramLayout.vue b/src/layouts/MiniprogramLayout.vue index 540ae38..7e77037 100644 --- a/src/layouts/MiniprogramLayout.vue +++ b/src/layouts/MiniprogramLayout.vue @@ -175,6 +175,15 @@ const onSwitchMp = ({ key }: { key: string }): void => { flex: 1; overflow-y: auto; background: var(--background); + // 移动端原型禁止出现右侧滚动条(与真实小程序一致) + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + } } .phone-shell__safe-area { diff --git a/src/layouts/components/AppTabBar.vue b/src/layouts/components/AppTabBar.vue index 31504f4..343c113 100644 --- a/src/layouts/components/AppTabBar.vue +++ b/src/layouts/components/AppTabBar.vue @@ -6,7 +6,13 @@ inactive-color="#999" @change="onChange" > - + {{ tab.label }} @@ -34,4 +40,37 @@ const onChange = (key: string | number) => { position: relative; flex-shrink: 0; } + +// 中间扫码 Tab:圆形凸起按钮风格(label 为空时触发) +:deep(.van-tabbar-item--icon-only) { + position: relative; + + .van-tabbar-item__icon { + // 让 icon 容器脱离默认行内布局,便于做大圆形 + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0; + } + + .van-icon { + width: 38px; + height: 38px; + line-height: 38px; + text-align: center; + font-size: 20px; + color: #fff; + background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%); + border-radius: 50%; + box-shadow: 0 4px 10px rgba(24, 144, 255, 0.4); + // 向上凸起,露出 tab 栏 + transform: translateY(-4px); + transition: transform 0.15s ease, box-shadow 0.15s ease; + } + + &:active .van-icon { + transform: translateY(-2px) scale(0.96); + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.5); + } +} diff --git a/src/meta/nav.ts b/src/meta/nav.ts index 83d8d6c..f7843c7 100644 --- a/src/meta/nav.ts +++ b/src/meta/nav.ts @@ -940,7 +940,7 @@ export const miniprograms: NavMiniprogram[] = [ key: 'ops', label: '实施运维小程序', icon: '🔧', - description: '现场作业移动终端:扫码、工单、巡检、维修上报(仅运维工程师)', + description: '现场作业移动终端:扫码、工单、巡检、维修上报', tabs: [ { key: 'workbench', @@ -988,8 +988,9 @@ export const miniprograms: NavMiniprogram[] = [ }, { key: 'scan', - label: '扫码', - icon: 'scan-o', + // 扫码 Tab 只展示图标,不显示文字(中间凸起按钮风格) + label: '', + icon: 'scan', page: { key: 'mp-ops-scan-home', label: '扫码', path: 'scan', component: () => import('@pages/miniprogram/ops/scan/Home.vue'), diff --git a/src/pages/admin-portal/device-center/asset/statistics/init/usePage.ts b/src/pages/admin-portal/device-center/asset/statistics/init/usePage.ts index acdd4e6..86b34ce 100644 --- a/src/pages/admin-portal/device-center/asset/statistics/init/usePage.ts +++ b/src/pages/admin-portal/device-center/asset/statistics/init/usePage.ts @@ -1,4 +1,4 @@ -import { computed, onMounted, reactive, ref } from 'vue' +import { onMounted, reactive, ref } from 'vue' import type { TablePaginationConfig } from 'ant-design-vue' import { useAntdStaticMethods } from '@utils/antDesign/popUp' import { createPaginationConfig } from '@utils/antDesign/table' @@ -12,13 +12,13 @@ import { YES_NO_OPTIONS, createSearchKey, } from './useSearch' -import { buildSummaryRow, tableColumns } from './useTable' +import { buildSummaryRecord, tableColumns } from './useTable' /** * 设备统计页主编排: * - 顶部 4 个核心指标卡(总数 / 投用 / 在库 / 异常) * - 多维搜索条件(关键字 / 类别 / 投用 / 运行 / IOT / 分发 / 上报 / 租户 / 场所 / 软件版本 / 时间范围) - * - 列表展示扁平设备记录 + 顶部统计行(合计) + * - 列表展示扁平设备记录 + 首行统计行(合计) * - 异步导出 + 查看导出任务 */ export const usePage = () => { @@ -42,9 +42,6 @@ export const usePage = () => { pagination: createPaginationConfig() as TablePaginationConfig, }) - /** 统计行(合计) */ - const summaryRow = computed(() => buildSummaryRow(table.dataSource)) - /** 构造请求参数(时间范围拍平) */ const buildParams = (): SearchForm => ({ ...search, @@ -54,14 +51,16 @@ export const usePage = () => { deployEnd: deployRange.value?.[1], }) - /** 拉取列表 */ + /** 拉取列表(首行为合计统计行,其后为数据行) */ const listRequest = (): void => { pageLoading.value = true list(buildParams()) .then((res) => { if (res.code === '00000') { - table.dataSource = res.data ?? [] - table.pagination.total = table.dataSource.length + const rows = res.data ?? [] + table.dataSource = [buildSummaryRecord(rows), ...rows] + // 合计行不计入 total,分页以原始数据量为基准 + table.pagination.total = rows.length } }) .catch((err: unknown) => { @@ -139,7 +138,6 @@ export const usePage = () => { deployRange, table, overview, - summaryRow, categoryOptions: CATEGORY_OPTIONS, deployStatusOptions: DEPLOY_STATUS_OPTIONS, runStatusOptions: RUN_STATUS_OPTIONS, diff --git a/src/pages/admin-portal/device-center/asset/statistics/init/useTable.ts b/src/pages/admin-portal/device-center/asset/statistics/init/useTable.ts index d1088db..2d26aed 100644 --- a/src/pages/admin-portal/device-center/asset/statistics/init/useTable.ts +++ b/src/pages/admin-portal/device-center/asset/statistics/init/useTable.ts @@ -3,9 +3,24 @@ import { h } from 'vue' import { Tag } from 'ant-design-vue' import type { DeviceStatItem } from '../types' +/** + * 统计行 customRender 入参类型 + * - summary 行的字段值会被替换为合计文本/数字,故 value/record 都放宽 + */ +type RenderCtx = { value: unknown; record: DeviceStatItem; index: number } + /** 列定义(按用户给定顺序) */ export const tableColumns: TableColumnsType = [ - { title: '序号', dataIndex: 'index', align: 'center', fixed: 'left', width: 70, customRender: ({ index }: { index: number }) => index + 1 }, + { + title: '序号', + dataIndex: 'index', + align: 'center', + fixed: 'left', + width: 70, + // summary 行显示"合计",数据行序号 = index(因 summary 占 index 0,数据行从 1 开始) + customRender: ({ index, record }: RenderCtx) => + record.__isSummary ? '合计' : index, + }, { title: '设备类别', dataIndex: 'categoryLabel', align: 'left', width: 140, resizable: true, ellipsis: true }, { title: '设备名称', dataIndex: 'name', align: 'left', width: 160, resizable: true, ellipsis: true }, { title: '设备编码', dataIndex: 'deviceCode', align: 'left', width: 180, resizable: true, ellipsis: true }, @@ -14,32 +29,34 @@ export const tableColumns: TableColumnsType = [ dataIndex: 'isIot', align: 'center', width: 100, - customRender: ({ value }: { value: boolean }) => - h(Tag, { color: value ? 'green' : 'default' }, () => (value ? '是' : '否')), + customRender: ({ value, record }: RenderCtx) => + record.__isSummary ? (value as number) : h(Tag, { color: value ? 'green' : 'default' }, () => (value ? '是' : '否')), }, { title: '投用状态', dataIndex: 'deployStatus', align: 'center', width: 100, - customRender: ({ value }: { value: DeviceStatItem['deployStatus'] }) => - h(Tag, { color: value === 'deployed' ? 'success' : 'default' }, () => (value === 'deployed' ? '投用' : '在库')), + customRender: ({ value, record }: RenderCtx) => + record.__isSummary + ? (value as string) + : h(Tag, { color: value === 'deployed' ? 'success' : 'default' }, () => (value === 'deployed' ? '投用' : '在库')), }, { title: '启用数据分发', dataIndex: 'dataDispatch', align: 'center', width: 120, - customRender: ({ value }: { value: boolean }) => - h(Tag, { color: value ? 'green' : 'default' }, () => (value ? '已启用' : '未启用')), + customRender: ({ value, record }: RenderCtx) => + record.__isSummary ? (value as number) : h(Tag, { color: value ? 'green' : 'default' }, () => (value ? '已启用' : '未启用')), }, { title: '启用数据上报', dataIndex: 'dataReport', align: 'center', width: 120, - customRender: ({ value }: { value: boolean }) => - h(Tag, { color: value ? 'green' : 'default' }, () => (value ? '已启用' : '未启用')), + customRender: ({ value, record }: RenderCtx) => + record.__isSummary ? (value as number) : h(Tag, { color: value ? 'green' : 'default' }, () => (value ? '已启用' : '未启用')), }, { title: '所属租户', dataIndex: 'tenantName', align: 'left', width: 140, resizable: true, ellipsis: true, customRender: ({ value }: { value: string }) => value ?? '—' }, { title: '布点场所', dataIndex: 'siteName', align: 'left', width: 140, resizable: true, ellipsis: true, customRender: ({ value }: { value: string }) => value ?? '—' }, @@ -49,7 +66,8 @@ export const tableColumns: TableColumnsType = [ dataIndex: 'runStatus', align: 'center', width: 100, - customRender: ({ value }: { value: DeviceStatItem['runStatus'] }) => { + customRender: ({ value, record }: RenderCtx) => { + if (record.__isSummary) return value as string const colorMap: Record = { online: 'success', offline: 'default', @@ -62,7 +80,7 @@ export const tableColumns: TableColumnsType = [ repairing: '维修中', scrapped: '报废', } - return h(Tag, { color: colorMap[value] ?? 'default' }, () => textMap[value] ?? '—') + return h(Tag, { color: colorMap[value as string] ?? 'default' }, () => textMap[value as string] ?? '—') }, }, { title: '入库时间', dataIndex: 'onlineTime', align: 'center', width: 170, sorter: true, resizable: true }, @@ -70,7 +88,10 @@ export const tableColumns: TableColumnsType = [ { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 100 }, ] -/** 计算统计行数据(列表上方统计行) */ +/** + * 计算统计行字段值(合计) + * 字段 key 与列 dataIndex 对齐,summary 行的 cell 会直接消费这些值 + */ export const buildSummaryRow = (rows: DeviceStatItem[]): Record => { const total = rows.length const deployed = rows.filter((r) => r.deployStatus === 'deployed').length @@ -81,12 +102,27 @@ export const buildSummaryRow = (rows: DeviceStatItem[]): Record r.runStatus === 'online').length const offline = rows.filter((r) => r.runStatus === 'offline').length return { - index: '合计', - name: `${total} 台`, - iot, - deployStatus: `投用 ${deployed} / 在库 ${inStock}`, + isIot: iot, + name: `${total}`, + deployStatus: `${deployed} / ${inStock}`, dataDispatch: dispatch, dataReport: report, - runStatus: `在用 ${online} / 离线 ${offline}`, + runStatus: `${online} / ${offline}`, } } + +/** + * 把合计字段包装成可插入 dataSource 的统计行 record + * - 带 __isSummary 标记,列 customRender 据此走简化渲染(直接返回文本/数字,不走 Tag) + * - 字段值类型与 DeviceStatItem 不完全一致(如 isIot 是 number),用类型断言绕过 + */ +export const buildSummaryRecord = (rows: DeviceStatItem[]): DeviceStatItem => + ({ + id: '__summary__', + __isSummary: true, + categoryLabel: '', + category: 'catering' as DeviceStatItem['category'], + name: '', + deviceCode: '', + ...buildSummaryRow(rows), + }) as unknown as DeviceStatItem diff --git a/src/pages/admin-portal/device-center/asset/statistics/statistics.vue b/src/pages/admin-portal/device-center/asset/statistics/statistics.vue index 2398abe..1672dfc 100644 --- a/src/pages/admin-portal/device-center/asset/statistics/statistics.vue +++ b/src/pages/admin-portal/device-center/asset/statistics/statistics.vue @@ -102,8 +102,14 @@ - - + + - - @@ -161,7 +147,6 @@ const { deployRange, table, overview, - summaryRow, categoryOptions, deployStatusOptions, runStatusOptions, @@ -195,4 +180,11 @@ const viewDetail = (record: DeviceStatItem): void => { margin: 0 !important; flex-shrink: 0; } + +// 合计行视觉强化:粗体 + 浅蓝底,与数据行区分 +:deep(.ant-table-row.__isSummary td) { + background: #fafafa !important; + font-weight: 600; + color: #1f1f1f; +} diff --git a/src/pages/admin-portal/device-center/asset/statistics/types/index.ts b/src/pages/admin-portal/device-center/asset/statistics/types/index.ts index befae4d..29da3cd 100644 --- a/src/pages/admin-portal/device-center/asset/statistics/types/index.ts +++ b/src/pages/admin-portal/device-center/asset/statistics/types/index.ts @@ -41,6 +41,8 @@ export interface DeviceStatItem { onlineTime: string /** 投用时间 */ deployTime: string + /** 统计行标记(true 表示该行是合计行,customRender 据此走简化渲染) */ + __isSummary?: boolean } /** 顶部统计聚合数据 */ diff --git a/src/pages/miniprogram/ops/inspect/Home.vue b/src/pages/miniprogram/ops/inspect/Home.vue index 3decde1..260c755 100644 --- a/src/pages/miniprogram/ops/inspect/Home.vue +++ b/src/pages/miniprogram/ops/inspect/Home.vue @@ -6,36 +6,44 @@ - -
-
- {{ statusText(plan.status) }} - {{ plan.deviceCount }} 台设备 + + + +
+
+ {{ statusText(plan.status) }} + {{ plan.deviceCount }} 台设备 +
+
{{ plan.name }}
+
+ + {{ plan.site }} +
+
+ + {{ plan.deviceCategory }} +
+
-
{{ plan.name }}
-
- - {{ plan.site }} -
-
- - {{ plan.deviceCategory }} -
- -
- + +
@@ -54,7 +62,9 @@ const router = useRouter() const list = ref([]) const loading = ref(false) +// finished 固定为 true:mock 数据一次性返回,无分页;保留 van-list 是为后续接入真接口预留 const finished = ref(true) +const refreshing = ref(false) const activeTab = ref('') const statusTabs: Array<{ label: string; value: InspectPlanStatus | '' }> = [ @@ -85,6 +95,7 @@ const loadList = (): void => { .finally(() => { loading.value = false finished.value = true + refreshing.value = false }) } @@ -95,6 +106,13 @@ const onTabChange = (): void => { loadList() } +const onRefresh = (): void => { + list.value = [] + loading.value = true + finished.value = false + loadList() +} + onMounted(() => { loadList() }) @@ -105,6 +123,11 @@ onMounted(() => { min-height: 100%; background: var(--background); padding-bottom: 16px; + + &__empty { + padding: 32px 0; + background: transparent; + } } .ip-card { diff --git a/src/pages/miniprogram/ops/inspect/detail.vue b/src/pages/miniprogram/ops/inspect/detail.vue index cad4be8..02d3540 100644 --- a/src/pages/miniprogram/ops/inspect/detail.vue +++ b/src/pages/miniprogram/ops/inspect/detail.vue @@ -1,63 +1,72 @@ @@ -71,6 +80,7 @@ import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { showSuccessToast, showFailToast } from 'vant' +import OpsBottomActions from '@components/OpsBottomActions.vue' import { finishInspectTask, getInspectDetail, submitInspectItem } from './api' import type { InspectItem, InspectItemType, InspectPlanStatus, InspectTaskDetail } from './types' @@ -177,6 +187,22 @@ onMounted(() => { background: var(--background); padding-bottom: 80px; + &__skeleton { + padding: 16px; + background: #fff; + + > .van-skeleton { + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); + + &:last-child { + border-bottom: none; + margin-bottom: 0; + } + } + } + &__header { display: flex; align-items: center; @@ -185,8 +211,17 @@ onMounted(() => { background: #fff; } - &__count { + &__name { flex: 1; + font-size: var(--font-base); + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__count { font-size: var(--font-xs); color: var(--text-secondary); } @@ -226,16 +261,6 @@ onMounted(() => { color: var(--primary); margin-right: 8px; } - - &__actions { - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 12px 16px env(safe-area-inset-bottom); - background: #fff; - border-top: 1px solid var(--border); - } } .inspect-item { diff --git a/src/pages/miniprogram/ops/me/Home.vue b/src/pages/miniprogram/ops/me/Home.vue index c9d2fbf..f021c86 100644 --- a/src/pages/miniprogram/ops/me/Home.vue +++ b/src/pages/miniprogram/ops/me/Home.vue @@ -88,7 +88,7 @@ */ import { computed, onMounted, reactive, ref } from 'vue' import { useRouter } from 'vue-router' -import { showSuccessToast, showFailToast } from 'vant' +import { showSuccessToast, showToast } from 'vant' import { clearOffline, getOfflineStatus, @@ -115,11 +115,12 @@ const goMessages = (): void => { } const onSetting = (): void => { - showFailToast('原型阶段未实现') + // 原型阶段未实现,用普通 toast 提示,避免误导为失败 + showToast('原型阶段未实现') } const onLogout = (): void => { - showFailToast('原型阶段未实现') + showToast('原型阶段未实现') } const onSync = (): void => { diff --git a/src/pages/miniprogram/ops/me/messageCenter.vue b/src/pages/miniprogram/ops/me/messageCenter.vue index 7c77c00..92c5956 100644 --- a/src/pages/miniprogram/ops/me/messageCenter.vue +++ b/src/pages/miniprogram/ops/me/messageCenter.vue @@ -10,23 +10,31 @@ - -
-
- {{ typeText(msg.type) }} - {{ msg.createdAt }} - + + + +
+
+ {{ typeText(msg.type) }} + {{ msg.createdAt }} + +
+
{{ msg.title }}
+
{{ msg.content }}
-
{{ msg.title }}
-
{{ msg.content }}
-
- + +
@@ -47,7 +55,9 @@ const router = useRouter() const list = ref([]) const loading = ref(false) +// finished 固定为 true:mock 数据一次性返回,无分页;保留 van-list 是为后续接入真接口预留 const finished = ref(true) +const refreshing = ref(false) const activeType = ref('') const typeTabs: Array<{ label: string; value: MessageType | '' }> = [ @@ -88,6 +98,13 @@ const onTabChange = (): void => { loadList() } +const onRefresh = (): void => { + list.value = [] + loading.value = true + finished.value = false + loadList() +} + const onRead = (msg: MessageItem): void => { if (!msg.read) { readMessage(msg.id) @@ -123,6 +140,7 @@ const loadList = (): void => { .finally(() => { loading.value = false finished.value = true + refreshing.value = false }) } @@ -141,6 +159,11 @@ onMounted(() => { font-size: var(--font-sm); color: var(--primary); } + + &__empty { + padding: 32px 0; + background: transparent; + } } .msg-card { diff --git a/src/pages/miniprogram/ops/scan/implRecord.vue b/src/pages/miniprogram/ops/scan/implRecord.vue index 784ef49..8cdbb20 100644 --- a/src/pages/miniprogram/ops/scan/implRecord.vue +++ b/src/pages/miniprogram/ops/scan/implRecord.vue @@ -90,6 +90,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { showSuccessToast } from 'vant' import { scanDevice, submitImplRecord } from './api' +import { formatNow } from '@utils/datetime' import type { ImplRecordForm, ImplType } from './types' const route = useRoute() @@ -163,8 +164,7 @@ const loadDevice = (): void => { } onMounted(() => { - const now = new Date() - form.operateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:00` + form.operateTime = formatNow() loadDevice() }) diff --git a/src/pages/miniprogram/ops/scan/init.vue b/src/pages/miniprogram/ops/scan/init.vue index ccb3644..fce97f1 100644 --- a/src/pages/miniprogram/ops/scan/init.vue +++ b/src/pages/miniprogram/ops/scan/init.vue @@ -102,6 +102,7 @@ import { onMounted, reactive, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { showSuccessToast } from 'vant' import { scanDevice, submitInit } from './api' +import { formatNow } from '@utils/datetime' import type { InitForm } from './types' const route = useRoute() @@ -175,8 +176,7 @@ const loadDevice = (): void => { } onMounted(() => { - const now = new Date() - form.operateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:00` + form.operateTime = formatNow() loadDevice() }) diff --git a/src/pages/miniprogram/ops/scan/monitor.vue b/src/pages/miniprogram/ops/scan/monitor.vue index e8e7c27..e4b0a94 100644 --- a/src/pages/miniprogram/ops/scan/monitor.vue +++ b/src/pages/miniprogram/ops/scan/monitor.vue @@ -1,101 +1,124 @@ @@ -180,6 +203,22 @@ onMounted(() => { background: var(--background); padding-bottom: 16px; + &__skeleton { + padding: 16px; + background: #fff; + + > .van-skeleton { + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); + + &:last-child { + border-bottom: none; + margin-bottom: 0; + } + } + } + &__header { background: #fff; padding: 12px 16px; @@ -205,10 +244,8 @@ onMounted(() => { } &__empty { - padding: 32px 16px; - text-align: center; - color: var(--text-placeholder); - font-size: var(--font-sm); + padding: 32px 0; + background: transparent; } &__repair-method { diff --git a/src/pages/miniprogram/ops/scan/ota.vue b/src/pages/miniprogram/ops/scan/ota.vue index 2c82bcb..45e6053 100644 --- a/src/pages/miniprogram/ops/scan/ota.vue +++ b/src/pages/miniprogram/ops/scan/ota.vue @@ -14,7 +14,17 @@
升级中…
- +
+ +
{{ task.currentVersion }} → {{ task.targetVersion }}
@@ -186,6 +196,8 @@ const loadTask = (): void => { } onMounted(() => { + // 设备信息需先于版本/任务加载(loadVersions / loadTask 依赖 deviceCode 已通过路由注入的 ref, + // 因此三者实际可并行;串行写法仅保留可读性,但为提升首屏速度,这里改为并行) loadDevice() loadVersions() loadTask() @@ -245,7 +257,21 @@ onMounted(() => { &__progress-title { font-size: var(--font-base); color: var(--text-primary); - margin-bottom: 12px; + margin-bottom: 16px; + } + + &__circle { + display: flex; + justify-content: center; + margin: 8px 0 12px; + + :deep(.van-circle__text) { + font-size: 18px; + font-weight: 600; + color: var(--primary); + font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; + font-variant-numeric: tabular-nums; + } } &__progress-info { diff --git a/src/pages/miniprogram/ops/scan/repair.vue b/src/pages/miniprogram/ops/scan/repair.vue index 426407f..b41bda2 100644 --- a/src/pages/miniprogram/ops/scan/repair.vue +++ b/src/pages/miniprogram/ops/scan/repair.vue @@ -101,6 +101,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { showSuccessToast } from 'vant' import { scanDevice, submitRepair } from './api' +import { formatNow } from '@utils/datetime' import type { RepairForm } from './types' const route = useRoute() @@ -185,8 +186,7 @@ const loadDevice = (): void => { } onMounted(() => { - const now = new Date() - form.repairTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:00` + form.repairTime = formatNow() loadDevice() }) diff --git a/src/pages/miniprogram/ops/scan/sceneMenu.vue b/src/pages/miniprogram/ops/scan/sceneMenu.vue index 2692958..18f492a 100644 --- a/src/pages/miniprogram/ops/scan/sceneMenu.vue +++ b/src/pages/miniprogram/ops/scan/sceneMenu.vue @@ -1,47 +1,55 @@ @@ -147,6 +155,22 @@ onMounted(() => { min-height: 100%; background: var(--background); + &__skeleton { + padding: 16px; + background: #fff; + + > .van-skeleton { + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); + + &:last-child { + border-bottom: none; + margin-bottom: 0; + } + } + } + &__device { margin: 8px 12px; background: #fff; diff --git a/src/pages/miniprogram/ops/workOrder/Home.vue b/src/pages/miniprogram/ops/workOrder/Home.vue index f6df82a..ef6b56c 100644 --- a/src/pages/miniprogram/ops/workOrder/Home.vue +++ b/src/pages/miniprogram/ops/workOrder/Home.vue @@ -4,50 +4,58 @@ - +
- +
- - -
-
- {{ typeText(item.type) }} - {{ item.code }} - 超 SLA + + + + +
+
+ {{ workOrderTypeText(item.type) }} + {{ item.code }} + 超 SLA +
+
{{ item.title }}
+
+ + {{ item.deviceCode }} + {{ item.deviceName }} +
+
+ + {{ item.site }} +
+
-
{{ item.title }}
-
- - {{ item.deviceCode }} - {{ item.deviceName }} -
-
- - {{ item.site }} -
- -
- + +
@@ -68,13 +76,21 @@ import { onMounted, reactive, ref } from 'vue' import { useRouter } from 'vue-router' import { listWorkOrders } from './api' +import { + WORK_ORDER_STATUS_TABS, + WORK_ORDER_TYPE_OPTS, + workOrderTypeTagType, + workOrderTypeText, +} from './constants' import type { WorkOrderItem, WorkOrderListParams, WorkOrderStatus, WorkOrderType } from './types' const router = useRouter() const list = ref([]) const loading = ref(false) +// finished 固定为 true:mock 数据一次性返回,无分页;保留 van-list 是为后续接入真接口预留 const finished = ref(true) +const refreshing = ref(false) const activeTab = ref('pending') const filter = reactive<{ type: WorkOrderType | ''; keyword: string }>({ @@ -82,32 +98,6 @@ const filter = reactive<{ type: WorkOrderType | ''; keyword: string }>({ keyword: '', }) -const statusTabs: Array<{ label: string; value: WorkOrderStatus }> = [ - { label: '待处理', value: 'pending' }, - { label: '处理中', value: 'processing' }, - { label: '已完成', value: 'done' }, -] - -const typeOpts = [ - { text: '全部类型', value: '' }, - { text: '报修', value: 'repair' }, - { text: '巡检异常', value: 'inspection' }, - { text: '自检异常', value: 'selfCheck' }, - { text: 'SLA 超时', value: 'sla' }, - { text: '售后转单', value: 'afterSale' }, -] - -const typeText = (t: WorkOrderType): string => ({ - repair: '报修', - inspection: '巡检', - selfCheck: '自检', - sla: 'SLA', - afterSale: '售后', -})[t] - -const typeTagType = (t: WorkOrderType): 'primary' | 'warning' | 'danger' | 'default' => - t === 'repair' ? 'danger' : t === 'inspection' ? 'primary' : t === 'selfCheck' ? 'warning' : 'default' - const goDetail = (id: string): void => { router.push(`/miniprogram/ops/work-order-detail/${id}`) } @@ -130,6 +120,7 @@ const loadList = (): void => { .finally(() => { loading.value = false finished.value = true + refreshing.value = false }) } @@ -144,6 +135,10 @@ const onTabChange = (): void => { reload() } +const onRefresh = (): void => { + reload() +} + onMounted(() => { loadList() }) @@ -174,6 +169,11 @@ onMounted(() => { font-size: var(--font-sm); box-shadow: var(--shadow-md); } + + &__empty { + padding: 32px 0; + background: transparent; + } } .wo-card { diff --git a/src/pages/miniprogram/ops/workOrder/constants.ts b/src/pages/miniprogram/ops/workOrder/constants.ts new file mode 100644 index 0000000..662fdc9 --- /dev/null +++ b/src/pages/miniprogram/ops/workOrder/constants.ts @@ -0,0 +1,53 @@ +/** + * 实施运维小程序 / 工单 字典常量 + * + * 设计目的:把工单列表页和详情页里分散的类型 / 状态映射集中管理, + * 避免 .vue 文件之间复制粘贴相同的 { repair: '报修' } 字典。 + * + * 用法: + * import { WORK_ORDER_TYPE_OPTS, workOrderTypeText } from './constants' + * {{ workOrderTypeText(item.type) }} + */ +import type { WorkOrderStatus, WorkOrderType } from './types' + +/** 工单状态下拉项(用于分段 Tab;不含"全部"——分段 Tab 直接切换状态) */ +export const WORK_ORDER_STATUS_TABS: Array<{ label: string; value: WorkOrderStatus }> = [ + { label: '待处理', value: 'pending' }, + { label: '处理中', value: 'processing' }, + { label: '已完成', value: 'done' }, +] + +/** 工单类型下拉项(含"全部"占位) */ +export const WORK_ORDER_TYPE_OPTS: Array<{ text: string; value: WorkOrderType | '' }> = [ + { text: '全部类型', value: '' }, + { text: '报修', value: 'repair' }, + { text: '巡检异常', value: 'inspection' }, + { text: '自检异常', value: 'selfCheck' }, + { text: 'SLA 超时', value: 'sla' }, + { text: '售后转单', value: 'afterSale' }, +] + +/** 工单类型 → 中文文本 */ +export const workOrderTypeText = (t: WorkOrderType): string => + ({ + repair: '报修', + inspection: '巡检', + selfCheck: '自检', + sla: 'SLA', + afterSale: '售后', + })[t] + +/** 工单类型 → Vant Tag 类型 */ +export const workOrderTypeTagType = (t: WorkOrderType): 'primary' | 'warning' | 'danger' | 'default' => + t === 'repair' ? 'danger' : t === 'inspection' ? 'primary' : t === 'selfCheck' ? 'warning' : 'default' + +/** 故障类型下拉项(维修上报用,与 scan/repair.vue 保持一致) */ +export const FAULT_TYPE_OPTS: Array<{ text: string; value: string }> = [ + { text: '硬件故障', value: '硬件故障' }, + { text: '软件故障', value: '软件故障' }, + { text: '网络故障', value: '网络故障' }, + { text: '传感器故障', value: '传感器故障' }, + { text: '电源故障', value: '电源故障' }, + { text: '机械故障', value: '机械故障' }, + { text: '其他', value: '其他' }, +] diff --git a/src/pages/miniprogram/ops/workOrder/detail.vue b/src/pages/miniprogram/ops/workOrder/detail.vue index 8be8b9e..f0714db 100644 --- a/src/pages/miniprogram/ops/workOrder/detail.vue +++ b/src/pages/miniprogram/ops/workOrder/detail.vue @@ -1,74 +1,92 @@ @@ -83,26 +101,17 @@ */ import { onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { showSuccessToast, showFailToast } from 'vant' +import { showSuccessToast, showToast } from 'vant' +import OpsBottomActions from '@components/OpsBottomActions.vue' import { acceptWorkOrder, closeWorkOrder, getWorkOrderDetail } from './api' -import type { WorkOrderDetail, WorkOrderType } from './types' +import { workOrderTypeTagType, workOrderTypeText } from './constants' +import type { WorkOrderDetail } from './types' const route = useRoute() const router = useRouter() const detail = ref(null) -const typeText = (t: WorkOrderType): string => ({ - repair: '报修', - inspection: '巡检', - selfCheck: '自检', - sla: 'SLA', - afterSale: '售后', -})[t] - -const typeTagType = (t: WorkOrderType): 'primary' | 'warning' | 'danger' | 'default' => - t === 'repair' ? 'danger' : t === 'inspection' ? 'primary' : t === 'selfCheck' ? 'warning' : 'default' - const onBack = (): void => { router.back() } @@ -134,8 +143,8 @@ const onRepair = (): void => { const onClose = (): void => { if (!detail.value) return - // 关单前需先完成维修上报,这里提示 - showFailToast('请先完成维修上报') + // 关单前需先完成维修上报,这里用普通 toast 提示(非失败语义) + showToast('请先完成维修上报') } const loadDetail = (): void => { @@ -159,6 +168,22 @@ onMounted(() => { background: var(--background); padding-bottom: 80px; + &__skeleton { + padding: 16px; + background: #fff; + + > .van-skeleton { + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); + + &:last-child { + border-bottom: none; + margin-bottom: 0; + } + } + } + &__header { background: #fff; padding: 12px 16px; @@ -244,12 +269,41 @@ onMounted(() => { &__step-time { font-size: var(--font-xs); color: var(--text-placeholder); + font-variant-numeric: tabular-nums; + font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; } &__step-action { + display: flex; + align-items: center; + gap: 6px; font-size: var(--font-sm); color: var(--text-primary); - margin-top: 2px; + margin-top: 4px; + font-weight: 500; + } + + &__step-action-text { + flex: 1; + } + + &__step-tag { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + line-height: 1.5; + + &--current { + background: #fff7e6; + color: var(--warning); + border: 1px solid #ffd591; + } + + &--start { + background: var(--primary-light); + color: var(--primary); + border: 1px solid #91d5ff; + } } &__step-operator { @@ -257,17 +311,5 @@ onMounted(() => { color: var(--text-secondary); margin-top: 2px; } - - &__actions { - position: fixed; - bottom: 0; - left: 0; - right: 0; - display: flex; - gap: 8px; - padding: 12px 16px env(safe-area-inset-bottom); - background: #fff; - border-top: 1px solid var(--border); - } } diff --git a/src/pages/miniprogram/ops/workOrder/repairReport.vue b/src/pages/miniprogram/ops/workOrder/repairReport.vue index 379fb94..454c17d 100644 --- a/src/pages/miniprogram/ops/workOrder/repairReport.vue +++ b/src/pages/miniprogram/ops/workOrder/repairReport.vue @@ -105,6 +105,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { showSuccessToast } from 'vant' import { getWorkOrderDetail, submitRepairReport } from './api' +import { formatNow } from '@utils/datetime' import type { RepairReportForm } from './types' const route = useRoute() @@ -186,8 +187,7 @@ const loadWorkOrder = (): void => { } onMounted(() => { - const now = new Date() - form.repairTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:00` + form.repairTime = formatNow() loadWorkOrder() }) diff --git a/src/pages/miniprogram/ops/workbench/Home.vue b/src/pages/miniprogram/ops/workbench/Home.vue index 5e6f80e..1369036 100644 --- a/src/pages/miniprogram/ops/workbench/Home.vue +++ b/src/pages/miniprogram/ops/workbench/Home.vue @@ -10,69 +10,80 @@
-
+
今日待办
-
-
- -
-
待处理告警
-
严重 {{ todo.criticalAlarms }} 条
+ +
+ +
-
+
本周统计
-
-
-
{{ week.weekOrders }}
-
处理工单
-
平均 {{ week.avgProcessHours }}h
-
-
-
{{ week.weekInspectionRate }}
-
巡检完成率
-
异常 {{ week.weekInspectionAbnormalRate }}
-
-
-
{{ week.monthInstallCount }}
-
本月装机
-
+ +
+
@@ -104,6 +115,7 @@ import { onMounted, ref } from 'vue' import { useRouter } from 'vue-router' import { getTodoStats, getWeekStats } from './api' +import { formatDate, weekDayText } from '@utils/datetime' import type { QuickEntry, TodoStats, WeekStats } from './types' const router = useRouter() @@ -113,8 +125,7 @@ const week = ref(null) const todayText = (() => { const d = new Date() - const week = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()] - return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} 周${week}` + return `${formatDate(d)} 周${weekDayText(d).replace('周', '')}` })() const quickEntries: QuickEntry[] = [ @@ -173,16 +184,36 @@ onMounted(() => { diff --git a/src/pages/miniprogram/ops/workbench/alarmList.vue b/src/pages/miniprogram/ops/workbench/alarmList.vue index 9f4ab13..18046b8 100644 --- a/src/pages/miniprogram/ops/workbench/alarmList.vue +++ b/src/pages/miniprogram/ops/workbench/alarmList.vue @@ -19,9 +19,9 @@
- - - + + +
- - -
+ + -
- {{ levelText(item.level) }} - {{ sourceText(item.source) }} - {{ item.triggerTime }} + +
+
+ {{ alarmLevelText(item.level) }} + {{ alarmSourceText(item.source) }} + {{ item.triggerTime }} +
+
{{ item.content }}
+
+ + {{ item.deviceCode }} + {{ item.deviceName }} +
+
-
{{ item.content }}
-
- - {{ item.deviceCode }} - {{ item.deviceName }} -
- -
-
+ +
@@ -75,13 +83,25 @@ import { computed, onMounted, reactive, ref } from 'vue' import { useRouter } from 'vue-router' import { listAlarms } from './api' -import type { AlarmItem, AlarmLevel, AlarmSource, AlarmStatus, AlarmListParams } from './types' +import { + ALARM_LEVEL_OPTS, + ALARM_SOURCE_OPTS, + ALARM_STATUS_OPTS, + alarmLevelTagType, + alarmLevelText, + alarmSourceText, + alarmStatusTagType, + alarmStatusText, +} from './constants' +import type { AlarmItem, AlarmListParams } from './types' const router = useRouter() const list = ref([]) const loading = ref(false) +// finished 固定为 true:mock 数据一次性返回,无分页;保留 van-list 是为后续接入真接口预留 const finished = ref(true) +const refreshing = ref(false) const filter = reactive({ level: '', @@ -90,31 +110,6 @@ const filter = reactive({ keyword: '', }) -const levelOpts = [ - { text: '全部级别', value: '' }, - { text: '严重', value: 'critical' }, - { text: '警告', value: 'warn' }, - { text: '提示', value: 'info' }, -] - -const sourceOpts = [ - { text: '全部来源', value: '' }, - { text: '运行日志', value: 'runLog' }, - { text: '自检异常', value: 'selfCheck' }, - { text: '巡检异常', value: 'inspection' }, - { text: 'SLA 超时', value: 'sla' }, - { text: 'OTA 失败', value: 'ota' }, - { text: '数据分发失败', value: 'dataDistribute' }, -] - -const statusOpts = [ - { text: '全部状态', value: '' }, - { text: '未处理', value: 'pending' }, - { text: '处理中', value: 'processing' }, - { text: '已忽略', value: 'ignored' }, - { text: '已转工单', value: 'workorder' }, -] - const stats = computed(() => { const all = list.value return { @@ -124,28 +119,6 @@ const stats = computed(() => { } }) -const levelText = (lv: AlarmLevel): string => ({ critical: '严重', warn: '警告', info: '提示' })[lv] -const sourceText = (s: AlarmSource): string => ({ - runLog: '运行日志', - selfCheck: '自检', - inspection: '巡检', - sla: 'SLA', - ota: 'OTA', - dataDistribute: '分发', -})[s] -const statusText = (s: AlarmStatus): string => ({ - pending: '未处理', - processing: '处理中', - ignored: '已忽略', - workorder: '已转工单', -})[s] - -const levelTagType = (lv: AlarmLevel): 'danger' | 'warning' | 'primary' => - lv === 'critical' ? 'danger' : lv === 'warn' ? 'warning' : 'primary' - -const statusTagType = (s: AlarmStatus): 'danger' | 'warning' | 'default' | 'primary' => - s === 'pending' ? 'danger' : s === 'processing' ? 'primary' : s === 'workorder' ? 'warning' : 'default' - const goDetail = (id: string): void => { router.push(`/miniprogram/ops/alarm-detail/${id}`) } @@ -159,6 +132,7 @@ const loadList = (): void => { .finally(() => { loading.value = false finished.value = true + refreshing.value = false }) } @@ -169,6 +143,10 @@ const reload = (): void => { loadList() } +const onRefresh = (): void => { + reload() +} + onMounted(() => { loadList() }) @@ -192,6 +170,11 @@ onMounted(() => { background: #fff; border-bottom: 1px solid var(--border); } + + &__empty { + padding: 32px 0; + background: transparent; + } } .stat-mini { @@ -205,9 +188,11 @@ onMounted(() => { } &__value { - font-size: 22px; + font-size: 24px; font-weight: 600; color: var(--primary); + font-variant-numeric: tabular-nums; + font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; } &--critical &__value { diff --git a/src/pages/miniprogram/ops/workbench/constants.ts b/src/pages/miniprogram/ops/workbench/constants.ts new file mode 100644 index 0000000..15536e1 --- /dev/null +++ b/src/pages/miniprogram/ops/workbench/constants.ts @@ -0,0 +1,71 @@ +/** + * 实施运维小程序 / 工作台 + 告警 字典常量 + * + * 设计目的:把告警列表页和详情页里分散的级别 / 来源 / 状态映射集中管理, + * 避免 .vue 文件之间复制粘贴相同的 { critical: '严重' } 字典。 + * + * 用法: + * import { ALARM_LEVEL_OPTS, alarmLevelText } from './constants' + * {{ alarmLevelText(item.level) }} + */ +import type { AlarmLevel, AlarmSource, AlarmStatus } from './types' + +/** 告警级别下拉项(含"全部"占位) */ +export const ALARM_LEVEL_OPTS: Array<{ text: string; value: AlarmLevel | '' }> = [ + { text: '全部级别', value: '' }, + { text: '严重', value: 'critical' }, + { text: '警告', value: 'warn' }, + { text: '提示', value: 'info' }, +] + +/** 告警来源下拉项 */ +export const ALARM_SOURCE_OPTS: Array<{ text: string; value: AlarmSource | '' }> = [ + { text: '全部来源', value: '' }, + { text: '运行日志', value: 'runLog' }, + { text: '自检异常', value: 'selfCheck' }, + { text: '巡检异常', value: 'inspection' }, + { text: 'SLA 超时', value: 'sla' }, + { text: 'OTA 失败', value: 'ota' }, + { text: '数据分发失败', value: 'dataDistribute' }, +] + +/** 告警状态下拉项 */ +export const ALARM_STATUS_OPTS: Array<{ text: string; value: AlarmStatus | '' }> = [ + { text: '全部状态', value: '' }, + { text: '未处理', value: 'pending' }, + { text: '处理中', value: 'processing' }, + { text: '已忽略', value: 'ignored' }, + { text: '已转工单', value: 'workorder' }, +] + +/** 告警级别 → 中文文本 */ +export const alarmLevelText = (lv: AlarmLevel): string => + ({ critical: '严重', warn: '警告', info: '提示' })[lv] + +/** 告警级别 → Vant Tag 类型 */ +export const alarmLevelTagType = (lv: AlarmLevel): 'danger' | 'warning' | 'primary' => + lv === 'critical' ? 'danger' : lv === 'warn' ? 'warning' : 'primary' + +/** 告警来源 → 中文文本 */ +export const alarmSourceText = (s: AlarmSource): string => + ({ + runLog: '运行日志', + selfCheck: '自检', + inspection: '巡检', + sla: 'SLA', + ota: 'OTA', + dataDistribute: '分发', + })[s] + +/** 告警状态 → 中文文本 */ +export const alarmStatusText = (s: AlarmStatus): string => + ({ + pending: '未处理', + processing: '处理中', + ignored: '已忽略', + workorder: '已转工单', + })[s] + +/** 告警状态 → Vant Tag 类型 */ +export const alarmStatusTagType = (s: AlarmStatus): 'danger' | 'warning' | 'default' | 'primary' => + s === 'pending' ? 'danger' : s === 'processing' ? 'primary' : s === 'workorder' ? 'warning' : 'default' diff --git a/src/styles/global.less b/src/styles/global.less index 7cf9611..e886c95 100644 --- a/src/styles/global.less +++ b/src/styles/global.less @@ -61,3 +61,42 @@ body { padding-left: 4px; padding-right: 4px; } + +/** + * 全局滚动条样式微调(Webkit / Chromium / 新版 Edge) + * - 默认滚动条太粗(12-16px)+ 颜色太暗,统一调整为 8px 细条 + 浅灰半透明 + * - hover 时颜色略深,保留可见性 + * - Firefox 用 scrollbar-width: thin + scrollbar-color 兜底 + */ +.ant-table-body, +.ant-table-content, +.ant-table-header, +.ant-tabs-nav-wrap, +.ant-layout-sider, +.scroll-container { + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.18) transparent; + + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.18); + border-radius: 4px; + transition: background 0.2s; + } + + &::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.32); + } + + &::-webkit-scrollbar-corner { + background: transparent; + } +} diff --git a/src/utils/datetime.ts b/src/utils/datetime.ts new file mode 100644 index 0000000..31f4365 --- /dev/null +++ b/src/utils/datetime.ts @@ -0,0 +1,61 @@ +/** + * 日期时间格式化工具 + * + * 设计目的:统一运维小程序各表单页面的"操作时间"字段格式,避免在每个 .vue + * 中重复写 `new Date()` + padStart 拼接逻辑。 + * + * 与 platform-vue-tenant 的 dayjs 调用对齐: + * - dayjs().format('YYYY-MM-DD HH:mm:ss') ≈ formatNow() + * - dayjs(date).format('YYYY-MM-DD HH:mm:ss') ≈ formatDateTime(date) + * + * 用法: + * import { formatNow, formatDateTime, formatDate } from '@utils/datetime' + * form.operateTime = formatNow() // 当前时间,秒位固定 00 + * form.repairTime = formatNow(0) // 同上 + * form.repairTime = formatNow() // 默认秒位 00(运维上报习惯) + * formatDateTime(new Date()) // 完整时间戳 + * formatDate(new Date()) // 仅日期 + */ + +const pad2 = (n: number): string => String(n).padStart(2, '0') + +/** + * 把任意 Date 转为 `YYYY-MM-DD HH:mm:ss` 字符串 + * @param date 日期对象,缺省取当前时间 + * @param fixedSeconds 是否固定秒位为 00(默认 true,匹配运维上报页面习惯) + */ +export const formatDateTime = (date: Date = new Date(), fixedSeconds = true): string => { + const y = date.getFullYear() + const m = pad2(date.getMonth() + 1) + const d = pad2(date.getDate()) + const h = pad2(date.getHours()) + const min = pad2(date.getMinutes()) + const s = fixedSeconds ? '00' : pad2(date.getSeconds()) + return `${y}-${m}-${d} ${h}:${min}:${s}` +} + +/** + * 当前时间的完整格式化字符串(秒位固定 00) + * 用于表单"操作时间 / 维修时间 / 实施时间"等自动回填字段 + */ +export const formatNow = (): string => formatDateTime(new Date(), true) + +/** + * 把任意 Date 转为 `YYYY-MM-DD` 字符串 + * @param date 日期对象,缺省取当前时间 + */ +export const formatDate = (date: Date = new Date()): string => { + const y = date.getFullYear() + const m = pad2(date.getMonth() + 1) + const d = pad2(date.getDate()) + return `${y}-${m}-${d}` +} + +/** + * 获取当前日期的中文星期描述("周一" / "周日") + * 用于工作台头部展示 + */ +export const weekDayText = (date: Date = new Date()): string => { + const names = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] + return names[date.getDay()] ?? '' +}