Commit 787af94f4513caef4bc11db91d9b709197e7ffd4

Authored by “wangming”
1 parent a382d270

完善设备查看会话记录功能,新增客户名称解析,更新统计输出和查询输入结构,优化前端筛选界面以支持设备名称和查看内容的模糊筛选。

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
  1 +-- 为 sb_records 表新增 F_CustomerName 字段
  2 +-- 用于存储「查看客户」,根据记录类型+ReId 从 Khsb/Zlgl/Gzcx 等表解析
  3 +-- 新记录由 StartViewRecord 自动填充;历史记录可保留空,列表接口会回退到 Khsb 子查询(仅对「我的设备」有效)
  4 +
  5 +ALTER TABLE `sb_records`
  6 + ADD COLUMN `F_CustomerName` varchar(200) NULL COMMENT '查看客户(所属客户)' AFTER `F_DeviceName`;
docs/sql/add_sb_records_device_name.sql 0 → 100644
  1 +-- 为 sb_records 表新增 F_DeviceName 字段(若已执行 add_sb_records_content_name.sql 可单独执行本脚本)
  2 +-- 实现「记录类型+设备名称+查看内容」三字段定位
  3 +
  4 +ALTER TABLE `sb_records`
  5 + ADD COLUMN `F_DeviceName` varchar(200) NULL COMMENT '设备名称(所属设备、关联产品名)' AFTER `F_Type`;
uniapp_jiju/apis/modules/oauth.js
@@ -236,4 +236,12 @@ export default { @@ -236,4 +236,12 @@ export default {
236 hqlogo(data){ 236 hqlogo(data){
237 return request.get('/api/Extend/Logogl?lx=' + '移动端') 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,12 +10,24 @@ namespace NCC.Extend.Entitys.Dto.SbRecords
10 /// <summary>总记录数</summary> 10 /// <summary>总记录数</summary>
11 public int TotalCount { get; set; } 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 /// <summary>按记录类型统计 [{ name, value }]</summary> 22 /// <summary>按记录类型统计 [{ name, value }]</summary>
14 public List<SbRecordsStatisticsItem> ByType { get; set; } 23 public List<SbRecordsStatisticsItem> ByType { get; set; }
15 24
16 - /// <summary>按关联设备统计(前 N 条)</summary> 25 + /// <summary>按设备名称统计(前 N 条,使用 F_DeviceName)</summary>
17 public List<SbRecordsStatisticsItem> ByEquipment { get; set; } 26 public List<SbRecordsStatisticsItem> ByEquipment { get; set; }
18 27
  28 + /// <summary>按查看内容统计(前 N 条,故障代码、知识名、培训名等)</summary>
  29 + public List<SbRecordsStatisticsItem> ByContent { get; set; }
  30 +
19 /// <summary>按客户统计(前 N 条)</summary> 31 /// <summary>按客户统计(前 N 条)</summary>
20 public List<SbRecordsStatisticsItem> ByCustomer { get; set; } 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,6 +53,15 @@ namespace NCC.Extend.Entitys.Dto.SbRecords
53 /// 记录类型 53 /// 记录类型
54 /// </summary> 54 /// </summary>
55 public string type { get; set; } 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,6 +82,12 @@ namespace NCC.Extend.Entitys
82 /// </summary> 82 /// </summary>
83 [SugarColumn(ColumnName = "F_DeviceName")] 83 [SugarColumn(ColumnName = "F_DeviceName")]
84 public string DeviceName { get; set; } 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 \ No newline at end of file 94 \ No newline at end of file
机具(服务端)/netcore/src/Modularity/Extend/NCC.Extend.Entitys/NCC.Extend.Entitys.xml
@@ -3737,11 +3737,23 @@ @@ -3737,11 +3737,23 @@
3737 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.TotalCount"> 3737 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.TotalCount">
3738 <summary>总记录数</summary> 3738 <summary>总记录数</summary>
3739 </member> 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 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.ByType"> 3749 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.ByType">
3741 <summary>按记录类型统计 [{ name, value }]</summary> 3750 <summary>按记录类型统计 [{ name, value }]</summary>
3742 </member> 3751 </member>
3743 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.ByEquipment"> 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 </member> 3757 </member>
3746 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.ByCustomer"> 3758 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsStatisticsOutput.ByCustomer">
3747 <summary>按客户统计(前 N 条)</summary> 3759 <summary>按客户统计(前 N 条)</summary>
@@ -3879,9 +3891,19 @@ @@ -3879,9 +3891,19 @@
3879 记录类型 3891 记录类型
3880 </summary> 3892 </summary>
3881 </member> 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 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListOutput.equipmentName"> 3904 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListOutput.equipmentName">
3883 <summary> 3905 <summary>
3884 - 关联设备(原关联记录,展示设备名称 3906 + 关联设备(兼容旧逻辑,展示设备名称;优先使用 deviceName
3885 </summary> 3907 </summary>
3886 </member> 3908 </member>
3887 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListOutput.customerName"> 3909 <member name="P:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsListOutput.customerName">
@@ -3949,6 +3971,16 @@ @@ -3949,6 +3971,16 @@
3949 记录类型 3971 记录类型
3950 </summary> 3972 </summary>
3951 </member> 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 <member name="T:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsUpInput"> 3984 <member name="T:NCC.Extend.Entitys.Dto.SbRecords.SbRecordsUpInput">
3953 <summary> 3985 <summary>
3954 查看历史记录更新输入参数 3986 查看历史记录更新输入参数
@@ -8763,6 +8795,21 @@ @@ -8763,6 +8795,21 @@
8763 记录类型 8795 记录类型
8764 </summary> 8796 </summary>
8765 </member> 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 <member name="T:NCC.Extend.Entitys.SbwhryEntity"> 8813 <member name="T:NCC.Extend.Entitys.SbwhryEntity">
8767 <summary> 8814 <summary>
8768 设备维护人员 8815 设备维护人员
机具(服务端)/netcore/src/Modularity/Extend/NCC.Extend/SbRecordsService.cs
@@ -65,6 +65,7 @@ namespace NCC.Extend.SbRecords @@ -65,6 +65,7 @@ namespace NCC.Extend.SbRecords
65 var now = DateTime.Now; 65 var now = DateTime.Now;
66 var resolvedContentName = !string.IsNullOrWhiteSpace(contentName) ? contentName : await ResolveContentNameAsync(type, reId); 66 var resolvedContentName = !string.IsNullOrWhiteSpace(contentName) ? contentName : await ResolveContentNameAsync(type, reId);
67 var resolvedDeviceName = !string.IsNullOrWhiteSpace(deviceName) ? deviceName : await ResolveDeviceNameAsync(type, reId); 67 var resolvedDeviceName = !string.IsNullOrWhiteSpace(deviceName) ? deviceName : await ResolveDeviceNameAsync(type, reId);
  68 + var resolvedCustomerName = await ResolveCustomerNameAsync(type, reId);
68 69
69 var entity = new SbRecordsEntity 70 var entity = new SbRecordsEntity
70 { 71 {
@@ -79,7 +80,8 @@ namespace NCC.Extend.SbRecords @@ -79,7 +80,8 @@ namespace NCC.Extend.SbRecords
79 Item2 = item2, 80 Item2 = item2,
80 Type = type, 81 Type = type,
81 ContentName = resolvedContentName, 82 ContentName = resolvedContentName,
82 - DeviceName = resolvedDeviceName 83 + DeviceName = resolvedDeviceName,
  84 + CustomerName = resolvedCustomerName
83 }; 85 };
84 86
85 var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync(); 87 var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync();
@@ -196,6 +198,59 @@ namespace NCC.Extend.SbRecords @@ -196,6 +198,59 @@ namespace NCC.Extend.SbRecords
196 } 198 }
197 199
198 /// <summary> 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 /// </summary> 255 /// </summary>
201 /// <param name="recordId">开始会话时返回的记录 Id。</param> 256 /// <param name="recordId">开始会话时返回的记录 Id。</param>
@@ -270,35 +325,60 @@ namespace NCC.Extend.SbRecords @@ -270,35 +325,60 @@ namespace NCC.Extend.SbRecords
270 .WhereIF(queryAddTime != null, p => p.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59)) 325 .WhereIF(queryAddTime != null, p => p.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59))
271 .WhereIF(!string.IsNullOrEmpty(input.addUser), p => p.AddUser.Equals(input.addUser)) 326 .WhereIF(!string.IsNullOrEmpty(input.addUser), p => p.AddUser.Equals(input.addUser))
272 .WhereIF(!string.IsNullOrEmpty(input.type), p => p.Type.Contains(input.type)) 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 .WhereIF(!string.IsNullOrEmpty(input.customerName), p => SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == p.ReId && k.Sskh != null && k.Sskh.Contains(input.customerName)).Any()); 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 var total = await baseQuery.CountAsync(); 332 var total = await baseQuery.CountAsync();
276 const int topN = 10; 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 var byTypeList = await baseQuery.GroupBy(p => p.Type ?? "") 340 var byTypeList = await baseQuery.GroupBy(p => p.Type ?? "")
278 .Select(g => new { Name = g.Type ?? "未分类", Value = SqlFunc.AggregateCount(g.Id) }) 341 .Select(g => new { Name = g.Type ?? "未分类", Value = SqlFunc.AggregateCount(g.Id) })
279 .ToListAsync(); 342 .ToListAsync();
280 var byType = byTypeList.OrderByDescending(x => x.Value).Select(x => new SbRecordsStatisticsItem { Name = x.Name ?? "未分类", Value = x.Value }).ToList(); 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 .ToListAsync(); 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 .ToListAsync(); 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 var byUser = byUserRaw.Select(x => new SbRecordsStatisticsItem 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 Value = x.Value 382 Value = x.Value
303 }).ToList(); 383 }).ToList();
304 var byCustomerRawList = await _db.Queryable<SbRecordsEntity, KhsbEntity>((r, k) => r.ReId == k.Id) 384 var byCustomerRawList = await _db.Queryable<SbRecordsEntity, KhsbEntity>((r, k) => r.ReId == k.Id)
@@ -317,8 +397,12 @@ namespace NCC.Extend.SbRecords @@ -317,8 +397,12 @@ namespace NCC.Extend.SbRecords
317 return new SbRecordsStatisticsOutput 397 return new SbRecordsStatisticsOutput
318 { 398 {
319 TotalCount = total, 399 TotalCount = total,
  400 + CompleteCount = completeCount,
  401 + TotalDurationSeconds = totalDuration,
  402 + AvgDurationSeconds = Math.Round(avgDuration, 2),
320 ByType = byType.Select(x => new SbRecordsStatisticsItem { Name = x.Name, Value = x.Value }).ToList(), 403 ByType = byType.Select(x => new SbRecordsStatisticsItem { Name = x.Name, Value = x.Value }).ToList(),
321 ByEquipment = byEquipment, 404 ByEquipment = byEquipment,
  405 + ByContent = byContent,
322 ByCustomer = byCustomer, 406 ByCustomer = byCustomer,
323 ByUser = byUser 407 ByUser = byUser
324 }; 408 };
@@ -375,6 +459,8 @@ namespace NCC.Extend.SbRecords @@ -375,6 +459,8 @@ namespace NCC.Extend.SbRecords
375 .WhereIF(queryAddTime != null, p => p.AddTime >= new DateTime(startAddTime.ToDate().Year, startAddTime.ToDate().Month, startAddTime.ToDate().Day, 0, 0, 0)) 459 .WhereIF(queryAddTime != null, p => p.AddTime >= new DateTime(startAddTime.ToDate().Year, startAddTime.ToDate().Month, startAddTime.ToDate().Day, 0, 0, 0))
376 .WhereIF(queryAddTime != null, p => p.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59)) 460 .WhereIF(queryAddTime != null, p => p.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59))
377 .WhereIF(!string.IsNullOrEmpty(input.addUser), p => p.AddUser.Equals(input.addUser)) 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 .WhereIF(!string.IsNullOrEmpty(input.customerName), p => SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == p.ReId && k.Sskh != null && k.Sskh.Contains(input.customerName)).Any()) 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 .WhereIF(!string.IsNullOrEmpty(input.item1), p => p.Item1.Contains(input.item1)) 465 .WhereIF(!string.IsNullOrEmpty(input.item1), p => p.Item1.Contains(input.item1))
380 .WhereIF(!string.IsNullOrEmpty(input.item2), p => p.Item2.Contains(input.item2)) 466 .WhereIF(!string.IsNullOrEmpty(input.item2), p => p.Item2.Contains(input.item2))
@@ -386,7 +472,7 @@ namespace NCC.Extend.SbRecords @@ -386,7 +472,7 @@ namespace NCC.Extend.SbRecords
386 contentName = it.ContentName, 472 contentName = it.ContentName,
387 deviceName = it.DeviceName, 473 deviceName = it.DeviceName,
388 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))), 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 addTime = it.AddTime, 476 addTime = it.AddTime,
391 addUser = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.AddUser).Select(u => u.RealName), 477 addUser = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.AddUser).Select(u => u.RealName),
392 enable = it.Enable, 478 enable = it.Enable,
@@ -429,6 +515,8 @@ namespace NCC.Extend.SbRecords @@ -429,6 +515,8 @@ namespace NCC.Extend.SbRecords
429 .WhereIF(queryAddTime != null, p => p.AddTime >= new DateTime(startAddTime.ToDate().Year, startAddTime.ToDate().Month, startAddTime.ToDate().Day, 0, 0, 0)) 515 .WhereIF(queryAddTime != null, p => p.AddTime >= new DateTime(startAddTime.ToDate().Year, startAddTime.ToDate().Month, startAddTime.ToDate().Day, 0, 0, 0))
430 .WhereIF(queryAddTime != null, p => p.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59)) 516 .WhereIF(queryAddTime != null, p => p.AddTime <= new DateTime(endAddTime.ToDate().Year, endAddTime.ToDate().Month, endAddTime.ToDate().Day, 23, 59, 59))
431 .WhereIF(!string.IsNullOrEmpty(input.addUser), p => p.AddUser.Equals(input.addUser)) 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 .WhereIF(!string.IsNullOrEmpty(input.customerName), p => SqlFunc.Subqueryable<KhsbEntity>().Where(k => k.Id == p.ReId && k.Sskh != null && k.Sskh.Contains(input.customerName)).Any()) 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 .WhereIF(!string.IsNullOrEmpty(input.item1), p => p.Item1.Contains(input.item1)) 521 .WhereIF(!string.IsNullOrEmpty(input.item1), p => p.Item1.Contains(input.item1))
434 .WhereIF(!string.IsNullOrEmpty(input.item2), p => p.Item2.Contains(input.item2)) 522 .WhereIF(!string.IsNullOrEmpty(input.item2), p => p.Item2.Contains(input.item2))
@@ -440,7 +528,7 @@ namespace NCC.Extend.SbRecords @@ -440,7 +528,7 @@ namespace NCC.Extend.SbRecords
440 contentName = it.ContentName, 528 contentName = it.ContentName,
441 deviceName = it.DeviceName, 529 deviceName = it.DeviceName,
442 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))), 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 addTime = it.AddTime, 532 addTime = it.AddTime,
445 addUser = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.AddUser).Select(u => u.RealName), 533 addUser = SqlFunc.Subqueryable<UserEntity>().Where(u => u.Id == it.AddUser).Select(u => u.RealName),
446 enable = it.Enable, 534 enable = it.Enable,
机具(管理端)/src/views/sbRecords/index.vue
1 <template> 1 <template>
2 - <div class="NCC-common-layout"> 2 + <div class="NCC-common-layout sb-records-page">
3 <div class="NCC-common-layout-center"> 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 </el-form-item> 18 </el-form-item>
75 </el-col> 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 </el-form> 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 </div> 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 </div> 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 </div> 167 </div>
111 </div> 168 </div>
112 <NCC-Form v-if="formVisible" ref="NCCForm" @refresh="refresh" /> 169 <NCC-Form v-if="formVisible" ref="NCCForm" @refresh="refresh" />
113 - <ExportBox v-if="exportBoxVisible" ref="ExportBox" @download="download" />  
114 </div> 170 </div>
115 </template> 171 </template>
116 172
117 <script> 173 <script>
118 import request from '@/utils/request' 174 import request from '@/utils/request'
119 import NCCForm from './Form' 175 import NCCForm from './Form'
120 -import ExportBox from './ExportBox'  
121 import echarts from 'echarts' 176 import echarts from 'echarts'
122 177
123 export default { 178 export default {
124 - components: { NCCForm, ExportBox }, 179 + components: { NCCForm },
125 data() { 180 data() {
126 return { 181 return {
127 showAll: false, 182 showAll: false,
@@ -130,11 +185,12 @@ export default { @@ -130,11 +185,12 @@ export default {
130 addTime: undefined, 185 addTime: undefined,
131 reId: undefined, 186 reId: undefined,
132 customerName: undefined, 187 customerName: undefined,
133 - type: undefined 188 + type: undefined,
  189 + deviceName: undefined,
  190 + contentName: undefined
134 }, 191 },
135 list: [], 192 list: [],
136 listLoading: true, 193 listLoading: true,
137 - multipleSelection: [],  
138 total: 0, 194 total: 0,
139 listQuery: { 195 listQuery: {
140 currentPage: 1, 196 currentPage: 1,
@@ -143,26 +199,19 @@ export default { @@ -143,26 +199,19 @@ export default {
143 sidx: '' 199 sidx: ''
144 }, 200 },
145 formVisible: false, 201 formVisible: false,
146 - exportBoxVisible: false, 202 + chartsExpanded: [], // 默认折叠;['charts'] 则展开统计图表
147 statistics: { 203 statistics: {
148 totalCount: 0, 204 totalCount: 0,
  205 + completeCount: 0,
  206 + totalDurationSeconds: 0,
  207 + avgDurationSeconds: 0,
149 byType: [], 208 byType: [],
150 byEquipment: [], 209 byEquipment: [],
  210 + byContent: [],
151 byCustomer: [], 211 byCustomer: [],
152 byUser: [] 212 byUser: []
153 }, 213 },
154 charts: {}, 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 reIdOptions: [] 215 reIdOptions: []
167 } 216 }
168 }, 217 },
@@ -193,13 +242,17 @@ export default { @@ -193,13 +242,17 @@ export default {
193 method: 'GET', 242 method: 'GET',
194 data: query 243 data: query
195 }).then(res => { 244 }).then(res => {
196 - const data = res.data || {} 245 + const d = res.data || {}
197 this.statistics = { 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 this.$nextTick(() => { 257 this.$nextTick(() => {
205 this.renderCharts() 258 this.renderCharts()
@@ -208,6 +261,9 @@ export default { @@ -208,6 +261,9 @@ export default {
208 this.$nextTick(() => this.renderCharts()) 261 this.$nextTick(() => this.renderCharts())
209 }) 262 })
210 }, 263 },
  264 + handleChartsExpand() {
  265 + this.$nextTick(() => this.renderCharts())
  266 + },
211 buildQuery() { 267 buildQuery() {
212 const _query = { ...this.listQuery, ...this.query } 268 const _query = { ...this.listQuery, ...this.query }
213 const query = {} 269 const query = {}
@@ -221,10 +277,23 @@ export default { @@ -221,10 +277,23 @@ export default {
221 return query 277 return query
222 }, 278 },
223 renderCharts() { 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 renderPie(refName, data, title) { 298 renderPie(refName, data, title) {
230 const el = this.$refs[refName] 299 const el = this.$refs[refName]
@@ -238,7 +307,7 @@ export default { @@ -238,7 +307,7 @@ export default {
238 this.charts[refName] = chart 307 this.charts[refName] = chart
239 const option = { 308 const option = {
240 tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, 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 chart.setOption(option) 312 chart.setOption(option)
244 }, 313 },
@@ -248,8 +317,8 @@ export default { @@ -248,8 +317,8 @@ export default {
248 if (this.charts[refName]) this.charts[refName].dispose() 317 if (this.charts[refName]) this.charts[refName].dispose()
249 const chart = echarts.init(el) 318 const chart = echarts.init(el)
250 this.charts[refName] = chart 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 const option = { 322 const option = {
254 tooltip: { trigger: 'axis' }, 323 tooltip: { trigger: 'axis' },
255 grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, 324 grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
@@ -285,53 +354,10 @@ export default { @@ -285,53 +354,10 @@ export default {
285 this.initData() 354 this.initData()
286 this.loadStatistics() 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 addOrUpdateHandle(id, isDetail) { 357 addOrUpdateHandle(id, isDetail) {
314 this.formVisible = true 358 this.formVisible = true
315 this.$nextTick(() => this.$refs.NCCForm.init(id, isDetail)) 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 refresh(isrRefresh) { 361 refresh(isrRefresh) {
336 this.formVisible = false 362 this.formVisible = false
337 if (isrRefresh) { 363 if (isrRefresh) {
@@ -344,26 +370,141 @@ export default { @@ -344,26 +370,141 @@ export default {
344 </script> 370 </script>
345 371
346 <style lang="scss" scoped> 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 margin-bottom: 16px; 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 </style> 510 </style>