feat 联调历史报告表

This commit is contained in:
Ironsp
2026-09-17 11:12:53 +08:00
parent 0399e8ede4
commit 449cf1f6c7
9 changed files with 266 additions and 11 deletions

View File

@@ -261,6 +261,7 @@ declare global {
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useProjectLifecycle: typeof import('./composables/useProjectLifecycle')['useProjectLifecycle']
const useQuery: typeof import('./composables/useQuery')['useQuery']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
@@ -609,6 +610,7 @@ declare module 'vue' {
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useProjectLifecycle: UnwrapRef<typeof import('./composables/useProjectLifecycle')['useProjectLifecycle']>
readonly useQuery: UnwrapRef<typeof import('./composables/useQuery')['useQuery']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>

View File

@@ -1,10 +1,13 @@
<script setup lang="ts">
import type { Project } from '@/service/project'
import { useProjectLifecycle } from '@/composables/useProjectLifecycle'
const props = defineProps<{
item: Project
}>()
const { startProject, pauseProject, endProject } = useProjectLifecycle()
const statusMap: Record<number, string> = {
0: '已暂停',
1: '已结束',
@@ -30,6 +33,21 @@ function formatArea(value?: number) {
const text = Number.isInteger(area) ? String(area) : String(area)
return `${text}m²`
}
function onStart() {
if (props.item.id)
startProject(props.item.id)
}
function onPause() {
if (props.item.id)
pauseProject(props.item.id)
}
function onEnd() {
if (props.item.id)
endProject(props.item.id)
}
</script>
<template>
@@ -74,14 +92,14 @@ function formatArea(value?: number) {
</text>
</view>
</view>
<view v-if="item.status !== 1" class="project-actions">
<wd-button size="large" round>
<view v-if="item.status !== 1" class="project-actions" @tap.stop>
<wd-button size="large" round @tap.stop="onStart">
开始项目
</wd-button>
<wd-button type="warning" size="large" round>
<wd-button type="warning" size="large" round @tap.stop="onPause">
暂停项目
</wd-button>
<wd-button type="danger" size="large" round>
<wd-button type="danger" size="large" round @tap.stop="onEnd">
结束项目
</wd-button>
</view>

View File

@@ -1,5 +1,8 @@
<script setup lang="ts">
defineProps<{
import { useProjectLifecycle } from '@/composables/useProjectLifecycle'
const props = defineProps<{
id: number
projectName: string
stakeRange: string
status: number
@@ -8,6 +11,8 @@ defineProps<{
rollerCount: string
}>()
const { startProject, pauseProject, endProject } = useProjectLifecycle()
function getStatusText(status: number) {
const map: Record<number, string> = {
0: '已暂停',
@@ -25,6 +30,21 @@ function getStatusColor(status: number) {
}
return map[status] || '#999999'
}
function onStart() {
if (props.id)
startProject(props.id)
}
function onPause() {
if (props.id)
pauseProject(props.id)
}
function onEnd() {
if (props.id)
endProject(props.id)
}
</script>
<template>
@@ -80,13 +100,13 @@ function getStatusColor(status: number) {
<!-- 操作按钮 -->
<view v-if="status !== 1" class="btn-row">
<wd-button size="large" type="primary" round>
<wd-button size="large" type="primary" round @tap="onStart">
开始项目
</wd-button>
<wd-button size="large" type="warning" round>
<wd-button size="large" type="warning" round @tap="onPause">
暂停项目
</wd-button>
<wd-button size="large" type="danger" round>
<wd-button size="large" type="danger" round @tap="onEnd">
结束项目
</wd-button>
</view>

View File

@@ -0,0 +1,196 @@
import type { Project } from '@/service/project'
import { useCurrentProject } from '@/composables/useCurrentProject'
import { historyReportService, projectService, workRecordService } from '@/service'
import { useDbStore } from '@/store/modules/db'
import storage from '@/utils/storage'
/** 本地时间字符串,格式与 seed 一致:YYYY-MM-DD HH:mm:ss */
function nowDateTime() {
return new Date().toISOString().replace('T', ' ').slice(0, 19)
}
/** 作业时长(小时),保留一位小数 */
function calcDurationHours(start?: string, end?: string) {
if (!start || !end)
return 0
const startMs = new Date(start.replace(/-/g, '/')).getTime()
const endMs = new Date(end.replace(/-/g, '/')).getTime()
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs)
return 0
return Math.round((endMs - startMs) / (1000 * 60 * 60) * 10) / 10
}
async function findOpenWorkRecord(projectId: number) {
const records = await workRecordService.selectByProjectId(projectId)
return records.find(item => item.id != null && !item.end_time) ?? null
}
/**
* 项目开始 / 暂停 / 结束:
* - 开始:status=2,新增工作记录(project_id + start_time)
* - 暂停:status=0,补全当前工作记录 end_time 与 duration
* - 结束:status=1,关闭未结束工作记录,写入历史报告
*/
export function useProjectLifecycle() {
const db = useDbStore()
const { currentProjectId, setCurrentProject } = useCurrentProject()
let busy = false
async function refreshAfterChange(projectId: number) {
await db.loadAll()
if (currentProjectId.value === projectId) {
const latest = await projectService.selectById(projectId)
if (latest && latest.status !== 1)
setCurrentProject(latest)
else if (!latest || latest.status === 1)
setCurrentProject(null)
}
}
async function withBusy(fn: () => Promise<void>) {
if (busy)
return
busy = true
try {
await fn()
}
finally {
busy = false
}
}
/** 开始项目 → 作业中 */
async function startProject(projectId: number) {
await withBusy(async () => {
const project = await projectService.selectById(projectId)
if (!project) {
uni.showToast({ title: '项目不存在', icon: 'none' })
return
}
if (project.status === 1) {
uni.showToast({ title: '项目已结束', icon: 'none' })
return
}
if (project.status === 2) {
uni.showToast({ title: '项目已在作业中', icon: 'none' })
return
}
const open = await findOpenWorkRecord(projectId)
if (open) {
uni.showToast({ title: '存在未结束的作业记录', icon: 'none' })
return
}
const startTime = nowDateTime()
await workRecordService.insert({
project_id: projectId,
start_time: startTime,
})
const payload: Partial<Project> = { status: 2 }
if (!project.start_time)
payload.start_time = startTime
await projectService.update(projectId, payload)
setCurrentProject({ ...project, ...payload })
await refreshAfterChange(projectId)
uni.showToast({ title: '已开始项目', icon: 'success' })
})
}
/** 暂停项目 → 已暂停,并结束当前工作记录 */
async function pauseProject(projectId: number) {
await withBusy(async () => {
const project = await projectService.selectById(projectId)
if (!project) {
uni.showToast({ title: '项目不存在', icon: 'none' })
return
}
if (project.status !== 2) {
uni.showToast({ title: '仅作业中可暂停', icon: 'none' })
return
}
const endTime = nowDateTime()
const open = await findOpenWorkRecord(projectId)
if (open?.id) {
await workRecordService.update(open.id, {
end_time: endTime,
duration: calcDurationHours(open.start_time, endTime),
})
}
await projectService.update(projectId, { status: 0 })
await refreshAfterChange(projectId)
uni.showToast({ title: '已暂停项目', icon: 'success' })
})
}
/** 结束项目 → 已结束,生成历史报告 */
async function endProject(projectId: number) {
await withBusy(async () => {
const project = await projectService.selectById(projectId)
if (!project) {
uni.showToast({ title: '项目不存在', icon: 'none' })
return
}
if (project.status === 1) {
uni.showToast({ title: '项目已结束', icon: 'none' })
return
}
const endTime = nowDateTime()
const open = await findOpenWorkRecord(projectId)
if (open?.id) {
await workRecordService.update(open.id, {
end_time: endTime,
duration: calcDurationHours(open.start_time, endTime),
})
}
const latest = await projectService.selectById(projectId)
if (!latest) {
uni.showToast({ title: '项目不存在', icon: 'none' })
return
}
const workRecords = await workRecordService.selectByProjectId(projectId)
const exceptionCount = workRecords.reduce((sum, item) => sum + (item.exception_count ?? 0), 0)
await historyReportService.insert({
project_id: projectId,
report_name: `${latest.project_name} 碾压报告`,
device_id: storage.get<string>('device_unique_id') || undefined,
construction_area: latest.stake_range,
start_time: latest.start_time,
end_time: endTime,
duration: latest.total_duration ?? 0,
compaction_area: latest.total_compaction_area ?? 0,
pass_count: latest.total_pass_count ?? 0,
qualified_rate: latest.qualified_rate ?? 0,
avg_speed: latest.avg_speed ?? 0,
avg_paving_temp: latest.avg_paving_temp ?? 0,
avg_compaction: latest.avg_compaction ?? 0,
exception_count: exceptionCount,
})
await projectService.update(projectId, {
status: 1,
end_time: endTime,
})
if (currentProjectId.value === projectId)
setCurrentProject(null)
await refreshAfterChange(projectId)
uni.showToast({ title: '已结束并生成报告', icon: 'success' })
})
}
return {
startProject,
pauseProject,
endProject,
}
}

View File

@@ -16,7 +16,7 @@
"navigationBarTitleText": "Road-Roller"
},
"pages": [
// GENERATED BY UNI-PAGES, PLATFORM: APP
// GENERATED BY UNI-PAGES, PLATFORM: H5 || APP
{
"path": "pages/Ahome/index",
"type": "home",
@@ -121,7 +121,7 @@
"color": "@tabFontColor",
"selectedColor": "@tabSelectedColor",
"list": [
// GENERATED BY UNI-PAGES, PLATFORM: APP
// GENERATED BY UNI-PAGES, PLATFORM: H5 || APP
{
"pagePath": "pages/Ahome/index",
"text": "首页",

View File

@@ -109,6 +109,7 @@ onMounted(async () => {
</view>
<project-item
v-for="item in projects"
:id="item.id"
:key="item.id"
:project-name="item.projectName"
:stake-range="item.stakeRange"

View File

@@ -28,7 +28,7 @@ const tableSqls = [
/** 当前代码期望的各表字段(用于检测旧库结构) */
const expectedColumns: Record<string, string[]> = {
project: ['id', 'project_name', 'stake_range', 'total_area', 'start_time', 'end_time', 'total_duration', 'total_compaction_area', 'total_pass_count', 'qualified_rate', 'progress', 'uncompacted_area', 'status', 'created_at', 'updated_at'],
project: ['id', 'project_name', 'stake_range', 'total_area', 'start_time', 'end_time', 'total_duration', 'total_compaction_area', 'total_pass_count', 'qualified_rate', 'progress', 'uncompacted_area', 'avg_speed', 'avg_paving_temp', 'avg_compaction', 'status', 'created_at', 'updated_at'],
work_record: ['id', 'project_id', 'start_time', 'end_time', 'duration', 'compaction_area', 'pass_count', 'exception_count', 'created_at'],
rtk_track: ['id', 'work_record_id', 'latitude', 'longitude', 'speed', 'heading', 'steel_wheel_temp', 'compaction_value', 'pass_count', 'qualified_rate', 'created_at'],
warning_record: ['id', 'project_id', 'device_name', 'exception_type', 'exception_level', 'occurred_at', 'site_description', 'disposal_record', 'disposal_type', 'is_read', 'created_at'],

View File

@@ -15,6 +15,9 @@ export const projectTableSql = `CREATE TABLE IF NOT EXISTS project (
qualified_rate REAL DEFAULT 0,
progress REAL DEFAULT 0,
uncompacted_area REAL DEFAULT 0,
avg_speed REAL DEFAULT 0,
avg_paving_temp REAL DEFAULT 0,
avg_compaction REAL DEFAULT 0,
status INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
@@ -33,6 +36,9 @@ export interface Project {
qualified_rate?: number // 压实合格率,默认 0
progress?: number // 完成进度,默认 0
uncompacted_area?: number // 未压实面积 = 总面积 - 压实面积,默认 0
avg_speed?: number // 平均行驶速度,默认 0
avg_paving_temp?: number // 平均温度,默认 0
avg_compaction?: number // 平均压实度,默认 0
status: 0 | 1 | 2 // 项目状态,0:已暂停,1:已结束,2:作业中,默认 0
created_at?: string // 创建时间,默认当前时间
updated_at?: string // 更新时间,默认当前时间

View File

@@ -26,24 +26,36 @@ async function seedProject() {
project_name: '白沟产业园场地',
stake_range: '1号地块',
total_area: 5200.0,
avg_speed: 5.1,
avg_paving_temp: 148.0,
avg_compaction: 93.2,
status: 1,
},
{
project_name: '大兴机场配套道路',
stake_range: '北侧路段',
total_area: 8800.0,
avg_speed: 4.9,
avg_paving_temp: 152.0,
avg_compaction: 94.1,
status: 1,
},
{
project_name: '保定城区改造工程',
stake_range: '朝阳路 ~ 裕华路',
total_area: 12000.0,
avg_speed: 5.3,
avg_paving_temp: 145.0,
avg_compaction: 91.8,
status: 1,
},
{
project_name: '沧州港区道路',
stake_range: 'B区 ~ D区',
total_area: 9500.0,
avg_speed: 4.7,
avg_paving_temp: 150.0,
avg_compaction: 95.3,
status: 1,
},
]