feat: 增加设计任务工时报表

This commit is contained in:
2026-07-15 18:19:25 +08:00
parent 4f42c3a030
commit 9d08fb1d1a
3 changed files with 334 additions and 50 deletions

View File

@@ -0,0 +1,135 @@
USE [YL_MESDB];
GO
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE [dbo].[_工时报表_任务工时_查询]
@ nvarchar(30) = N'',
@ nvarchar(30) = N'',
@ nvarchar(100) = N'',
@ nvarchar(100) = N'',
@ nvarchar(100) = N'',
@ nvarchar(100) = N'',
@DID int = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ datetime = TRY_CONVERT(datetime, NULLIF(@, N''));
DECLARE @ datetime = TRY_CONVERT(datetime, NULLIF(@, N''));
;WITH base AS
(
SELECT
r.[RID],
r.[DID],
ISNULL(r.[], N'') AS [],
t.[],
t.[],
t.[],
t.[],
t.[],
t.[],
r.[],
r.[],
ISNULL(r.[], 0) AS [],
r.[],
r.[],
r.[]
FROM [dbo].[MES_设计报工_记录] AS r
INNER JOIN [dbo].[MES_设计报工_任务] AS t ON r.[DID] = t.[DID]
WHERE
r.[] IS NOT NULL
AND r.[] IS NOT NULL
AND (@ IS NULL OR r.[] >= @)
AND (@ IS NULL OR r.[] < DATEADD(day, 1, @))
AND (ISNULL(@, N'') = N'' OR r.[] LIKE N'%' + @ + N'%')
AND (ISNULL(@, N'') = N'' OR t.[] LIKE N'%' + @ + N'%')
AND (ISNULL(@, N'') = N'' OR t.[] LIKE N'%' + @ + N'%')
AND (ISNULL(@, N'') = N'' OR t.[] LIKE N'%' + @ + N'%')
AND (@DID IS NULL OR @DID = 0 OR r.[DID] = @DID)
),
result AS
(
SELECT
1 AS [Level],
N't-' + CAST([DID] AS nvarchar(20)) AS [id],
CAST(N'' AS nvarchar(120)) AS [parentId],
CAST(N'' AS nvarchar(100)) AS [],
CAST(N'' AS nvarchar(20)) AS [],
CAST(NULL AS int) AS [RID],
[DID],
ISNULL(MAX([]), N'') AS [],
ISNULL(MAX([]), N'') AS [],
ISNULL(MAX([]), N'') AS [],
ISNULL(MAX([]), N'') AS [],
ISNULL(MAX([]), N'') AS [],
ISNULL(MAX([]), N'') AS [],
CONVERT(varchar(19), MIN([]), 120) AS [],
CONVERT(varchar(19), MAX([]), 120) AS [],
COUNT(1) AS [],
CAST(SUM([]) / 3600.0 AS decimal(18, 2)) AS [],
CASE
WHEN SUM(CONVERT(int, [])) = COUNT(1) THEN N'已校验'
WHEN SUM(CONVERT(int, [])) = 0 THEN N'未校验'
ELSE N'部分校验'
END AS [],
ISNULL(STRING_AGG(
CASE WHEN NULLIF(LTRIM(RTRIM([])), N'') IS NOT NULL
THEN CONVERT(nvarchar(max), CONVERT(nvarchar(19), [], 120) + NCHAR(13) + NCHAR(10) + []) END,
NCHAR(13) + NCHAR(10) + NCHAR(13) + NCHAR(10)
) WITHIN GROUP (ORDER BY [], [RID]), N'') AS [],
ISNULL(STRING_AGG(
CASE WHEN NULLIF(LTRIM(RTRIM([])), N'') IS NOT NULL
THEN CONVERT(nvarchar(max), []) END,
NCHAR(13) + NCHAR(10)
) WITHIN GROUP (ORDER BY [], [RID]), N'') AS []
FROM base
GROUP BY [DID]
UNION ALL
SELECT
2 AS [Level],
N't-' + CAST([DID] AS nvarchar(20)) + N'-o-' + [] AS [id],
N't-' + CAST([DID] AS nvarchar(20)) AS [parentId],
[],
CAST(N'' AS nvarchar(20)) AS [],
CAST(NULL AS int) AS [RID],
[DID],
CAST(N'' AS nvarchar(100)) AS [],
CAST(N'' AS nvarchar(100)) AS [],
CAST(N'' AS nvarchar(200)) AS [],
CAST(N'' AS nvarchar(100)) AS [],
CAST(N'' AS nvarchar(100)) AS [],
CAST(N'' AS nvarchar(500)) AS [],
CONVERT(varchar(19), MIN([]), 120) AS [],
CONVERT(varchar(19), MAX([]), 120) AS [],
COUNT(1) AS [],
CAST(SUM([]) / 3600.0 AS decimal(18, 2)) AS [],
CASE
WHEN SUM(CONVERT(int, [])) = COUNT(1) THEN N'已校验'
WHEN SUM(CONVERT(int, [])) = 0 THEN N'未校验'
ELSE N'部分校验'
END AS [],
ISNULL(STRING_AGG(
CASE WHEN NULLIF(LTRIM(RTRIM([])), N'') IS NOT NULL
THEN CONVERT(nvarchar(max), CONVERT(nvarchar(19), [], 120) + NCHAR(13) + NCHAR(10) + []) END,
NCHAR(13) + NCHAR(10) + NCHAR(13) + NCHAR(10)
) WITHIN GROUP (ORDER BY [], [RID]), N'') AS [],
ISNULL(STRING_AGG(
CASE WHEN NULLIF(LTRIM(RTRIM([])), N'') IS NOT NULL
THEN CONVERT(nvarchar(max), []) END,
NCHAR(13) + NCHAR(10)
) WITHIN GROUP (ORDER BY [], [RID]), N'') AS []
FROM base
GROUP BY [DID], []
)
SELECT *
FROM result
ORDER BY [DID] DESC, [Level], [];
END
GO

View File

@@ -3436,6 +3436,68 @@ pm run build通过仅有既有警告。
- 数据库明文密码没有进入提交内容。
- 本条上传过程日志将通过后续独立提交同步到远程,确保工作区最终无未提交修改。
### 验证结果
- 上传前本地与远程分叉为 `0 0`
- 全工作区和暂存区 `git diff --check` 通过。
- 暂存区凭据特征扫描无命中。
- 两个设计报工页面 ESLint 通过。
- 最终生产构建通过。
- 功能提交已成功推送至远程。
## 2026-07-15 设计工时报表增加任务工时页签
### 用户提问
- 用户指定修改 `src/views/ProductionManagement/DesignReportTask/Report/index.vue`,说明当前页面为按日期和人员计算的工时报表。
- 要求在同一页面新增一个按 DID 计算的工时报表,顶部页签切换方式参考 `ProductionManagement/Timesheet/index`
- 原报表命名为“作业者工时”,新增报表命名为“任务工时”。
### 执行过程
1. 检查用户指定的 `Report/index.vue`、设计报工模块全部页面、菜单脚本、项目文档及参考页面 `ProductionManagement/Timesheet/index.vue`
2. 确认用户描述的按日期和人员计算报表实际位于 `src/views/ProductionManagement/DesignReportTask/HoursReport/index.vue`,菜单名为“设计工时报表”;指定的 `Report/index.vue` 实际是设计报工开始、结束、完成操作页。
3. 根据菜单路由和业务意图,选择修改正确的 `HoursReport/index.vue`,避免把报工操作页面误改成工时报表。
4. 阅读参考 Timesheet 页面“作业者工时”和“任务工时”页签、查询切换、树形数据组装及导出逻辑。
5. 检查现有设计作业者工时查询过程 `设计报工_工时报表_作业者工时_查询`,确认现有结构为操作人→工作日期→报工明细三级树。
6. 查询目标数据库现有设计报工记录和现有报表返回结果,确认筛选参数、字段类型、工时秒数、校验状态、进度说明及备注的数据口径。
7. 修改 `HoursReport/index.vue`
- 增加 Element UI 顶部页签,名称为“作业者工时”和“任务工时”。
- 默认显示“作业者工时”,保留原有人员→日期→明细三级树和全部字段。
- 新增“任务工时”表格,使用 DID→操作人两级树。
- DID 根节点显示任务描述、项目号、物料、图号、类别、报工次数、总工时、最早开始、最晚结束和综合校验状态。
- 操作人子节点显示该人员在当前 DID 上的报工次数、工时、起止时间、校验状态、进度说明和备注。
- 两个页签共用开始日期、结束日期、操作人、项目号、物料编码、类别和 DID 筛选条件。
- 点击页签时查询对应报表过程。
- 底部总工时根据当前页签根节点动态计算。
- 导出按钮根据当前页签导出对应树形数据,工作表和文件名分别使用“作业者工时”或“任务工时”。
8. 新增数据库脚本 `db_backups/add_design_report_task_hours_report_20260715.sql`,创建过程 `设计报工_工时报表_任务工时_查询`
9. 新过程沿用原作业者工时的全部筛选条件,只统计已正常结束的设计报工记录,并按 DID 汇总根节点、按 DID 和操作人汇总子节点。
10. 新过程对每个汇总层计算报工次数、工时小时、最早开始时间、最晚结束时间;校验状态区分“已校验”“未校验”“部分校验”;进度说明按结束时间和内容聚合,备注同步聚合。
11. 执行前端单文件 ESLint 和目标差异空白检查,均通过;仅输出项目依赖的 Browserslist 和 Baseline 数据过期提示。
12. 在独立 LocalDB 测试实例中创建最小任务和报工表,执行新过程脚本并插入两个人员共同报工同一 DID 的测试数据。
13. 隔离验证结果DID 10 根节点汇总 2 次、1.50 小时、状态“部分校验”;张三子节点 1 次、1.00 小时、已校验;李四子节点 1 次、0.50 小时、未校验;任务及人员的进度说明和备注聚合正确。
14. 删除 LocalDB 测试夹具、测试实例及 MDF/LDF 文件,确认没有临时测试资源残留。
15. 执行 `npm run build`,生产构建成功;仅有项目原有的大资源体积和浏览器兼容数据过期警告,未生成受版本控制的 `dist` 变更。
16. 使用用户此前提供的数据库凭据,将 `add_design_report_task_hours_report_20260715.sql` 发布到 `192.168.2.92 / YL_MESDB`;日志不记录明文密码。
17. 在目标库创建新过程成功,修改时间为 `2026-07-15 18:14:19`
18. 使用临时表只读接收目标库作业者工时和任务工时两个过程的完整返回结果,交叉核对真实数据:
- 两个报表统计的已结束报工次数均为 30。
- 任务工时根节点与人员子节点结构正确。
- 多人任务 DID 1 汇总为 3 次、0.02 小时;子节点为超级管理员 2 次、张一帆 1 次。
- 两种报表按不同维度分别先保留两位小数,跨全部分组求和存在 0.01 至 0.02 小时累计舍入差,属于分组舍入口径差异,不影响单个 DID 汇总。
19. 最终再次执行 HoursReport 页面 ESLint、目标文件 `git diff --check`、数据库对象检查和本地服务检查,均通过;`https://localhost:1997/` 返回 HTTP 200。
### 修改文件
- `src/views/ProductionManagement/DesignReportTask/HoursReport/index.vue`
- `db_backups/add_design_report_task_hours_report_20260715.sql`
- `gptlog-process/gpdlog.md`
### 结论
- 设计工时报表现在同一页面提供“作业者工时”和“任务工时”两个顶部页签。
- 原作业者工时功能、筛选条件和三级树结构保持不变。
- 新任务工时按照 DID 汇总,并可展开查看每位操作人的工时贡献。
- 查询、总工时和 Excel 导出都会随当前页签切换。
- 新任务工时查询过程已经发布到目标数据库并通过真实数据验证。
### 验证结果
- HoursReport 单文件 ESLint 通过。
- 目标文件 `git diff --check` 通过。
- 隔离 LocalDB 的 DID 及人员汇总验证通过。

View File

@@ -64,7 +64,7 @@
<el-input v-model="searchForm.did" clearable size="small" style="width: 90px; margin-right: 8px" />
<el-button :loading="loading" icon="el-icon-search" plain size="small" type="primary" @click="searchTable">查询</el-button>
<el-button
:disabled="loading || tableData.length === 0"
:disabled="loading || currentTableData.length === 0"
:loading="exporting"
icon="el-icon-download"
plain
@@ -74,42 +74,84 @@
>导出</el-button>
</div>
<el-table
v-loading="loading"
:data="tableData"
border
default-expand-all
height="74vh"
row-key="id"
:tree-props="{ children: 'children' }"
size="mini"
style="width: 100%; margin-top: 12px"
>
<el-table-column label="操作人 / 日期 / 任务" min-width="260" show-overflow-tooltip>
<template slot-scope="scope">
<span :class="{ 'bold-text': scope.row.Level <= 2 }">{{ getTreeTitle(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="工作日期" prop="工作日期" width="110" />
<el-table-column align="center" label="RID" prop="RID" width="70" />
<el-table-column align="center" label="DID" prop="DID" width="70" />
<el-table-column align="left" label="项目号" prop="项目号" width="120" show-overflow-tooltip />
<el-table-column align="left" label="物料编码" prop="物料编码" width="170" show-overflow-tooltip />
<el-table-column align="left" label="物料名称" prop="物料名称" width="180" show-overflow-tooltip />
<el-table-column align="left" label="图号" prop="图号" width="120" show-overflow-tooltip />
<el-table-column align="left" label="类别" prop="类别" width="120" show-overflow-tooltip />
<el-table-column align="center" label="报工次数" prop="报工次数" width="90" />
<el-table-column align="right" label="工时(h)" prop="工时小时" width="90">
<template slot-scope="scope">
<span :class="{ 'bold-text': scope.row.Level <= 2 }">{{ formatHours(scope.row.工时小时) }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="开始时间" prop="开始时间" width="150" />
<el-table-column align="center" label="结束时间" prop="结束时间" width="150" />
<el-table-column align="center" label="校验状态" prop="校验状态" width="90" />
<el-table-column align="left" label="进度说明" prop="进度说明" min-width="160" show-overflow-tooltip />
<el-table-column align="left" label="备注" prop="备注" min-width="160" show-overflow-tooltip />
</el-table>
<el-tabs v-model="activeTab" class="report-tabs" @tab-click="handleTabClick">
<el-tab-pane label="作业者工时" name="operator">
<el-table
v-loading="loading"
:data="operatorTableData"
border
default-expand-all
height="70vh"
row-key="id"
:tree-props="{ children: 'children' }"
size="mini"
style="width: 100%"
>
<el-table-column label="操作人 / 日期 / 任务" min-width="260" show-overflow-tooltip>
<template slot-scope="scope">
<span :class="{ 'bold-text': scope.row.Level <= 2 }">{{ getOperatorTreeTitle(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="工作日期" prop="工作日期" width="110" />
<el-table-column align="center" label="RID" prop="RID" width="70" />
<el-table-column align="center" label="DID" prop="DID" width="70" />
<el-table-column align="left" label="项目号" prop="项目号" width="120" show-overflow-tooltip />
<el-table-column align="left" label="物料编码" prop="物料编码" width="170" show-overflow-tooltip />
<el-table-column align="left" label="物料名称" prop="物料名称" width="180" show-overflow-tooltip />
<el-table-column align="left" label="图号" prop="图号" width="120" show-overflow-tooltip />
<el-table-column align="left" label="类别" prop="类别" width="120" show-overflow-tooltip />
<el-table-column align="center" label="报工次数" prop="报工次数" width="90" />
<el-table-column align="right" label="工时(h)" prop="工时小时" width="90">
<template slot-scope="scope">
<span :class="{ 'bold-text': scope.row.Level <= 2 }">{{ formatHours(scope.row.工时小时) }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="开始时间" prop="开始时间" width="150" />
<el-table-column align="center" label="结束时间" prop="结束时间" width="150" />
<el-table-column align="center" label="校验状态" prop="校验状态" width="90" />
<el-table-column align="left" label="进度说明" prop="进度说明" min-width="160" show-overflow-tooltip />
<el-table-column align="left" label="备注" prop="备注" min-width="160" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<el-tab-pane label="任务工时" name="task">
<el-table
v-loading="loading"
:data="taskTableData"
border
default-expand-all
height="70vh"
row-key="id"
:tree-props="{ children: 'children' }"
size="mini"
style="width: 100%"
>
<el-table-column label="任务 / 操作人" min-width="260" show-overflow-tooltip>
<template slot-scope="scope">
<span :class="{ 'bold-text': scope.row.Level === 1 }">{{ getTaskTreeTitle(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="DID" prop="DID" width="70" />
<el-table-column align="left" label="项目号" prop="项目号" width="120" show-overflow-tooltip />
<el-table-column align="left" label="物料编码" prop="物料编码" width="170" show-overflow-tooltip />
<el-table-column align="left" label="物料名称" prop="物料名称" width="180" show-overflow-tooltip />
<el-table-column align="left" label="图号" prop="图号" width="120" show-overflow-tooltip />
<el-table-column align="left" label="类别" prop="类别" width="120" show-overflow-tooltip />
<el-table-column align="left" label="任务描述" prop="任务描述" width="220" show-overflow-tooltip />
<el-table-column align="center" label="报工次数" prop="报工次数" width="90" />
<el-table-column align="right" label="工时(h)" prop="工时小时" width="90">
<template slot-scope="scope">
<span :class="{ 'bold-text': scope.row.Level === 1 }">{{ formatHours(scope.row.工时小时) }}</span>
</template>
</el-table-column>
<el-table-column align="center" label="开始时间" prop="开始时间" width="150" />
<el-table-column align="center" label="结束时间" prop="结束时间" width="150" />
<el-table-column align="center" label="校验状态" prop="校验状态" width="90" />
<el-table-column align="left" label="进度说明" prop="进度说明" min-width="180" show-overflow-tooltip />
<el-table-column align="left" label="备注" prop="备注" min-width="160" show-overflow-tooltip />
</el-table>
</el-tab-pane>
</el-tabs>
<div class="summary-footer">
<span>总工时<strong>{{ totalHours }}</strong> 小时</span>
@@ -127,7 +169,9 @@ export default {
return {
loading: false,
exporting: false,
tableData: [],
activeTab: 'operator',
operatorTableData: [],
taskTableData: [],
projectOptions: [],
materialOptions: [],
searchForm: {
@@ -142,8 +186,11 @@ export default {
}
},
computed: {
currentTableData() {
return this.activeTab === 'task' ? this.taskTableData : this.operatorTableData
},
totalHours() {
const total = this.tableData.reduce((sum, row) => sum + (Number(row.工时小时) || 0), 0)
const total = this.currentTableData.reduce((sum, row) => sum + (Number(row.工时小时) || 0), 0)
return total.toFixed(2)
}
},
@@ -177,29 +224,38 @@ export default {
},
async searchTable() {
this.loading = true
const data = this.CreateData('11', '设计报工_工时报表_作业者工时_查询', this.buildSearchParams())
const isTaskReport = this.activeTab === 'task'
const procedure = isTaskReport ? '设计报工_工时报表_任务工时_查询' : '设计报工_工时报表_作业者工时_查询'
const data = this.CreateData('11', procedure, this.buildSearchParams())
try {
const response = await this.ExecDatabase(data)
this.tableData = this.buildTree(response.data || [])
if (isTaskReport) {
this.taskTableData = this.buildTaskTree(response.data || [])
} else {
this.operatorTableData = this.buildOperatorTree(response.data || [])
}
} catch (error) {
console.error('设计工时报表查询失败:', error)
console.error(`${isTaskReport ? '任务工时' : '作业者工时'}查询失败:`, error)
this.$message.error('查询失败')
} finally {
this.loading = false
}
},
exportToExcel() {
if (this.tableData.length === 0) {
if (this.currentTableData.length === 0) {
this.$message.warning('没有数据可导出')
return
}
this.exporting = true
try {
const rows = this.flattenTree(this.tableData)
const rows = this.flattenTree(this.currentTableData)
const isTaskReport = this.activeTab === 'task'
const reportName = isTaskReport ? '任务工时' : '作业者工时'
const exportData = rows.map(row => ({
层级: row.Level === 1 ? '操作人汇总' : (row.Level === 2 ? '日期汇总' : '任务明细'),
'操作人 / 日期 / 任务': `${' '.repeat(Math.max((row.Level || 1) - 1, 0))}${this.getTreeTitle(row)}`,
层级: this.getExportLevel(row, isTaskReport),
[isTaskReport ? '任务 / 操作人' : '操作人 / 日期 / 任务']:
`${' '.repeat(Math.max((row.Level || 1) - 1, 0))}${isTaskReport ? this.getTaskTreeTitle(row) : this.getOperatorTreeTitle(row)}`,
工作日期: this.getExportValue(row.工作日期),
RID: this.getExportValue(row.RID),
DID: this.getExportValue(row.DID),
@@ -237,8 +293,8 @@ export default {
{ wch: 30 },
{ wch: 30 }
]
XLSX.utils.book_append_sheet(workbook, worksheet, '设计工时报表')
XLSX.writeFile(workbook, `设计工时报表_${this.formatDateForFileName(new Date())}.xlsx`)
XLSX.utils.book_append_sheet(workbook, worksheet, reportName)
XLSX.writeFile(workbook, `设计${reportName}_${this.formatDateForFileName(new Date())}.xlsx`)
this.$message.success('导出成功')
} catch (error) {
console.error('设计工时报表导出失败:', error)
@@ -259,6 +315,10 @@ export default {
getExportValue(value) {
return value === null || value === undefined ? '' : value
},
getExportLevel(row, isTaskReport) {
if (isTaskReport) return row.Level === 1 ? '任务汇总' : '操作人汇总'
return row.Level === 1 ? '操作人汇总' : (row.Level === 2 ? '日期汇总' : '任务明细')
},
formatDateForFileName(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
@@ -286,7 +346,7 @@ export default {
this.materialOptions = []
}
},
buildTree(rows) {
buildOperatorTree(rows) {
const personMap = new Map()
const dateMap = new Map()
@@ -314,11 +374,34 @@ export default {
return Array.from(personMap.values())
},
getTreeTitle(row) {
buildTaskTree(rows) {
const taskMap = new Map()
rows.forEach(row => {
if (row.Level === 1) {
taskMap.set(row.id, { ...row, children: [] })
}
})
rows.forEach(row => {
if (row.Level === 2) {
const parent = taskMap.get(row.parentId)
if (parent) parent.children.push({ ...row })
}
})
return Array.from(taskMap.values())
},
getOperatorTreeTitle(row) {
if (row.Level === 1) return row.操作人 || '未填写操作人'
if (row.Level === 2) return row.工作日期 ? `日期 ${row.工作日期}` : '未填写日期'
return row.类别 ? `${row.类别} | ${row.任务描述 || row.DID || ''}` : (row.任务描述 || `${row.DID || ''}`)
},
getTaskTreeTitle(row) {
if (row.Level === 2) return row.操作人 || '未填写操作人'
const description = row.任务描述 || ''
return description ? `DID ${row.DID} | ${description}` : `DID ${row.DID}`
},
handleTabClick() {
this.searchTable()
},
formatHours(value) {
if (value === null || value === undefined || value === '') return ''
const numberValue = Number(value)
@@ -352,6 +435,10 @@ export default {
font-weight: 600;
}
.report-tabs {
margin-top: 10px;
}
.summary-footer {
height: 36px;
line-height: 36px;