You need to sign in before continuing.
Commit 787af94f4513caef4bc11db91d9b709197e7ffd4
1 parent
a382d270
完善设备查看会话记录功能,新增客户名称解析,更新统计输出和查询输入结构,优化前端筛选界面以支持设备名称和查看内容的模糊筛选。
Showing
13 changed files
with
858 additions
and
216 deletions
docs/md/sbRecords接口测试报告.md
0 → 100644
| 1 | +# sbRecords 统计接口测试报告 | |
| 2 | + | |
| 3 | +## 测试时间 | |
| 4 | +2025-02-17 | |
| 5 | + | |
| 6 | +## 一、Statistics 接口 | |
| 7 | + | |
| 8 | +**接口**:`GET /api/Extend/SbRecords/Actions/Statistics` | |
| 9 | + | |
| 10 | +**参数**:与列表查询一致,支持 addTime、type、addUser、reId、customerName、deviceName、contentName | |
| 11 | + | |
| 12 | +**返回结构**: | |
| 13 | +```json | |
| 14 | +{ | |
| 15 | + "totalCount": 25, | |
| 16 | + "completeCount": 24, | |
| 17 | + "totalDurationSeconds": 212, | |
| 18 | + "avgDurationSeconds": 8.83, | |
| 19 | + "byType": [{ "name": "备件支持", "value": 6 }, ...], | |
| 20 | + "byEquipment": [{ "name": "设备名称", "value": 3 }, ...], | |
| 21 | + "byContent": [{ "name": "查看内容", "value": 1 }, ...], | |
| 22 | + "byCustomer": [...], | |
| 23 | + "byUser": [...] | |
| 24 | +} | |
| 25 | +``` | |
| 26 | + | |
| 27 | +**测试结果**:✅ 通过 | |
| 28 | +- 返回新增字段:completeCount、totalDurationSeconds、avgDurationSeconds、byContent | |
| 29 | +- byEquipment 已改为按 F_DeviceName 分组 | |
| 30 | +- 筛选参数生效 | |
| 31 | + | |
| 32 | +## 二、GetList 接口 | |
| 33 | + | |
| 34 | +**接口**:`GET /api/Extend/SbRecords` | |
| 35 | + | |
| 36 | +**新增筛选参数**:deviceName、contentName(模糊匹配) | |
| 37 | + | |
| 38 | +**测试结果**:✅ 通过 | |
| 39 | +- deviceName=动力、contentName=猫 等筛选正常 | |
| 40 | +- 返回 deviceName、contentName 字段 | |
| 41 | + | |
| 42 | +## 三、curl 示例 | |
| 43 | + | |
| 44 | +```bash | |
| 45 | +# 1. 获取 Token | |
| 46 | +TOKEN=$(curl -s -X POST "http://localhost:2011/api/oauth/Login" \ | |
| 47 | + -H "Content-Type: application/x-www-form-urlencoded" \ | |
| 48 | + -d "account=admin&password=66762a3ccde2a2cff3060d7a4a0a576b" \ | |
| 49 | + | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])") | |
| 50 | + | |
| 51 | +# 2. 调用 Statistics | |
| 52 | +curl -s -X GET "http://localhost:2011/api/Extend/SbRecords/Actions/Statistics" \ | |
| 53 | + -H "Authorization: $TOKEN" | |
| 54 | + | |
| 55 | +# 3. 带筛选参数 | |
| 56 | +curl -s -X GET "http://localhost:2011/api/Extend/SbRecords/Actions/Statistics?type=故障排查" \ | |
| 57 | + -H "Authorization: $TOKEN" | |
| 58 | + | |
| 59 | +# 4. 列表筛选 | |
| 60 | +curl -s -X GET "http://localhost:2011/api/Extend/SbRecords?deviceName=动力&contentName=猫" \ | |
| 61 | + -H "Authorization: $TOKEN" | |
| 62 | +``` | |
| 63 | + | |
| 64 | +## 四、结论 | |
| 65 | + | |
| 66 | +| 接口 | 状态 | | |
| 67 | +|------|------| | |
| 68 | +| Statistics | ✅ 通过 | | |
| 69 | +| GetList(deviceName/contentName 筛选) | ✅ 通过 | | |
| 70 | + | |
| 71 | +## 五、页面 UI 修复验证(2025-02-17) | |
| 72 | + | |
| 73 | +### 修复项 | |
| 74 | + | |
| 75 | +| 问题 | 修复方案 | 验证要点 | | |
| 76 | +|------|----------|----------| | |
| 77 | +| 表格重复「序号」列 | NCC-table 默认 `hasNO=true` 会渲染序号列,与自定义 `indexMethod` 列重复;设置 `:has-n-o="false"` 关闭内置序号 | 表头仅有一列「序号」,分页序号正确 | | |
| 78 | +| 空值显示不规范 | 项目规范:没有信息的字段显示「无」 | 设备名称、查看内容、查看客户、查看用户、记录类型 为空时显示「无」 | | |
| 79 | +| 操作列未左对齐 | 项目规范:操作按钮必须左对齐 | 操作列「删除」按钮左对齐 | | |
| 80 | +| 统计卡片内边距 | 项目规范:卡片内边距 12px | 统计卡片 padding 为 12px | | |
| 81 | + | |
| 82 | +### 验证清单 | |
| 83 | + | |
| 84 | +- [ ] 表格表头无重复「序号」列 | |
| 85 | +- [ ] 分页切换后序号连续(如第 2 页从 21 开始) | |
| 86 | +- [ ] 空值字段显示「无」而非空白或「-」 | |
| 87 | +- [ ] 操作列「删除」按钮左对齐 | |
| 88 | +- [ ] 统计卡片布局符合规范(100px 高、12px 内边距、12px 圆角) | |
| 89 | +- [ ] 图表展开后主内容区可正常滚动 | ... | ... |
docs/md/移动端设备查看埋点方案.md
0 → 100644
| 1 | +# 移动端设备查看埋点方案 | |
| 2 | + | |
| 3 | +## 一、需求与目标 | |
| 4 | + | |
| 5 | +- **需求**:记录用户**在哪个板块**看了**哪个设备**,以及**停留时长** | |
| 6 | +- **对接接口**:`StartViewRecord`(开始会话)、`EndViewRecord`(结束会话) | |
| 7 | +- **数据落库**:`sb_records` 表(F_ReId、F_Type、F_ContentName、F_DeviceName、F_AddTime、F_LeaveTime、F_DurationSeconds) | |
| 8 | + | |
| 9 | +--- | |
| 10 | + | |
| 11 | +## 二、首页 8 个板块 vs 需埋点 6 个 | |
| 12 | + | |
| 13 | +| 序号 | 板块名称 | 路由/页面 | 是否埋点 | 说明 | | |
| 14 | +|------|----------|-----------|----------|------| | |
| 15 | +| 1 | 故障排查 | `/pages/new/gzpc/gzpc` → `detail` | ✅ | 有设备/故障关联 | | |
| 16 | +| 2 | 知识库 | `/pages/new/zsk/zsk` → `detail` | ✅ | 有产品/设备关联 | | |
| 17 | +| 3 | 我的设备 | `/pages/myDevice/myDevice` → `detail` | ✅ | 直接是设备 | | |
| 18 | +| 4 | 资料管理 | `/pages/new/zlgl/zlgl` → `detail` | ✅ | 有设备/资料关联 | | |
| 19 | +| 5 | 备件支持 | `/pages/new/bjzc/bjzc` → `detail` | ✅ | 有产品/设备关联 | | |
| 20 | +| 6 | 培训展示 | `/pages/new/pxzs/pxzs` → `detail` | ✅ | 有产品关联 | | |
| 21 | +| 7 | 信息推送 | `/pages/new/xxts/xxts` | ❌ | 无设备维度 | | |
| 22 | +| 8 | 用户反馈 | `/pages/new/yhfk/yhfk` | ❌ | 无设备维度 | | |
| 23 | + | |
| 24 | +--- | |
| 25 | + | |
| 26 | +## 三、埋点粒度与 reId 约定 | |
| 27 | + | |
| 28 | +**原则**:只在**详情页**埋点(列表页无具体“设备/资料”对象)。 | |
| 29 | +**reId**:当前详情对应的**主实体 id**(用于 sb_records 的 F_ReId)。 | |
| 30 | +**type**:板块名称,用于区分来源。 | |
| 31 | + | |
| 32 | +| 板块 | 详情页路径 | reId 来源 | type 值 | deviceName(设备名称) | contentName(查看内容) | | |
| 33 | +|------|------------|-----------|---------|------------------------|-------------------------| | |
| 34 | +| 故障排查 | `gzpc/detail` | `info.id` | 故障排查 | `info.sbmc` | `info.code`(故障代码) | | |
| 35 | +| 知识库 | `zsk/detail` | `detail.id` | 知识库 | 后端根据 sssb 查 cpgl | `detail.sbm` | | |
| 36 | +| 我的设备 | `myDevice/detail` | `options.id` | 我的设备 | `detail.sbmc` | `detail.sbmc` | | |
| 37 | +| 资料管理 | `zlgl/detail` | `info.id` | 资料管理 | `info.zlm` | `info.zlm` | | |
| 38 | +| 备件支持 | `bjzc/detail` | `info.id` | 备件支持 | `info.glcpmc` | `info.mc` | | |
| 39 | +| 培训展示 | `pxzs/detail` | `info.id` | 培训展示 | `info.sbmc` | `info.pxmc` | | |
| 40 | + | |
| 41 | +> **说明**:sb_records 通过 **记录类型 + 设备名称 + 查看内容** 三字段定位。F_Type、F_DeviceName、F_ContentName 均可由前端传入,未传时后端根据 type+reId 从对应表自动解析。 | |
| 42 | + | |
| 43 | +--- | |
| 44 | + | |
| 45 | +## 四、埋点时机 | |
| 46 | + | |
| 47 | +| 时机 | 动作 | 说明 | | |
| 48 | +|------|------|------| | |
| 49 | +| 进入详情页 | 调用 `StartViewRecord` | `onLoad` 或 `onShow` 中,拿到 reId、type 后调用 | | |
| 50 | +| 离开详情页 | 调用 `EndViewRecord` | `onUnload` 或 `onHide` 中,传入之前保存的 recordId | | |
| 51 | + | |
| 52 | +**推荐**: | |
| 53 | +- **Start**:在详情数据加载完成后(如 `gzpcxq`、`fetchDetail` 等接口返回后)调用,确保有 reId | |
| 54 | +- **End**:在 `onUnload` 中调用(页面销毁时),`onHide` 可能因切后台未真正离开,可选补充 | |
| 55 | + | |
| 56 | +--- | |
| 57 | + | |
| 58 | +## 五、各详情页数据流与埋点接入点 | |
| 59 | + | |
| 60 | +### 5.1 故障排查详情 `gzpc/detail.vue` | |
| 61 | + | |
| 62 | +- **进入**:`onLoad` 从 `uni.getStorageSync("detail")` 取 `info`,调用 `gzpcxq({ id: info.id })` | |
| 63 | +- **reId**:`this.info.id`(故障 id)或 `this.info.sssbId`(若有) | |
| 64 | +- **type**:`"故障排查"` | |
| 65 | +- **埋点**:`gzpcxq` 成功后 `StartViewRecord`;`ht()` 返回 / `onUnload` 时 `EndViewRecord` | |
| 66 | + | |
| 67 | +### 5.2 知识库详情 `zsk/detail.vue` | |
| 68 | + | |
| 69 | +- **进入**:`onLoad(options)` 取 `options.id`,调用 `fetchDetail` → `zskxq({ id })` | |
| 70 | +- **reId**:`this.id` 或 `this.detail.id` | |
| 71 | +- **type**:`"知识库"` | |
| 72 | +- **埋点**:`fetchDetail` 成功后 `StartViewRecord`;`onUnload` 时 `EndViewRecord` | |
| 73 | + | |
| 74 | +### 5.3 我的设备详情 `myDevice/detail.vue` | |
| 75 | + | |
| 76 | +- **进入**:`onLoad(options)` 取 `options.id`,调用 `fetchDeviceDetail(deviceId)` | |
| 77 | +- **reId**:`options.id` 或 `this.detail.id` | |
| 78 | +- **type**:`"我的设备"` | |
| 79 | +- **埋点**:`fetchDeviceDetail` 成功后 `StartViewRecord`;`onUnload` 时 `EndViewRecord` | |
| 80 | + | |
| 81 | +### 5.4 资料管理详情 `zlgl/detail.vue` | |
| 82 | + | |
| 83 | +- **进入**:`onLoad(options)` 取 `options.id`,调用详情接口 | |
| 84 | +- **reId**:`options.id` 或 `this.info.id` | |
| 85 | +- **type**:`"资料管理"` | |
| 86 | +- **埋点**:详情加载成功后 `StartViewRecord`;`onUnload` 时 `EndViewRecord` | |
| 87 | + | |
| 88 | +### 5.5 备件支持详情 `bjzc/detail.vue` | |
| 89 | + | |
| 90 | +- **进入**:`onLoad` 从 `uni.getStorageSync("detail")` 取 info,调用 `bjzcxq` | |
| 91 | +- **reId**:`this.info.id`(备件 id) | |
| 92 | +- **type**:`"备件支持"` | |
| 93 | +- **埋点**:`bjzcxq` 成功后 `StartViewRecord`;`onUnload` 时 `EndViewRecord` | |
| 94 | + | |
| 95 | +### 5.6 培训展示详情 `pxzs/detail.vue` | |
| 96 | + | |
| 97 | +- **进入**:`onLoad` 从 `uni.getStorageSync("detail")` 取 info,调用 `pxzsxqs` | |
| 98 | +- **reId**:`info.id`(培训 id) | |
| 99 | +- **type**:`"培训展示"` | |
| 100 | +- **埋点**:`pxzsxqs` 成功后 `StartViewRecord`;`onUnload` 时 `EndViewRecord` | |
| 101 | + | |
| 102 | +--- | |
| 103 | + | |
| 104 | +## 六、前端实现要点 | |
| 105 | + | |
| 106 | +### 6.1 API 封装 | |
| 107 | + | |
| 108 | +在 `uniapp_jiju/apis/modules/oauth.js`(或新建 `sbRecords.js`)中增加: | |
| 109 | + | |
| 110 | +```javascript | |
| 111 | +// 开始设备查看会话(进入详情页时调用) | |
| 112 | +startViewRecord(data) { | |
| 113 | + return request.post('/api/Extend/SbRecords/Actions/StartViewRecord', data, { | |
| 114 | + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } | |
| 115 | + }) | |
| 116 | +}, | |
| 117 | +// 结束设备查看会话(离开详情页时调用) | |
| 118 | +endViewRecord(data) { | |
| 119 | + return request.post('/api/Extend/SbRecords/Actions/EndViewRecord', data, { | |
| 120 | + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } | |
| 121 | + }) | |
| 122 | +} | |
| 123 | +``` | |
| 124 | + | |
| 125 | +> 注意:项目规范 GET 用 data 传参;POST 表单可用 `application/x-www-form-urlencoded`,参数为 `reId`、`type`、`recordId`。 | |
| 126 | + | |
| 127 | +### 6.2 参数格式 | |
| 128 | + | |
| 129 | +- **StartViewRecord**:`reId`(必填)、`type`(必填)、`contentName`(可选,查看内容)、`deviceName`(可选,设备名称)、`item1`(可选)、`item2`(可选) | |
| 130 | +- **EndViewRecord**:`recordId`(必填,Start 返回的 id) | |
| 131 | + | |
| 132 | +> contentName、deviceName 未传时,后端根据 type+reId 从对应表自动解析。 | |
| 133 | + | |
| 134 | +### 6.3 通用逻辑(可抽成 mixin) | |
| 135 | + | |
| 136 | +```javascript | |
| 137 | +// 伪代码 | |
| 138 | +data() { | |
| 139 | + return { viewRecordId: null } | |
| 140 | +}, | |
| 141 | +methods: { | |
| 142 | + async startViewTracking(reId, type, contentName, deviceName) { | |
| 143 | + if (!reId || !type) return | |
| 144 | + try { | |
| 145 | + const data = { reId, type } | |
| 146 | + if (contentName) data.contentName = contentName | |
| 147 | + if (deviceName) data.deviceName = deviceName | |
| 148 | + const res = await this.API.startViewRecord(data) | |
| 149 | + async endViewTracking() { | |
| 150 | + if (!this.viewRecordId) return | |
| 151 | + try { | |
| 152 | + await this.API.endViewRecord({ recordId: this.viewRecordId }) | |
| 153 | + } catch (e) { console.warn('EndViewRecord failed', e) } | |
| 154 | + this.viewRecordId = null | |
| 155 | + } | |
| 156 | +}, | |
| 157 | +onUnload() { | |
| 158 | + this.endViewTracking() | |
| 159 | +} | |
| 160 | +``` | |
| 161 | + | |
| 162 | +各详情页在数据加载成功后调用 `this.startViewTracking(reId, type, contentName, deviceName)`,在 `onUnload` 中调用 `this.endViewTracking()`。 | |
| 163 | + | |
| 164 | +--- | |
| 165 | + | |
| 166 | +## 七、实现顺序建议 | |
| 167 | + | |
| 168 | +1. **API 封装**:在 `oauth.js` 或新建模块中增加 `startViewRecord`、`endViewRecord` | |
| 169 | +2. **Mixin 或工具函数**:抽 `startViewTracking`、`endViewTracking`,避免重复代码 | |
| 170 | +3. **按页面接入**:依次在 6 个详情页接入埋点(建议顺序:我的设备 → 故障排查 → 知识库 → 资料管理 → 备件支持 → 培训展示) | |
| 171 | +4. **联调与验证**:真机/模拟器操作,查 `sb_records` 表确认 F_Type、F_DeviceName、F_ContentName、F_DurationSeconds 正确 | |
| 172 | + | |
| 173 | +--- | |
| 174 | + | |
| 175 | +## 八、注意事项 | |
| 176 | + | |
| 177 | +- **未登录**:若接口需 Token,未登录用户可能调用失败,可静默失败或跳过埋点 | |
| 178 | +- **快速返回**:用户进入后立即返回,End 仍会执行,Duration 可能为 0 或很小,属正常 | |
| 179 | +- **页面栈**:`navigateBack` 会触发 `onUnload`,可正常执行 End;`reLaunch`、`redirectTo` 同理 | |
| 180 | +- **recordId 存储**:必须存在组件 data 中,`onUnload` 时能访问到,避免用全局变量导致多页面冲突 | ... | ... |
docs/sql/add_sb_records_content_name.sql
0 → 100644
| 1 | +-- 为 sb_records 表新增字段,实现「记录类型+设备名称+查看内容」三字段定位 | |
| 2 | +-- 执行时间:按需执行(若 F_ContentName 已存在,可跳过第一条) | |
| 3 | + | |
| 4 | +-- 1. 查看内容(故障代码、知识名、资料名、备件名、培训名等) | |
| 5 | +ALTER TABLE `sb_records` | |
| 6 | + ADD COLUMN `F_ContentName` varchar(200) NULL COMMENT '查看内容(故障代码、知识名、资料名等)' AFTER `F_Type`; | |
| 7 | + | |
| 8 | +-- 2. 设备名称(所属设备、关联产品/设备名) | |
| 9 | +ALTER TABLE `sb_records` | |
| 10 | + ADD COLUMN `F_DeviceName` varchar(200) NULL COMMENT '设备名称(所属设备、关联产品名)' AFTER `F_Type`; | ... | ... |
docs/sql/add_sb_records_customer_name.sql
0 → 100644
docs/sql/add_sb_records_device_name.sql
0 → 100644
uniapp_jiju/apis/modules/oauth.js
| ... | ... | @@ -236,4 +236,12 @@ export default { |
| 236 | 236 | hqlogo(data){ |
| 237 | 237 | return request.get('/api/Extend/Logogl?lx=' + '移动端') |
| 238 | 238 | }, |
| 239 | + // 开始设备查看会话(进入详情页时调用,用于记录停留时长) | |
| 240 | + startViewRecord(data) { | |
| 241 | + return request.postFormData('/api/Extend/SbRecords/Actions/StartViewRecord', data) | |
| 242 | + }, | |
| 243 | + // 结束设备查看会话(离开详情页时调用,用于计算停留时长) | |
| 244 | + endViewRecord(data) { | |
| 245 | + return request.postFormData('/api/Extend/SbRecords/Actions/EndViewRecord', data) | |
| 246 | + }, | |
| 239 | 247 | } | ... | ... |
uniapp_jiju/common/mixins/viewRecordMixin.js
0 → 100644
| 1 | +/** | |
| 2 | + * 设备查看记录埋点 mixin | |
| 3 | + * 用于详情页:进入时 StartViewRecord,离开时 EndViewRecord | |
| 4 | + * 使用:在详情页 mixins: [viewRecordMixin],数据加载成功后调用 this.startViewTracking(reId, type, contentName, deviceName) | |
| 5 | + * 记录类型、设备名称、查看内容 三字段便于定位 | |
| 6 | + */ | |
| 7 | +export default { | |
| 8 | + data() { | |
| 9 | + return { | |
| 10 | + viewRecordId: null | |
| 11 | + } | |
| 12 | + }, | |
| 13 | + methods: { | |
| 14 | + async startViewTracking(reId, type, contentName, deviceName) { | |
| 15 | + if (!reId || !type) return | |
| 16 | + try { | |
| 17 | + const data = { reId: String(reId), type } | |
| 18 | + if (contentName) data.contentName = String(contentName) | |
| 19 | + if (deviceName) data.deviceName = String(deviceName) | |
| 20 | + const res = await this.API.startViewRecord(data) | |
| 21 | + if (res && res.data) { | |
| 22 | + this.viewRecordId = res.data | |
| 23 | + } | |
| 24 | + } catch (e) { | |
| 25 | + console.warn('StartViewRecord failed', e) | |
| 26 | + } | |
| 27 | + }, | |
| 28 | + async endViewTracking() { | |
| 29 | + if (!this.viewRecordId) return | |
| 30 | + try { | |
| 31 | + await this.API.endViewRecord({ recordId: this.viewRecordId }) | |
| 32 | + } catch (e) { | |
| 33 | + console.warn('EndViewRecord failed', e) | |
| 34 | + } | |
| 35 | + this.viewRecordId = null | |
| 36 | + } | |
| 37 | + }, | |
| 38 | + onUnload() { | |
| 39 | + this.endViewTracking() | |
| 40 | + } | |
| 41 | +} | ... | ... |
机具(服务端)/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/SbRecords/SbRecordsStatisticsOutput.cs
| ... | ... | @@ -10,12 +10,24 @@ namespace NCC.Extend.Entitys.Dto.SbRecords |
| 10 | 10 | /// <summary>总记录数</summary> |
| 11 | 11 | public int TotalCount { get; set; } |
| 12 | 12 | |
| 13 | + /// <summary>完整记录数(有离开时间的记录)</summary> | |
| 14 | + public int CompleteCount { get; set; } | |
| 15 | + | |
| 16 | + /// <summary>总停留时长(秒)</summary> | |
| 17 | + public long TotalDurationSeconds { get; set; } | |
| 18 | + | |
| 19 | + /// <summary>平均停留时长(秒,仅统计完整记录)</summary> | |
| 20 | + public double AvgDurationSeconds { get; set; } | |
| 21 | + | |
| 13 | 22 | /// <summary>按记录类型统计 [{ name, value }]</summary> |
| 14 | 23 | public List<SbRecordsStatisticsItem> ByType { get; set; } |
| 15 | 24 | |
| 16 | - /// <summary>按关联设备统计(前 N 条)</summary> | |
| 25 | + /// <summary>按设备名称统计(前 N 条,使用 F_DeviceName)</summary> | |
| 17 | 26 | public List<SbRecordsStatisticsItem> ByEquipment { get; set; } |
| 18 | 27 | |
| 28 | + /// <summary>按查看内容统计(前 N 条,故障代码、知识名、培训名等)</summary> | |
| 29 | + public List<SbRecordsStatisticsItem> ByContent { get; set; } | |
| 30 | + | |
| 19 | 31 | /// <summary>按客户统计(前 N 条)</summary> |
| 20 | 32 | public List<SbRecordsStatisticsItem> ByCustomer { get; set; } |
| 21 | 33 | ... | ... |
机具(服务端)/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/SbRecordsListQueryInput.cs
| ... | ... | @@ -53,6 +53,15 @@ namespace NCC.Extend.Entitys.Dto.SbRecords |
| 53 | 53 | /// 记录类型 |
| 54 | 54 | /// </summary> |
| 55 | 55 | public string type { get; set; } |
| 56 | - | |
| 56 | + | |
| 57 | + /// <summary> | |
| 58 | + /// 设备名称(模糊筛选) | |
| 59 | + /// </summary> | |
| 60 | + public string deviceName { get; set; } | |
| 61 | + | |
| 62 | + /// <summary> | |
| 63 | + /// 查看内容(模糊筛选) | |
| 64 | + /// </summary> | |
| 65 | + public string contentName { get; set; } | |
| 57 | 66 | } |
| 58 | 67 | } | ... | ... |
机具(服务端)/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/SbRecordsEntity.cs
| ... | ... | @@ -82,6 +82,12 @@ namespace NCC.Extend.Entitys |
| 82 | 82 | /// </summary> |
| 83 | 83 | [SugarColumn(ColumnName = "F_DeviceName")] |
| 84 | 84 | public string DeviceName { get; set; } |
| 85 | + | |
| 86 | + /// <summary> | |
| 87 | + /// 查看客户(所属客户,根据记录类型+ReId 从 Khsb/Zlgl/Gzcx 等表解析) | |
| 88 | + /// </summary> | |
| 89 | + [SugarColumn(ColumnName = "F_CustomerName")] | |
| 90 | + public string CustomerName { get; set; } | |
| 85 | 91 | |
| 86 | 92 | } |
| 87 | 93 | } |
| 88 | 94 | \ No newline at end of file | ... | ... |
机具(服务端)/netcore/src/Modularity/Extend/NCC.Extend.Entitys/NCC.Extend.Entitys.xml
| ... | ... | @@ -3737,11 +3737,23 @@ |
| 3737 | 3737 | <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.TotalCount"> |
| 3738 | 3738 | <summary>总记录数</summary> |
| 3739 | 3739 | </member> |
| 3740 | + <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.CompleteCount"> | |
| 3741 | + <summary>完整记录数(有离开时间的记录)</summary> | |
| 3742 | + </member> | |
| 3743 | + <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.TotalDurationSeconds"> | |
| 3744 | + <summary>总停留时长(秒)</summary> | |
| 3745 | + </member> | |
| 3746 | + <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.AvgDurationSeconds"> | |
| 3747 | + <summary>平均停留时长(秒,仅统计完整记录)</summary> | |
| 3748 | + </member> | |
| 3740 | 3749 | <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.ByType"> |
| 3741 | 3750 | <summary>按记录类型统计 [{ name, value }]</summary> |
| 3742 | 3751 | </member> |
| 3743 | 3752 | <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.ByEquipment"> |
| 3744 | - <summary>按关联设备统计(前 N 条)</summary> | |
| 3753 | + <summary>按设备名称统计(前 N 条,使用 F_DeviceName)</summary> | |
| 3754 | + </member> | |
| 3755 | + <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.ByContent"> | |
| 3756 | + <summary>按查看内容统计(前 N 条,故障代码、知识名、培训名等)</summary> | |
| 3745 | 3757 | </member> |
| 3746 | 3758 | <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.ByCustomer"> |
| 3747 | 3759 | <summary>按客户统计(前 N 条)</summary> |
| ... | ... | @@ -3879,9 +3891,19 @@ |
| 3879 | 3891 | 记录类型 |
| 3880 | 3892 | </summary> |
| 3881 | 3893 | </member> |
| 3894 | + <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListOutput.contentName"> | |
| 3895 | + <summary> | |
| 3896 | + 查看内容(故障代码、知识名、资料名、备件名、培训名等) | |
| 3897 | + </summary> | |
| 3898 | + </member> | |
| 3899 | + <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListOutput.deviceName"> | |
| 3900 | + <summary> | |
| 3901 | + 设备名称(所属设备、关联产品名;优先 F_DeviceName,无则根据 reId 查表) | |
| 3902 | + </summary> | |
| 3903 | + </member> | |
| 3882 | 3904 | <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListOutput.equipmentName"> |
| 3883 | 3905 | <summary> |
| 3884 | - 关联设备(原关联记录,展示设备名称) | |
| 3906 | + 关联设备(兼容旧逻辑,展示设备名称;优先使用 deviceName) | |
| 3885 | 3907 | </summary> |
| 3886 | 3908 | </member> |
| 3887 | 3909 | <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListOutput.customerName"> |
| ... | ... | @@ -3949,6 +3971,16 @@ |
| 3949 | 3971 | 记录类型 |
| 3950 | 3972 | </summary> |
| 3951 | 3973 | </member> |
| 3974 | + <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListQueryInput.deviceName"> | |
| 3975 | + <summary> | |
| 3976 | + 设备名称(模糊筛选) | |
| 3977 | + </summary> | |
| 3978 | + </member> | |
| 3979 | + <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListQueryInput.contentName"> | |
| 3980 | + <summary> | |
| 3981 | + 查看内容(模糊筛选) | |
| 3982 | + </summary> | |
| 3983 | + </member> | |
| 3952 | 3984 | <member name="T:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsUpInput"> |
| 3953 | 3985 | <summary> |
| 3954 | 3986 | 查看历史记录更新输入参数 |
| ... | ... | @@ -8763,6 +8795,21 @@ |
| 8763 | 8795 | 记录类型 |
| 8764 | 8796 | </summary> |
| 8765 | 8797 | </member> |
| 8798 | + <member name="P:NCC.Extend.Entitys.SbRecordsEntity.ContentName"> | |
| 8799 | + <summary> | |
| 8800 | + 查看内容(故障代码、知识名、资料名、备件名、培训名等) | |
| 8801 | + </summary> | |
| 8802 | + </member> | |
| 8803 | + <member name="P:NCC.Extend.Entitys.SbRecordsEntity.DeviceName"> | |
| 8804 | + <summary> | |
| 8805 | + 设备名称(所属设备、关联产品名,便于「记录类型+设备名称+查看内容」三字段定位) | |
| 8806 | + </summary> | |
| 8807 | + </member> | |
| 8808 | + <member name="P:NCC.Extend.Entitys.SbRecordsEntity.CustomerName"> | |
| 8809 | + <summary> | |
| 8810 | + 查看客户(所属客户,根据记录类型+ReId 从 Khsb/Zlgl/Gzcx 等表解析) | |
| 8811 | + </summary> | |
| 8812 | + </member> | |
| 8766 | 8813 | <member name="T:NCC.Extend.Entitys.SbwhryEntity"> |
| 8767 | 8814 | <summary> |
| 8768 | 8815 | 设备维护人员 | ... | ... |
机具(服务端)/netcore/src/Modularity/Extend/NCC.Extend/SbRecordsService.cs
| ... | ... | @@ -65,6 +65,7 @@ namespace NCC.Extend.SbRecords |
| 65 | 65 | var now = DateTime.Now; |
| 66 | 66 | var resolvedContentName = !string.IsNullOrWhiteSpace(contentName) ? contentName : await ResolveContentNameAsync(type, reId); |
| 67 | 67 | var resolvedDeviceName = !string.IsNullOrWhiteSpace(deviceName) ? deviceName : await ResolveDeviceNameAsync(type, reId); |
| 68 | + var resolvedCustomerName = await ResolveCustomerNameAsync(type, reId); | |
| 68 | 69 | |
| 69 | 70 | var entity = new SbRecordsEntity |
| 70 | 71 | { |
| ... | ... | @@ -79,7 +80,8 @@ namespace NCC.Extend.SbRecords |
| 79 | 80 | Item2 = item2, |
| 80 | 81 | Type = type, |
| 81 | 82 | ContentName = resolvedContentName, |
| 82 | - DeviceName = resolvedDeviceName | |
| 83 | + DeviceName = resolvedDeviceName, | |
| 84 | + CustomerName = resolvedCustomerName | |
| 83 | 85 | }; |
| 84 | 86 | |
| 85 | 87 | var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync(); |
| ... | ... | @@ -196,6 +198,59 @@ namespace NCC.Extend.SbRecords |
| 196 | 198 | } |
| 197 | 199 | |
| 198 | 200 | /// <summary> |
| 201 | + /// 根据 type 和 reId 从对应业务表解析查看客户(所属客户)。 | |
| 202 | + /// 我的设备:ReId=Khsb.Id;故障排查/知识库/资料管理/备件支持/培训展示:通过产品关联 Khsb.Fl 获取 Sskh。 | |
| 203 | + /// </summary> | |
| 204 | + private async Task<string> ResolveCustomerNameAsync(string type, string reId) | |
| 205 | + { | |
| 206 | + if (string.IsNullOrWhiteSpace(reId)) return null; | |
| 207 | + var t = (type ?? "").Trim(); | |
| 208 | + | |
| 209 | + if (t.Contains("我的设备") || t.Contains("查看设备")) | |
| 210 | + { | |
| 211 | + var sskh = await _db.Queryable<KhsbEntity>().Where(x => x.Id == reId).Select(x => x.Sskh).FirstAsync(); | |
| 212 | + return sskh; | |
| 213 | + } | |
| 214 | + if (t.Contains("故障排查") || t.Contains("故障查询")) | |
| 215 | + { | |
| 216 | + var zlFl = await _db.Queryable<GzcxEntity, ZlglEntity>((g, z) => g.Sssb == z.Id) | |
| 217 | + .Where((g, z) => g.Id == reId) | |
| 218 | + .Select((g, z) => z.Fl) | |
| 219 | + .FirstAsync(); | |
| 220 | + if (string.IsNullOrEmpty(zlFl)) return null; | |
| 221 | + return await _db.Queryable<KhsbEntity>().Where(k => k.Fl == zlFl && k.Sskh != null && k.Sskh != "").Select(k => k.Sskh).FirstAsync(); | |
| 222 | + } | |
| 223 | + if (t.Contains("知识库")) | |
| 224 | + { | |
| 225 | + var sssb = await _db.Queryable<ZskEntity>().Where(x => x.Id == reId).Select(x => x.Sssb).FirstAsync(); | |
| 226 | + if (string.IsNullOrEmpty(sssb)) return null; | |
| 227 | + return await _db.Queryable<KhsbEntity>().Where(k => (k.Fl == sssb) && k.Sskh != null && k.Sskh != "").Select(k => k.Sskh).FirstAsync(); | |
| 228 | + } | |
| 229 | + if (t.Contains("资料管理")) | |
| 230 | + { | |
| 231 | + var zlFl = await _db.Queryable<ZlglEntity>().Where(x => x.Id == reId).Select(x => x.Fl).FirstAsync(); | |
| 232 | + if (string.IsNullOrEmpty(zlFl)) return null; | |
| 233 | + return await _db.Queryable<KhsbEntity>().Where(k => k.Fl == zlFl && k.Sskh != null && k.Sskh != "").Select(k => k.Sskh).FirstAsync(); | |
| 234 | + } | |
| 235 | + if (t.Contains("备件支持")) | |
| 236 | + { | |
| 237 | + var zlFl = await _db.Queryable<BjxxEntity, ZlglEntity>((b, z) => b.Sssb == z.Id) | |
| 238 | + .Where((b, z) => b.Id == reId) | |
| 239 | + .Select((b, z) => z.Fl) | |
| 240 | + .FirstAsync(); | |
| 241 | + if (string.IsNullOrEmpty(zlFl)) return null; | |
| 242 | + return await _db.Queryable<KhsbEntity>().Where(k => k.Fl == zlFl && k.Sskh != null && k.Sskh != "").Select(k => k.Sskh).FirstAsync(); | |
| 243 | + } | |
| 244 | + if (t.Contains("培训展示")) | |
| 245 | + { | |
| 246 | + var sssb = await _db.Queryable<PxzsEntity>().Where(x => x.Id == reId).Select(x => x.Sssb).FirstAsync(); | |
| 247 | + if (string.IsNullOrEmpty(sssb)) return null; | |
| 248 | + return await _db.Queryable<KhsbEntity>().Where(k => k.Fl == sssb && k.Sskh != null && k.Sskh != "").Select(k => k.Sskh).FirstAsync(); | |
| 249 | + } | |
| 250 | + return null; | |
| 251 | + } | |
| 252 | + | |
| 253 | + /// <summary> | |
| 199 | 254 | /// 结束一条设备查看会话记录(离开页面时调用),并计算停留时长。 |
| 200 | 255 | /// </summary> |
| 201 | 256 | /// <param name="recordId">开始会话时返回的记录 Id。</param> |
| ... | ... | @@ -270,35 +325,60 @@ namespace NCC.Extend.SbRecords |
| 270 | 325 | .WhereIF(queryAddTime != null, p => p.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59)) |
| 271 | 326 | .WhereIF(!string.IsNullOrEmpty(input.addUser), p => p.AddUser.Equals(input.addUser)) |
| 272 | 327 | .WhereIF(!string.IsNullOrEmpty(input.type), p => p.Type.Contains(input.type)) |
| 328 | + .WhereIF(!string.IsNullOrEmpty(input.deviceName), p => p.DeviceName != null && p.DeviceName.Contains(input.deviceName)) | |
| 329 | + .WhereIF(!string.IsNullOrEmpty(input.contentName), p => p.ContentName != null && p.ContentName.Contains(input.contentName)) | |
| 273 | 330 | .WhereIF(!string.IsNullOrEmpty(input.customerName), p => SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == p.ReId && k.Sskh != null && k.Sskh.Contains(input.customerName)).Any()); |
| 274 | 331 | |
| 275 | 332 | var total = await baseQuery.CountAsync(); |
| 276 | 333 | const int topN = 10; |
| 334 | + | |
| 335 | + var completeQuery = baseQuery.Where(p => p.LeaveTime != null); | |
| 336 | + var completeCount = await completeQuery.CountAsync(); | |
| 337 | + var totalDuration = completeCount > 0 ? await completeQuery.SumAsync(p => p.DurationSeconds) : 0L; | |
| 338 | + var avgDuration = completeCount > 0 ? (double)totalDuration / completeCount : 0; | |
| 339 | + | |
| 277 | 340 | var byTypeList = await baseQuery.GroupBy(p => p.Type ?? "") |
| 278 | 341 | .Select(g => new { Name = g.Type ?? "未分类", Value = SqlFunc.AggregateCount(g.Id) }) |
| 279 | 342 | .ToListAsync(); |
| 280 | 343 | var byType = byTypeList.OrderByDescending(x => x.Value).Select(x => new SbRecordsStatisticsItem { Name = x.Name ?? "未分类", Value = x.Value }).ToList(); |
| 281 | - var byEquipmentRawList = await baseQuery.GroupBy(p => p.ReId) | |
| 282 | - .Select(g => new { ReId = g.ReId, Value = SqlFunc.AggregateCount(g.Id) }) | |
| 344 | + | |
| 345 | + var byEquipmentRawList = await baseQuery.GroupBy(p => p.DeviceName ?? "") | |
| 346 | + .Select(g => new { Name = g.DeviceName ?? "未记录", Value = SqlFunc.AggregateCount(g.Id) }) | |
| 283 | 347 | .ToListAsync(); |
| 284 | - var byEquipmentRaw = byEquipmentRawList.OrderByDescending(x => x.Value).Take(topN).ToList(); | |
| 285 | - var reIds = byEquipmentRaw.Select(x => x.ReId).Distinct().ToList(); | |
| 286 | - var eqNames = await _db.Queryable<ZsbtzEntity>().Where(t => reIds.Contains(t.Id)).Select(t => new { t.Id, t.Sbmc }).ToListAsync(); | |
| 287 | - var khsbNames = await _db.Queryable<KhsbEntity>().Where(k => reIds.Contains(k.Id)).Select(k => new { k.Id, k.Sbmc }).ToListAsync(); | |
| 288 | - var byEquipment = byEquipmentRaw.Select(x => new SbRecordsStatisticsItem | |
| 289 | - { | |
| 290 | - Name = eqNames.FirstOrDefault(e => e.Id == x.ReId)?.Sbmc ?? khsbNames.FirstOrDefault(k => k.Id == x.ReId)?.Sbmc ?? x.ReId ?? "-", | |
| 291 | - Value = x.Value | |
| 292 | - }).ToList(); | |
| 293 | - var byUserRawList = await baseQuery.GroupBy(p => p.AddUser) | |
| 294 | - .Select(g => new { AddUser = g.AddUser, Value = SqlFunc.AggregateCount(g.Id) }) | |
| 348 | + var byEquipment = byEquipmentRawList | |
| 349 | + .Where(x => !string.IsNullOrEmpty(x.Name)) | |
| 350 | + .OrderByDescending(x => x.Value) | |
| 351 | + .Take(topN) | |
| 352 | + .Select(x => new SbRecordsStatisticsItem { Name = x.Name ?? "未记录", Value = x.Value }) | |
| 353 | + .ToList(); | |
| 354 | + | |
| 355 | + var byContentRawList = await baseQuery.GroupBy(p => p.ContentName ?? "") | |
| 356 | + .Select(g => new { Name = g.ContentName ?? "未记录", Value = SqlFunc.AggregateCount(g.Id) }) | |
| 357 | + .ToListAsync(); | |
| 358 | + var byContent = byContentRawList | |
| 359 | + .Where(x => !string.IsNullOrEmpty(x.Name)) | |
| 360 | + .OrderByDescending(x => x.Value) | |
| 361 | + .Take(topN) | |
| 362 | + .Select(x => new SbRecordsStatisticsItem { Name = x.Name ?? "未记录", Value = x.Value }) | |
| 363 | + .ToList(); | |
| 364 | + // 按查看用户统计:LEFT JOIN UserEntity 直接获取 RealName,避免 GroupBy 分组键与后续查表不一致 | |
| 365 | + var byUserQuery = _db.Queryable<SbRecordsEntity, UserEntity>((r, u) => new JoinQueryInfos(JoinType.Left, r.AddUser == u.Id)) | |
| 366 | + .WhereIF(!string.IsNullOrEmpty(input.reId), (r, u) => r.ReId.Equals(input.reId)) | |
| 367 | + .WhereIF(queryAddTime != null, (r, u) => r.AddTime >= new DateTime(startAddTime.ToDate().Year, startAddTime.ToDate().Month, startAddTime.ToDate().Day, 0, 0, 0)) | |
| 368 | + .WhereIF(queryAddTime != null, (r, u) => r.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59)) | |
| 369 | + .WhereIF(!string.IsNullOrEmpty(input.addUser), (r, u) => r.AddUser.Equals(input.addUser)) | |
| 370 | + .WhereIF(!string.IsNullOrEmpty(input.type), (r, u) => r.Type != null && r.Type.Contains(input.type)) | |
| 371 | + .WhereIF(!string.IsNullOrEmpty(input.deviceName), (r, u) => r.DeviceName != null && r.DeviceName.Contains(input.deviceName)) | |
| 372 | + .WhereIF(!string.IsNullOrEmpty(input.contentName), (r, u) => r.ContentName != null && r.ContentName.Contains(input.contentName)) | |
| 373 | + .WhereIF(!string.IsNullOrEmpty(input.customerName), (r, u) => SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == r.ReId && k.Sskh != null && k.Sskh.Contains(input.customerName)).Any()); | |
| 374 | + var byUserRawList = await byUserQuery | |
| 375 | + .GroupBy((r, u) => r.AddUser ?? "") | |
| 376 | + .Select((r, u) => new { AddUser = r.AddUser ?? "", RealName = SqlFunc.AggregateMax(u.RealName), Value = SqlFunc.AggregateCount(r.Id) }) | |
| 295 | 377 | .ToListAsync(); |
| 296 | - var byUserRaw = byUserRawList.OrderByDescending(x => x.Value).Take(topN).ToList(); | |
| 297 | - var userIds = byUserRaw.Select(x => x.AddUser).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); | |
| 298 | - var userNames = await _db.Queryable<UserEntity>().Where(u => userIds.Contains(u.Id)).Select(u => new { u.Id, u.RealName }).ToListAsync(); | |
| 378 | + var byUserRaw = byUserRawList.Where(x => !string.IsNullOrEmpty(x.AddUser)).OrderByDescending(x => x.Value).Take(topN).ToList(); | |
| 299 | 379 | var byUser = byUserRaw.Select(x => new SbRecordsStatisticsItem |
| 300 | 380 | { |
| 301 | - Name = string.IsNullOrEmpty(x.AddUser) ? "-" : (userNames.FirstOrDefault(u => u.Id == x.AddUser)?.RealName ?? x.AddUser), | |
| 381 | + Name = string.IsNullOrEmpty(x.RealName) ? x.AddUser : x.RealName, | |
| 302 | 382 | Value = x.Value |
| 303 | 383 | }).ToList(); |
| 304 | 384 | var byCustomerRawList = await _db.Queryable<SbRecordsEntity, KhsbEntity>((r, k) => r.ReId == k.Id) |
| ... | ... | @@ -317,8 +397,12 @@ namespace NCC.Extend.SbRecords |
| 317 | 397 | return new SbRecordsStatisticsOutput |
| 318 | 398 | { |
| 319 | 399 | TotalCount = total, |
| 400 | + CompleteCount = completeCount, | |
| 401 | + TotalDurationSeconds = totalDuration, | |
| 402 | + AvgDurationSeconds = Math.Round(avgDuration, 2), | |
| 320 | 403 | ByType = byType.Select(x => new SbRecordsStatisticsItem { Name = x.Name, Value = x.Value }).ToList(), |
| 321 | 404 | ByEquipment = byEquipment, |
| 405 | + ByContent = byContent, | |
| 322 | 406 | ByCustomer = byCustomer, |
| 323 | 407 | ByUser = byUser |
| 324 | 408 | }; |
| ... | ... | @@ -375,6 +459,8 @@ namespace NCC.Extend.SbRecords |
| 375 | 459 | .WhereIF(queryAddTime != null, p => p.AddTime >= new DateTime(startAddTime.ToDate().Year, startAddTime.ToDate().Month, startAddTime.ToDate().Day, 0, 0, 0)) |
| 376 | 460 | .WhereIF(queryAddTime != null, p => p.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59)) |
| 377 | 461 | .WhereIF(!string.IsNullOrEmpty(input.addUser), p => p.AddUser.Equals(input.addUser)) |
| 462 | + .WhereIF(!string.IsNullOrEmpty(input.deviceName), p => p.DeviceName != null && p.DeviceName.Contains(input.deviceName)) | |
| 463 | + .WhereIF(!string.IsNullOrEmpty(input.contentName), p => p.ContentName != null && p.ContentName.Contains(input.contentName)) | |
| 378 | 464 | .WhereIF(!string.IsNullOrEmpty(input.customerName), p => SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == p.ReId && k.Sskh != null && k.Sskh.Contains(input.customerName)).Any()) |
| 379 | 465 | .WhereIF(!string.IsNullOrEmpty(input.item1), p => p.Item1.Contains(input.item1)) |
| 380 | 466 | .WhereIF(!string.IsNullOrEmpty(input.item2), p => p.Item2.Contains(input.item2)) |
| ... | ... | @@ -386,7 +472,7 @@ namespace NCC.Extend.SbRecords |
| 386 | 472 | contentName = it.ContentName, |
| 387 | 473 | deviceName = it.DeviceName, |
| 388 | 474 | equipmentName = SqlFunc.IsNull(it.DeviceName, SqlFunc.IsNull(SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == it.ReId).Select(k => k.Sbmc), SqlFunc.Subqueryable<ZsbtzEntity>().Where(t => t.Id == it.ReId).Select(t => t.Sbmc))), |
| 389 | - customerName = SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == it.ReId).Select(k => k.Sskh), | |
| 475 | + customerName = SqlFunc.IsNull(it.CustomerName, SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == it.ReId).Select(k => k.Sskh)), | |
| 390 | 476 | addTime = it.AddTime, |
| 391 | 477 | addUser = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.AddUser).Select(u => u.RealName), |
| 392 | 478 | enable = it.Enable, |
| ... | ... | @@ -429,6 +515,8 @@ namespace NCC.Extend.SbRecords |
| 429 | 515 | .WhereIF(queryAddTime != null, p => p.AddTime >= new DateTime(startAddTime.ToDate().Year, startAddTime.ToDate().Month, startAddTime.ToDate().Day, 0, 0, 0)) |
| 430 | 516 | .WhereIF(queryAddTime != null, p => p.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59)) |
| 431 | 517 | .WhereIF(!string.IsNullOrEmpty(input.addUser), p => p.AddUser.Equals(input.addUser)) |
| 518 | + .WhereIF(!string.IsNullOrEmpty(input.deviceName), p => p.DeviceName != null && p.DeviceName.Contains(input.deviceName)) | |
| 519 | + .WhereIF(!string.IsNullOrEmpty(input.contentName), p => p.ContentName != null && p.ContentName.Contains(input.contentName)) | |
| 432 | 520 | .WhereIF(!string.IsNullOrEmpty(input.customerName), p => SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == p.ReId && k.Sskh != null && k.Sskh.Contains(input.customerName)).Any()) |
| 433 | 521 | .WhereIF(!string.IsNullOrEmpty(input.item1), p => p.Item1.Contains(input.item1)) |
| 434 | 522 | .WhereIF(!string.IsNullOrEmpty(input.item2), p => p.Item2.Contains(input.item2)) |
| ... | ... | @@ -440,7 +528,7 @@ namespace NCC.Extend.SbRecords |
| 440 | 528 | contentName = it.ContentName, |
| 441 | 529 | deviceName = it.DeviceName, |
| 442 | 530 | equipmentName = SqlFunc.IsNull(it.DeviceName, SqlFunc.IsNull(SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == it.ReId).Select(k => k.Sbmc), SqlFunc.Subqueryable<ZsbtzEntity>().Where(t => t.Id == it.ReId).Select(t => t.Sbmc))), |
| 443 | - customerName = SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == it.ReId).Select(k => k.Sskh), | |
| 531 | + customerName = SqlFunc.IsNull(it.CustomerName, SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == it.ReId).Select(k => k.Sskh)), | |
| 444 | 532 | addTime = it.AddTime, |
| 445 | 533 | addUser = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.AddUser).Select(u => u.RealName), |
| 446 | 534 | enable = it.Enable, | ... | ... |
机具(管理端)/src/views/sbRecords/index.vue
| 1 | 1 | <template> |
| 2 | - <div class="NCC-common-layout"> | |
| 2 | + <div class="NCC-common-layout sb-records-page"> | |
| 3 | 3 | <div class="NCC-common-layout-center"> |
| 4 | - <!-- 统计与图表 --> | |
| 5 | - <section class="sb-records-stats"> | |
| 6 | - <div class="stats-total"> | |
| 7 | - <span class="stats-total-label">记录总数</span> | |
| 8 | - <span class="stats-total-value">{{ statistics.totalCount }}</span> | |
| 9 | - </div> | |
| 10 | - <el-row :gutter="16" class="charts-row"> | |
| 11 | - <el-col :span="12"> | |
| 12 | - <div class="chart-card"> | |
| 13 | - <div class="chart-title">按记录类型</div> | |
| 14 | - <div ref="chartByType" class="chart-dom"></div> | |
| 15 | - </div> | |
| 16 | - </el-col> | |
| 17 | - <el-col :span="12"> | |
| 18 | - <div class="chart-card"> | |
| 19 | - <div class="chart-title">按关联设备(Top10)</div> | |
| 20 | - <div ref="chartByEquipment" class="chart-dom"></div> | |
| 21 | - </div> | |
| 22 | - </el-col> | |
| 23 | - <el-col :span="12"> | |
| 24 | - <div class="chart-card"> | |
| 25 | - <div class="chart-title">按客户(Top10)</div> | |
| 26 | - <div ref="chartByCustomer" class="chart-dom"></div> | |
| 27 | - </div> | |
| 28 | - </el-col> | |
| 29 | - <el-col :span="12"> | |
| 30 | - <div class="chart-card"> | |
| 31 | - <div class="chart-title">按查看用户(Top10)</div> | |
| 32 | - <div ref="chartByUser" class="chart-dom"></div> | |
| 33 | - </div> | |
| 34 | - </el-col> | |
| 35 | - </el-row> | |
| 36 | - </section> | |
| 37 | - | |
| 38 | - <el-row class="NCC-common-search-box" :gutter="16"> | |
| 39 | - <el-form @submit.native.prevent> | |
| 40 | - <el-col :span="6"> | |
| 41 | - <el-form-item label="关联设备"> | |
| 42 | - <el-select v-model="query.reId" placeholder="请选择关联设备" clearable filterable> | |
| 43 | - <el-option v-for="item in reIdOptions" :key="item.id" :label="item.fullName" :value="item.id" /> | |
| 44 | - </el-select> | |
| 45 | - </el-form-item> | |
| 46 | - </el-col> | |
| 47 | - <el-col :span="6"> | |
| 48 | - <el-form-item label="查看客户"> | |
| 49 | - <el-input v-model="query.customerName" placeholder="客户名称" clearable /> | |
| 50 | - </el-form-item> | |
| 51 | - </el-col> | |
| 52 | - <el-col :span="6"> | |
| 53 | - <el-form-item label="查看用户"> | |
| 54 | - <userSelect v-model="query.addUser" placeholder="请选择查看用户" /> | |
| 55 | - </el-form-item> | |
| 56 | - </el-col> | |
| 57 | - <el-col :span="6"> | |
| 58 | - <el-form-item label="记录类型"> | |
| 59 | - <el-input v-model="query.type" placeholder="记录类型" clearable /> | |
| 60 | - </el-form-item> | |
| 61 | - </el-col> | |
| 62 | - <el-col :span="6"> | |
| 63 | - <el-form-item> | |
| 64 | - <el-button type="primary" icon="el-icon-search" @click="search()">查询</el-button> | |
| 65 | - <el-button icon="el-icon-refresh-right" @click="reset()">重置</el-button> | |
| 66 | - <el-button type="text" icon="el-icon-arrow-down" @click="showAll=true" v-if="!showAll">展开</el-button> | |
| 67 | - <el-button type="text" icon="el-icon-arrow-up" @click="showAll=false" v-else>收起</el-button> | |
| 68 | - </el-form-item> | |
| 69 | - </el-col> | |
| 70 | - <template v-if="showAll"> | |
| 71 | - <el-col :span="6"> | |
| 72 | - <el-form-item label="记录时间"> | |
| 73 | - <el-date-picker v-model="query.addTime" type="datetimerange" value-format="timestamp" format="yyyy-MM-dd HH:mm:ss" start-placeholder="开始日期" end-placeholder="结束日期" style="width:100%" /> | |
| 4 | + <!-- 1. 筛选区:紧凑卡片 --> | |
| 5 | + <section class="sb-records-filter"> | |
| 6 | + <el-form @submit.native.prevent class="sb-records-form"> | |
| 7 | + <el-row :gutter="16"> | |
| 8 | + <el-col :xs="24" :sm="12" :md="8" :lg="6"> | |
| 9 | + <el-form-item label="关联设备"> | |
| 10 | + <el-select v-model="query.reId" placeholder="请选择" clearable filterable style="width:100%"> | |
| 11 | + <el-option v-for="item in reIdOptions" :key="item.id" :label="item.fullName" :value="item.id" /> | |
| 12 | + </el-select> | |
| 13 | + </el-form-item> | |
| 14 | + </el-col> | |
| 15 | + <el-col :xs="24" :sm="12" :md="8" :lg="6"> | |
| 16 | + <el-form-item label="查看客户"> | |
| 17 | + <el-input v-model="query.customerName" placeholder="客户名称" clearable /> | |
| 74 | 18 | </el-form-item> |
| 75 | 19 | </el-col> |
| 76 | - </template> | |
| 20 | + <el-col :xs="24" :sm="12" :md="8" :lg="6"> | |
| 21 | + <el-form-item label="查看用户"> | |
| 22 | + <userSelect v-model="query.addUser" placeholder="请选择" /> | |
| 23 | + </el-form-item> | |
| 24 | + </el-col> | |
| 25 | + <el-col :xs="24" :sm="12" :md="8" :lg="6"> | |
| 26 | + <el-form-item label="记录类型"> | |
| 27 | + <el-input v-model="query.type" placeholder="记录类型" clearable /> | |
| 28 | + </el-form-item> | |
| 29 | + </el-col> | |
| 30 | + <template v-if="showAll"> | |
| 31 | + <el-col :xs="24" :sm="12" :md="8" :lg="6"> | |
| 32 | + <el-form-item label="设备名称"> | |
| 33 | + <el-input v-model="query.deviceName" placeholder="设备名称" clearable /> | |
| 34 | + </el-form-item> | |
| 35 | + </el-col> | |
| 36 | + <el-col :xs="24" :sm="12" :md="8" :lg="6"> | |
| 37 | + <el-form-item label="查看内容"> | |
| 38 | + <el-input v-model="query.contentName" placeholder="故障代码、知识名等" clearable /> | |
| 39 | + </el-form-item> | |
| 40 | + </el-col> | |
| 41 | + <el-col :xs="24" :sm="12" :md="8" :lg="6"> | |
| 42 | + <el-form-item label="记录时间"> | |
| 43 | + <el-date-picker v-model="query.addTime" type="datetimerange" value-format="timestamp" format="yyyy-MM-dd HH:mm:ss" start-placeholder="开始" end-placeholder="结束" style="width:100%" /> | |
| 44 | + </el-form-item> | |
| 45 | + </el-col> | |
| 46 | + </template> | |
| 47 | + <el-col :xs="24" :sm="12" :md="8" :lg="6"> | |
| 48 | + <el-form-item label-width="0" class="sb-records-form-actions"> | |
| 49 | + <el-button type="primary" icon="el-icon-search" @click="search()">查询</el-button> | |
| 50 | + <el-button icon="el-icon-refresh-right" @click="reset()">重置</el-button> | |
| 51 | + <el-button type="text" @click="showAll=!showAll"> | |
| 52 | + {{ showAll ? '收起' : '展开' }} | |
| 53 | + <i :class="showAll ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" style="margin-left:4px" /> | |
| 54 | + </el-button> | |
| 55 | + </el-form-item> | |
| 56 | + </el-col> | |
| 57 | + </el-row> | |
| 77 | 58 | </el-form> |
| 78 | - </el-row> | |
| 59 | + </section> | |
| 79 | 60 | |
| 80 | - <div class="NCC-common-layout-main NCC-flex-main"> | |
| 81 | - <div class="NCC-common-head"> | |
| 82 | - <div> | |
| 83 | - <el-button type="text" icon="el-icon-download" @click="exportData()">导出</el-button> | |
| 84 | - <el-button type="text" icon="el-icon-delete" @click="handleBatchRemoveDel()">批量删除</el-button> | |
| 61 | + <!-- 2. 主内容区:统计 + 列表 --> | |
| 62 | + <div class="sb-records-scroll-wrap"> | |
| 63 | + <section class="sb-records-content"> | |
| 64 | + <!-- 统计卡片 --> | |
| 65 | + <div class="sb-records-summary"> | |
| 66 | + <el-row :gutter="16"> | |
| 67 | + <el-col :xs="12" :sm="12" :md="6"> | |
| 68 | + <div class="stat-card stat-card-primary"> | |
| 69 | + <div class="stat-card-label">记录总数</div> | |
| 70 | + <div class="stat-card-value">{{ statistics.totalCount }}</div> | |
| 71 | + </div> | |
| 72 | + </el-col> | |
| 73 | + <el-col :xs="12" :sm="12" :md="6"> | |
| 74 | + <div class="stat-card stat-card-success"> | |
| 75 | + <div class="stat-card-label">完整记录</div> | |
| 76 | + <div class="stat-card-value">{{ statistics.completeCount }}</div> | |
| 77 | + </div> | |
| 78 | + </el-col> | |
| 79 | + <el-col :xs="12" :sm="12" :md="6"> | |
| 80 | + <div class="stat-card stat-card-warning"> | |
| 81 | + <div class="stat-card-label">总停留时长</div> | |
| 82 | + <div class="stat-card-value">{{ formatDuration(statistics.totalDurationSeconds) }}</div> | |
| 83 | + </div> | |
| 84 | + </el-col> | |
| 85 | + <el-col :xs="12" :sm="12" :md="6"> | |
| 86 | + <div class="stat-card stat-card-info"> | |
| 87 | + <div class="stat-card-label">平均停留</div> | |
| 88 | + <div class="stat-card-value">{{ formatDuration(Math.round(statistics.avgDurationSeconds || 0)) }}</div> | |
| 89 | + </div> | |
| 90 | + </el-col> | |
| 91 | + </el-row> | |
| 85 | 92 | </div> |
| 86 | - <div class="NCC-common-head-right"> | |
| 87 | - <el-tooltip effect="dark" content="刷新" placement="top"> | |
| 88 | - <el-link icon="icon-ym icon-ym-Refresh NCC-common-head-icon" :underline="false" @click="reset()" /> | |
| 89 | - </el-tooltip> | |
| 90 | - <screenfull isContainer /> | |
| 93 | + | |
| 94 | + <!-- 列表区 --> | |
| 95 | + <div class="sb-records-list"> | |
| 96 | + <h3 class="sb-records-list-title">查看记录列表</h3> | |
| 97 | + <div class="sb-records-table-wrap"> | |
| 98 | + <NCC-table v-loading="listLoading" :data="list" :has-n-o="false" class="sb-records-table"> | |
| 99 | + <el-table-column type="index" label="序号" width="60" align="center" :index="indexMethod" /> | |
| 100 | + <el-table-column label="记录类型" align="left" width="100"> | |
| 101 | + <template slot-scope="scope">{{ scope.row.type || '无' }}</template> | |
| 102 | + </el-table-column> | |
| 103 | + <el-table-column label="设备名称" align="left" min-width="120" show-overflow-tooltip> | |
| 104 | + <template slot-scope="scope">{{ scope.row.deviceName || scope.row.equipmentName || scope.row.reId || '无' }}</template> | |
| 105 | + </el-table-column> | |
| 106 | + <el-table-column label="查看内容" align="left" min-width="120" show-overflow-tooltip> | |
| 107 | + <template slot-scope="scope">{{ scope.row.contentName || '无' }}</template> | |
| 108 | + </el-table-column> | |
| 109 | + <el-table-column label="查看客户" align="left" min-width="100" show-overflow-tooltip> | |
| 110 | + <template slot-scope="scope">{{ scope.row.customerName || '无' }}</template> | |
| 111 | + </el-table-column> | |
| 112 | + <el-table-column label="查看用户" align="left" min-width="100"> | |
| 113 | + <template slot-scope="scope">{{ scope.row.addUser || '无' }}</template> | |
| 114 | + </el-table-column> | |
| 115 | + <el-table-column prop="addTime" label="记录时间" align="left" width="160" :formatter="ncc.tableDateFormat" /> | |
| 116 | + </NCC-table> | |
| 117 | + </div> | |
| 118 | + <pagination :total="total" :page.sync="listQuery.currentPage" :limit.sync="listQuery.pageSize" @pagination="initData" /> | |
| 91 | 119 | </div> |
| 92 | - </div> | |
| 93 | - <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange"> | |
| 94 | - <el-table-column type="index" label="序号" width="60" align="center" :index="indexMethod" /> | |
| 95 | - <el-table-column prop="type" label="记录类型" align="left" width="100" /> | |
| 96 | - <el-table-column label="设备名称" align="left" min-width="120" show-overflow-tooltip> | |
| 97 | - <template slot-scope="scope">{{ scope.row.deviceName || scope.row.equipmentName || scope.row.reId || '-' }}</template> | |
| 98 | - </el-table-column> | |
| 99 | - <el-table-column prop="contentName" label="查看内容" align="left" min-width="120" show-overflow-tooltip /> | |
| 100 | - <el-table-column prop="customerName" label="查看客户" align="left" min-width="100" show-overflow-tooltip /> | |
| 101 | - <el-table-column prop="addUser" label="查看用户" align="left" min-width="100" /> | |
| 102 | - <el-table-column prop="addTime" label="记录时间" align="left" width="160" :formatter="ncc.tableDateFormat" /> | |
| 103 | - <el-table-column label="操作" fixed="right" width="100"> | |
| 104 | - <template slot-scope="scope"> | |
| 105 | - <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn">删除</el-button> | |
| 106 | - </template> | |
| 107 | - </el-table-column> | |
| 108 | - </NCC-table> | |
| 109 | - <pagination :total="total" :page.sync="listQuery.currentPage" :limit.sync="listQuery.pageSize" @pagination="initData" /> | |
| 120 | + </section> | |
| 121 | + | |
| 122 | + <!-- 3. 统计图表:可折叠 --> | |
| 123 | + <section class="sb-records-charts"> | |
| 124 | + <el-collapse v-model="chartsExpanded" @change="handleChartsExpand"> | |
| 125 | + <el-collapse-item name="charts"> | |
| 126 | + <template slot="title"> | |
| 127 | + <span class="charts-collapse-title"> | |
| 128 | + <i class="el-icon-data-analysis"></i> | |
| 129 | + 统计图表 | |
| 130 | + </span> | |
| 131 | + </template> | |
| 132 | + <el-row :gutter="16"> | |
| 133 | + <el-col :xs="24" :sm="24" :md="12"> | |
| 134 | + <div class="chart-card"> | |
| 135 | + <div class="chart-title">按记录类型</div> | |
| 136 | + <div ref="chartByType" class="chart-dom"></div> | |
| 137 | + </div> | |
| 138 | + </el-col> | |
| 139 | + <el-col :xs="24" :sm="24" :md="12"> | |
| 140 | + <div class="chart-card"> | |
| 141 | + <div class="chart-title">按设备名称(Top10)</div> | |
| 142 | + <div ref="chartByEquipment" class="chart-dom"></div> | |
| 143 | + </div> | |
| 144 | + </el-col> | |
| 145 | + <el-col :xs="24" :sm="24" :md="12"> | |
| 146 | + <div class="chart-card"> | |
| 147 | + <div class="chart-title">按查看内容(Top10)</div> | |
| 148 | + <div ref="chartByContent" class="chart-dom"></div> | |
| 149 | + </div> | |
| 150 | + </el-col> | |
| 151 | + <el-col :xs="24" :sm="24" :md="12"> | |
| 152 | + <div class="chart-card"> | |
| 153 | + <div class="chart-title">按客户(Top10)</div> | |
| 154 | + <div ref="chartByCustomer" class="chart-dom"></div> | |
| 155 | + </div> | |
| 156 | + </el-col> | |
| 157 | + <el-col :xs="24" :sm="24" :md="12"> | |
| 158 | + <div class="chart-card"> | |
| 159 | + <div class="chart-title">按查看用户(Top10)</div> | |
| 160 | + <div ref="chartByUser" class="chart-dom"></div> | |
| 161 | + </div> | |
| 162 | + </el-col> | |
| 163 | + </el-row> | |
| 164 | + </el-collapse-item> | |
| 165 | + </el-collapse> | |
| 166 | + </section> | |
| 110 | 167 | </div> |
| 111 | 168 | </div> |
| 112 | 169 | <NCC-Form v-if="formVisible" ref="NCCForm" @refresh="refresh" /> |
| 113 | - <ExportBox v-if="exportBoxVisible" ref="ExportBox" @download="download" /> | |
| 114 | 170 | </div> |
| 115 | 171 | </template> |
| 116 | 172 | |
| 117 | 173 | <script> |
| 118 | 174 | import request from '@/utils/request' |
| 119 | 175 | import NCCForm from './Form' |
| 120 | -import ExportBox from './ExportBox' | |
| 121 | 176 | import echarts from 'echarts' |
| 122 | 177 | |
| 123 | 178 | export default { |
| 124 | - components: { NCCForm, ExportBox }, | |
| 179 | + components: { NCCForm }, | |
| 125 | 180 | data() { |
| 126 | 181 | return { |
| 127 | 182 | showAll: false, |
| ... | ... | @@ -130,11 +185,12 @@ export default { |
| 130 | 185 | addTime: undefined, |
| 131 | 186 | reId: undefined, |
| 132 | 187 | customerName: undefined, |
| 133 | - type: undefined | |
| 188 | + type: undefined, | |
| 189 | + deviceName: undefined, | |
| 190 | + contentName: undefined | |
| 134 | 191 | }, |
| 135 | 192 | list: [], |
| 136 | 193 | listLoading: true, |
| 137 | - multipleSelection: [], | |
| 138 | 194 | total: 0, |
| 139 | 195 | listQuery: { |
| 140 | 196 | currentPage: 1, |
| ... | ... | @@ -143,26 +199,19 @@ export default { |
| 143 | 199 | sidx: '' |
| 144 | 200 | }, |
| 145 | 201 | formVisible: false, |
| 146 | - exportBoxVisible: false, | |
| 202 | + chartsExpanded: [], // 默认折叠;['charts'] 则展开统计图表 | |
| 147 | 203 | statistics: { |
| 148 | 204 | totalCount: 0, |
| 205 | + completeCount: 0, | |
| 206 | + totalDurationSeconds: 0, | |
| 207 | + avgDurationSeconds: 0, | |
| 149 | 208 | byType: [], |
| 150 | 209 | byEquipment: [], |
| 210 | + byContent: [], | |
| 151 | 211 | byCustomer: [], |
| 152 | 212 | byUser: [] |
| 153 | 213 | }, |
| 154 | 214 | charts: {}, |
| 155 | - columnList: [ | |
| 156 | - { prop: 'index', label: '序号' }, | |
| 157 | - { prop: 'customerName', label: '客户' }, | |
| 158 | - { prop: 'addUser', label: '用户' }, | |
| 159 | - { prop: 'type', label: '记录类型' }, | |
| 160 | - { prop: 'deviceName', label: '设备名称' }, | |
| 161 | - { prop: 'contentName', label: '查看内容' }, | |
| 162 | - { prop: 'equipmentName', label: '设备' }, | |
| 163 | - { prop: 'addTime', label: '时间' }, | |
| 164 | - { prop: 'countNum', label: '次数' } | |
| 165 | - ], | |
| 166 | 215 | reIdOptions: [] |
| 167 | 216 | } |
| 168 | 217 | }, |
| ... | ... | @@ -193,13 +242,17 @@ export default { |
| 193 | 242 | method: 'GET', |
| 194 | 243 | data: query |
| 195 | 244 | }).then(res => { |
| 196 | - const data = res.data || {} | |
| 245 | + const d = res.data || {} | |
| 197 | 246 | this.statistics = { |
| 198 | - totalCount: data.totalCount != null ? data.totalCount : 0, | |
| 199 | - byType: data.byType || [], | |
| 200 | - byEquipment: data.byEquipment || [], | |
| 201 | - byCustomer: data.byCustomer || [], | |
| 202 | - byUser: data.byUser || [] | |
| 247 | + totalCount: d.totalCount != null ? d.totalCount : (d.TotalCount != null ? d.TotalCount : 0), | |
| 248 | + completeCount: d.completeCount != null ? d.completeCount : (d.CompleteCount != null ? d.CompleteCount : 0), | |
| 249 | + totalDurationSeconds: d.totalDurationSeconds != null ? d.totalDurationSeconds : (d.TotalDurationSeconds != null ? d.TotalDurationSeconds : 0), | |
| 250 | + avgDurationSeconds: d.avgDurationSeconds != null ? d.avgDurationSeconds : (d.AvgDurationSeconds != null ? d.AvgDurationSeconds : 0), | |
| 251 | + byType: d.byType || d.ByType || [], | |
| 252 | + byEquipment: d.byEquipment || d.ByEquipment || [], | |
| 253 | + byContent: d.byContent || d.ByContent || [], | |
| 254 | + byCustomer: d.byCustomer || d.ByCustomer || [], | |
| 255 | + byUser: d.byUser || d.ByUser || [] | |
| 203 | 256 | } |
| 204 | 257 | this.$nextTick(() => { |
| 205 | 258 | this.renderCharts() |
| ... | ... | @@ -208,6 +261,9 @@ export default { |
| 208 | 261 | this.$nextTick(() => this.renderCharts()) |
| 209 | 262 | }) |
| 210 | 263 | }, |
| 264 | + handleChartsExpand() { | |
| 265 | + this.$nextTick(() => this.renderCharts()) | |
| 266 | + }, | |
| 211 | 267 | buildQuery() { |
| 212 | 268 | const _query = { ...this.listQuery, ...this.query } |
| 213 | 269 | const query = {} |
| ... | ... | @@ -221,10 +277,23 @@ export default { |
| 221 | 277 | return query |
| 222 | 278 | }, |
| 223 | 279 | renderCharts() { |
| 224 | - this.renderPie('chartByType', this.statistics.byType, '记录类型') | |
| 225 | - this.renderBar('chartByEquipment', this.statistics.byEquipment, '关联设备') | |
| 226 | - this.renderBar('chartByCustomer', this.statistics.byCustomer, '客户') | |
| 227 | - this.renderBar('chartByUser', this.statistics.byUser, '查看用户') | |
| 280 | + if (!this.chartsExpanded.includes('charts')) return | |
| 281 | + this.$nextTick(() => { | |
| 282 | + this.renderPie('chartByType', this.statistics.byType, '记录类型') | |
| 283 | + this.renderBar('chartByEquipment', this.statistics.byEquipment, '设备名称') | |
| 284 | + this.renderBar('chartByContent', this.statistics.byContent, '查看内容') | |
| 285 | + this.renderBar('chartByCustomer', this.statistics.byCustomer, '客户') | |
| 286 | + this.renderBar('chartByUser', this.statistics.byUser, '查看用户') | |
| 287 | + }) | |
| 288 | + }, | |
| 289 | + formatDuration(seconds) { | |
| 290 | + if (seconds == null || seconds < 0) return '0秒' | |
| 291 | + if (seconds < 60) return seconds + '秒' | |
| 292 | + if (seconds < 3600) return Math.floor(seconds / 60) + '分' + (seconds % 60 ? seconds % 60 + '秒' : '') | |
| 293 | + const h = Math.floor(seconds / 3600) | |
| 294 | + const m = Math.floor((seconds % 3600) / 60) | |
| 295 | + const s = seconds % 60 | |
| 296 | + return h + '时' + (m ? m + '分' : '') + (s ? s + '秒' : '') | |
| 228 | 297 | }, |
| 229 | 298 | renderPie(refName, data, title) { |
| 230 | 299 | const el = this.$refs[refName] |
| ... | ... | @@ -238,7 +307,7 @@ export default { |
| 238 | 307 | this.charts[refName] = chart |
| 239 | 308 | const option = { |
| 240 | 309 | tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, |
| 241 | - series: [{ type: 'pie', radius: '60%', data: data.map(d => ({ name: d.name || '-', value: d.value })) }] | |
| 310 | + series: [{ type: 'pie', radius: '60%', data: data.map(d => ({ name: d.name || d.Name || '-', value: d.value != null ? d.value : (d.Value != null ? d.Value : 0) })) }] | |
| 242 | 311 | } |
| 243 | 312 | chart.setOption(option) |
| 244 | 313 | }, |
| ... | ... | @@ -248,8 +317,8 @@ export default { |
| 248 | 317 | if (this.charts[refName]) this.charts[refName].dispose() |
| 249 | 318 | const chart = echarts.init(el) |
| 250 | 319 | this.charts[refName] = chart |
| 251 | - const names = (data || []).map(d => (d.name || '-').substring(0, 8)) | |
| 252 | - const values = (data || []).map(d => d.value) | |
| 320 | + const names = (data || []).map(d => ((d.name || d.Name || '-') + '').substring(0, 12)) | |
| 321 | + const values = (data || []).map(d => d.value != null ? d.value : (d.Value != null ? d.Value : 0)) | |
| 253 | 322 | const option = { |
| 254 | 323 | tooltip: { trigger: 'axis' }, |
| 255 | 324 | grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, |
| ... | ... | @@ -285,53 +354,10 @@ export default { |
| 285 | 354 | this.initData() |
| 286 | 355 | this.loadStatistics() |
| 287 | 356 | }, |
| 288 | - handleDel(id) { | |
| 289 | - this.$confirm('此操作将永久删除该数据, 是否继续?', '提示', { type: 'warning' }).then(() => { | |
| 290 | - request({ url: `/api/Extend/SbRecords/${id}`, method: 'DELETE' }).then(res => { | |
| 291 | - this.$message.success(res.msg) | |
| 292 | - this.initData() | |
| 293 | - this.loadStatistics() | |
| 294 | - }) | |
| 295 | - }).catch(() => {}) | |
| 296 | - }, | |
| 297 | - handleSelectionChange(val) { | |
| 298 | - this.multipleSelection = (val || []).map(item => item.id) | |
| 299 | - }, | |
| 300 | - handleBatchRemoveDel() { | |
| 301 | - if (!this.multipleSelection.length) { | |
| 302 | - this.$message.error('请选择一条数据') | |
| 303 | - return | |
| 304 | - } | |
| 305 | - this.$confirm('您确定要删除这些数据吗, 是否继续?', '提示', { type: 'warning' }).then(() => { | |
| 306 | - request({ url: '/api/Extend/SbRecords/batchRemove', method: 'POST', data: this.multipleSelection }).then(res => { | |
| 307 | - this.$message.success(res.msg) | |
| 308 | - this.initData() | |
| 309 | - this.loadStatistics() | |
| 310 | - }) | |
| 311 | - }).catch(() => {}) | |
| 312 | - }, | |
| 313 | 357 | addOrUpdateHandle(id, isDetail) { |
| 314 | 358 | this.formVisible = true |
| 315 | 359 | this.$nextTick(() => this.$refs.NCCForm.init(id, isDetail)) |
| 316 | 360 | }, |
| 317 | - exportData() { | |
| 318 | - this.exportBoxVisible = true | |
| 319 | - this.$nextTick(() => this.$refs.ExportBox.init(this.columnList)) | |
| 320 | - }, | |
| 321 | - download(data) { | |
| 322 | - const query = { ...data, ...this.listQuery, ...this.query } | |
| 323 | - const q = {} | |
| 324 | - for (const key in query) { | |
| 325 | - if (Array.isArray(query[key])) q[key] = query[key].join() | |
| 326 | - else q[key] = query[key] | |
| 327 | - } | |
| 328 | - request({ url: '/api/Extend/SbRecords/Actions/Export', method: 'GET', data: q }).then(res => { | |
| 329 | - if (!res.data || !res.data.url) return | |
| 330 | - window.location.href = this.define.comUrl + res.data.url | |
| 331 | - this.$refs.ExportBox.visible = false | |
| 332 | - this.exportBoxVisible = false | |
| 333 | - }) | |
| 334 | - }, | |
| 335 | 361 | refresh(isrRefresh) { |
| 336 | 362 | this.formVisible = false |
| 337 | 363 | if (isrRefresh) { |
| ... | ... | @@ -344,26 +370,141 @@ export default { |
| 344 | 370 | </script> |
| 345 | 371 | |
| 346 | 372 | <style lang="scss" scoped> |
| 347 | -.sb-records-stats { | |
| 373 | +/* 页面整体:Data-Dense Dashboard 风格 */ | |
| 374 | +.sb-records-page .NCC-common-layout-center { | |
| 375 | + background: #F8FAFC; | |
| 376 | + padding: 16px; | |
| 377 | +} | |
| 378 | + | |
| 379 | +/* 1. 筛选区 */ | |
| 380 | +.sb-records-filter { | |
| 381 | + background: #fff; | |
| 382 | + border-radius: 12px; | |
| 383 | + padding: 20px; | |
| 384 | + margin-bottom: 16px; | |
| 385 | + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); | |
| 386 | + flex-shrink: 0; | |
| 387 | +} | |
| 388 | +.sb-records-form { | |
| 389 | + .el-form-item { margin-bottom: 16px; } | |
| 390 | + .el-form-item__label { padding-right: 12px; } | |
| 391 | +} | |
| 392 | +.sb-records-form-actions { | |
| 393 | + .el-button + .el-button { margin-left: 8px; } | |
| 394 | +} | |
| 395 | + | |
| 396 | +/* 2. 可滚动内容区 */ | |
| 397 | +.sb-records-scroll-wrap { | |
| 398 | + flex: 1; | |
| 399 | + min-height: 0; | |
| 400 | + overflow-y: auto; | |
| 401 | + overflow-x: hidden; | |
| 402 | +} | |
| 403 | + | |
| 404 | +/* 3. 主内容区:统计 + 列表 */ | |
| 405 | +.sb-records-content { | |
| 406 | + background: #fff; | |
| 407 | + border-radius: 12px; | |
| 408 | + padding: 20px; | |
| 348 | 409 | margin-bottom: 16px; |
| 349 | - .stats-total { | |
| 350 | - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 351 | - color: #fff; | |
| 352 | - padding: 16px 24px; | |
| 353 | - border-radius: 8px; | |
| 354 | - margin-bottom: 16px; | |
| 355 | - .stats-total-label { font-size: 14px; opacity: 0.9; } | |
| 356 | - .stats-total-value { font-size: 28px; font-weight: 700; margin-left: 12px; } | |
| 410 | + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); | |
| 411 | +} | |
| 412 | + | |
| 413 | +/* 统计卡片 */ | |
| 414 | +.sb-records-summary { | |
| 415 | + margin-bottom: 20px; | |
| 416 | + .el-row { margin: 0 -8px; } | |
| 417 | + .el-col { padding: 0 8px; margin-bottom: 16px; } | |
| 418 | + .stat-card { | |
| 419 | + height: 100px; | |
| 420 | + padding: 12px; | |
| 421 | + border-radius: 12px; | |
| 422 | + display: flex; | |
| 423 | + flex-direction: column; | |
| 424 | + justify-content: center; | |
| 425 | + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); | |
| 426 | + transition: box-shadow 0.2s ease, transform 0.2s ease; | |
| 427 | + cursor: default; | |
| 428 | + &:hover { | |
| 429 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); | |
| 430 | + } | |
| 431 | + .stat-card-label { | |
| 432 | + font-size: 13px; | |
| 433 | + color: rgba(255, 255, 255, 0.9); | |
| 434 | + margin-bottom: 6px; | |
| 435 | + letter-spacing: 0.3px; | |
| 436 | + } | |
| 437 | + .stat-card-value { | |
| 438 | + font-size: 22px; | |
| 439 | + font-weight: 600; | |
| 440 | + color: #fff; | |
| 441 | + letter-spacing: 0.5px; | |
| 442 | + } | |
| 443 | + &.stat-card-primary { background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%); } | |
| 444 | + &.stat-card-success { background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%); } | |
| 445 | + &.stat-card-warning { background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%); } | |
| 446 | + &.stat-card-info { background: linear-gradient(135deg, #909399 0%, #a6a9ad 100%); } | |
| 357 | 447 | } |
| 358 | - .charts-row { margin-top: 12px; } | |
| 359 | - .chart-card { | |
| 360 | - background: #fff; | |
| 361 | - border-radius: 8px; | |
| 362 | - padding: 16px; | |
| 363 | - margin-bottom: 16px; | |
| 364 | - box-shadow: 0 1px 4px rgba(0,0,0,0.08); | |
| 365 | - .chart-title { font-size: 14px; color: #303133; margin-bottom: 12px; font-weight: 500; } | |
| 366 | - .chart-dom { height: 260px; } | |
| 448 | +} | |
| 449 | + | |
| 450 | +/* 列表区 */ | |
| 451 | +.sb-records-list { | |
| 452 | + .sb-records-list-title { | |
| 453 | + font-size: 16px; | |
| 454 | + font-weight: 600; | |
| 455 | + color: #303133; | |
| 456 | + margin: 0 0 16px; | |
| 457 | + padding-bottom: 12px; | |
| 458 | + border-bottom: 1px solid #ebeef5; | |
| 459 | + letter-spacing: 0.3px; | |
| 367 | 460 | } |
| 368 | 461 | } |
| 462 | +.sb-records-table-wrap { | |
| 463 | + margin-bottom: 16px; | |
| 464 | +} | |
| 465 | +::v-deep .sb-records-table .el-table__row:hover { | |
| 466 | + background-color: #f5f7fa; | |
| 467 | +} | |
| 468 | + | |
| 469 | +/* 4. 统计图表 */ | |
| 470 | +.sb-records-charts { | |
| 471 | + background: #fff; | |
| 472 | + border-radius: 12px; | |
| 473 | + padding: 0; | |
| 474 | + margin-bottom: 16px; | |
| 475 | + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); | |
| 476 | + overflow: hidden; | |
| 477 | +} | |
| 478 | +.sb-records-charts .charts-collapse-title { | |
| 479 | + font-size: 14px; | |
| 480 | + font-weight: 600; | |
| 481 | + letter-spacing: 0.3px; | |
| 482 | + i { margin-right: 8px; color: #409EFF; } | |
| 483 | +} | |
| 484 | +.sb-records-charts .el-row { margin: 0 -8px; } | |
| 485 | +.sb-records-charts .el-col { padding: 0 8px; margin-bottom: 16px; } | |
| 486 | +.sb-records-charts .chart-card { | |
| 487 | + background: #fafbfc; | |
| 488 | + border-radius: 8px; | |
| 489 | + padding: 16px 20px; | |
| 490 | + border: 1px solid #ebeef5; | |
| 491 | + .chart-title { | |
| 492 | + font-size: 14px; | |
| 493 | + color: #303133; | |
| 494 | + margin-bottom: 12px; | |
| 495 | + font-weight: 500; | |
| 496 | + } | |
| 497 | + .chart-dom { height: 260px; } | |
| 498 | +} | |
| 499 | + | |
| 500 | +::v-deep .el-collapse-item__header { | |
| 501 | + height: 52px; | |
| 502 | + font-size: 14px; | |
| 503 | + padding: 0 20px; | |
| 504 | + background: #fff; | |
| 505 | +} | |
| 506 | +::v-deep .el-collapse-item__content { | |
| 507 | + padding: 20px; | |
| 508 | + background: #fff; | |
| 509 | +} | |
| 369 | 510 | </style> | ... | ... |