feat 联调报告上传接口

This commit is contained in:
Ironsp
2026-09-27 15:21:39 +08:00
parent 8ffbc10a4e
commit 54a5f96644
7 changed files with 271 additions and 186 deletions

10
pnpm-lock.yaml generated
View File

@@ -3240,8 +3240,8 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
caniuse-lite@1.0.30001737:
resolution: {integrity: sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==}
caniuse-lite@1.0.30001812:
resolution: {integrity: sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -10260,7 +10260,7 @@ snapshots:
autoprefixer@10.4.20(postcss@8.5.6):
dependencies:
browserslist: 4.25.3
caniuse-lite: 1.0.30001737
caniuse-lite: 1.0.30001812
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.1.1
@@ -10410,7 +10410,7 @@ snapshots:
browserslist@4.25.3:
dependencies:
caniuse-lite: 1.0.30001737
caniuse-lite: 1.0.30001812
electron-to-chromium: 1.5.209
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.3)
@@ -10482,7 +10482,7 @@ snapshots:
camelcase@6.3.0: {}
caniuse-lite@1.0.30001737: {}
caniuse-lite@1.0.30001812: {}
ccount@2.0.1: {}

View File

@@ -1,4 +1,4 @@
import type { ApiResult, PageResult, ReportStatus } from '../types'
import type { ApiResult, ReportStatus } from '../types'
import { request } from '../request'
export interface ReportItem {
@@ -70,11 +70,10 @@ export interface UploadReportParams {
}
/** GET /api/report/list 报告列表 */
export function getReportList(params?: Record<string, unknown>) {
return request<PageResult<ReportItem>>({
export function getReportList() {
return request({
url: '/api/report/list',
method: 'GET',
params,
})
}
@@ -86,7 +85,7 @@ export function getReportDetail(reportId: number | string) {
})
}
/** POST /api/report/upload 上传报告(JSON) */
/** POST /api/report/upload 上传报告数据(JSON) */
export function uploadReport(data: UploadReportParams) {
return request<ApiResult>({
url: '/api/report/upload',

View File

@@ -1,8 +1,6 @@
<script setup lang="ts">
import storage from '@/utils/storage'
const props = defineProps<{
id?: number
reportId?: number
title: string
deviceCode: string
status: string
@@ -14,25 +12,13 @@ const props = defineProps<{
}>()
function goToDetail() {
const key = `report_${Date.now()}`
storage.set(key, {
id: props.id,
title: props.title,
deviceCode: props.deviceCode,
status: props.status,
time: props.time,
area: props.area,
duration: props.duration,
areaCount: props.areaCount,
passRate: props.passRate,
})
const query = [`key=${key}`]
if (props.id != null)
query.push(`id=${props.id}`)
if (props.reportId == null) {
uni.showToast({ title: '报告ID缺失', icon: 'none' })
return
}
uni.navigateTo({
url: `/pages/report/detail/index?${query.join('&')}`,
url: `/pages/report/detail/index?reportId=${props.reportId}`,
})
}
</script>

View File

@@ -1,7 +1,9 @@
import type { Project } from '@/service/project'
import { uploadReport } from '@/api/report'
import { useCurrentProject } from '@/composables/useCurrentProject'
import { historyReportService, projectService, workRecordService } from '@/service'
import { useDbStore } from '@/store/modules/db'
import { getDeviceId } from '@/utils/device'
import storage from '@/utils/storage'
/** 本地时间字符串,格式与 seed 一致:YYYY-MM-DD HH:mm:ss */
@@ -127,7 +129,7 @@ export function useProjectLifecycle() {
})
}
/** 结束项目 → 已结束,生成历史报告 */
/** 结束项目 → 先上传报告,成功后再改本地状态并生成历史报告 */
async function endProject(projectId: number) {
await withBusy(async () => {
const project = await projectService.selectById(projectId)
@@ -136,34 +138,71 @@ export function useProjectLifecycle() {
return
}
if (project.status === 1) {
uni.showToast({ title: '项目已结束', icon: 'none' })
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 deviceId = getDeviceId() || storage.get<string>('device_unique_id') || ''
if (!deviceId || !latest.project_name) {
uni.showToast({ title: '上传失败,请重新点击结束项目', icon: 'none' })
return
}
const endTime = nowDateTime()
const workStartTime = latest.start_time || endTime
const workRecords = await workRecordService.selectByProjectId(projectId)
const exceptionCount = workRecords.reduce((sum, item) => sum + (item.exception_count ?? 0), 0)
let uploaded = false
try {
const res = await uploadReport({
deviceId,
projectName: latest.project_name,
reportNo: `RTK-${projectId}`,
workStartTime,
workEndTime: endTime,
constructionSection: latest.stake_range,
compactArea: latest.total_compaction_area ?? 0,
qualificationRate: latest.qualified_rate ?? 0,
workDuration: latest.total_duration ?? 0,
passCount: latest.total_pass_count ?? 0,
avgSpeed: latest.avg_speed ?? 0,
avgTemperature: latest.avg_paving_temp ?? 0,
avgCompaction: latest.avg_compaction ?? 0,
exceptionCount,
})
uploaded = res.code === 200
}
catch {
uploaded = false
}
if (!uploaded) {
uni.showToast({ title: '上传失败,请重新点击结束项目', icon: 'none' })
return
}
// 上传成功后再改本地状态
const open = await findOpenWorkRecord(projectId)
if (open?.id) {
await workRecordService.update(open.id, {
end_time: endTime,
duration: calcDurationHours(open.start_time, endTime),
})
}
await historyReportService.insert({
project_id: projectId,
report_name: `${latest.project_name} 碾压报告`,
device_id: storage.get<string>('device_unique_id') || undefined,
device_id: deviceId,
construction_area: latest.stake_range,
start_time: latest.start_time,
start_time: workStartTime,
end_time: endTime,
duration: latest.total_duration ?? 0,
compaction_area: latest.total_compaction_area ?? 0,
@@ -184,7 +223,7 @@ export function useProjectLifecycle() {
setCurrentProject(null)
await refreshAfterChange(projectId)
uni.showToast({ title: '已结束并生成报告', icon: 'success' })
uni.showToast({ title: '项目已经结束', icon: 'success' })
})
}

View File

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

View File

@@ -1,11 +1,10 @@
<script setup lang="ts">
import type { ReportItem } from '@/api/report'
import type { HistoryReport } from '@/service/historyReport'
import { onLoad } from '@dcloudio/uni-app'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { uploadReport } from '@/api/report'
import { getReportDetail } from '@/api/report'
import { useDbStore } from '@/store/modules/db'
import { getDeviceId } from '@/utils/device'
import storage from '@/utils/storage'
definePage({
layout: false,
@@ -31,11 +30,13 @@ interface ReportDetailView {
}
const db = useDbStore()
const reportId = ref<number | null>(null)
const reportData = ref<ReportDetailView | null>(null)
const apiReport = ref<ReportItem | null>(null)
const rawReport = ref<HistoryReport | null>(null)
const uploading = ref(false)
function parseDate(value?: string) {
function parseDate(value?: string | null) {
if (!value)
return null
const date = new Date(value.replace(/-/g, '/'))
@@ -46,7 +47,7 @@ function pad(value: number, length = 2) {
return String(value).padStart(length, '0')
}
function formatDateTimeRange(start?: string, end?: string) {
function formatDateTimeRange(start?: string | null, end?: string | null) {
const startDate = parseDate(start)
const endDate = parseDate(end)
if (!startDate)
@@ -64,36 +65,45 @@ function formatDateTimeRange(start?: string, end?: string) {
return `${datePart}${startHm}-${endDate.getFullYear()}年${pad(endDate.getMonth() + 1)}月${pad(endDate.getDate())}日${endHm}`
}
/** 接口时间格式 yyyy-MM-dd HH:mm:ss */
function formatApiDateTime(value?: string) {
const date = parseDate(value)
if (!date)
return ''
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
/** 报告编号:RTK-项目id */
function buildReportNo(item: HistoryReport) {
return `RTK-${item.project_id}`
}
function formatDuration(value?: number) {
function formatDuration(value?: number | null) {
const hours = value ?? 0
const text = Number.isInteger(hours) ? String(hours) : hours.toFixed(1)
return `${text}H`
}
function formatArea(value?: number) {
function formatArea(value?: number | null) {
return `${value ?? 0}m²`
}
function formatRate(value?: number) {
function formatRate(value?: number | null) {
const rate = value ?? 0
const text = Number.isInteger(rate) ? String(rate) : rate.toFixed(1)
return `${text}%`
}
function mapReportDetail(item: HistoryReport): ReportDetailView {
function formatMetric(value: number | null | undefined, unit: string) {
return value != null ? `${value}${unit}` : '-'
}
function mapApiReportDetail(item: ReportItem): ReportDetailView {
return {
deviceCode: item.deviceId || '-',
time: formatDateTimeRange(item.workStartTime, item.workEndTime),
status: String(item.reportStatus) === '1' ? '有异常' : '作业已完成',
duration: formatDuration(item.workDuration),
areaCount: formatArea(item.compactArea),
passCount: `${item.passCount ?? 0}遍`,
passRate: formatRate(item.qualificationRate),
avgSpeed: formatMetric(item.avgSpeed, 'km/h'),
avgFrequency: formatMetric(item.avgFrequency, 'Hz'),
avgAmplitude: formatMetric(item.avgAmplitude, 'mm'),
avgTemperature: formatMetric(item.avgTemperature, '℃'),
avgCompaction: item.avgCompaction != null ? formatRate(item.avgCompaction) : '-',
exceptionCount: String(item.exceptionCount ?? 0),
}
}
function mapLocalReportDetail(item: HistoryReport): ReportDetailView {
return {
deviceCode: item.device_id || '-',
time: formatDateTimeRange(item.start_time, item.end_time),
@@ -102,120 +112,76 @@ function mapReportDetail(item: HistoryReport): ReportDetailView {
areaCount: formatArea(item.compaction_area),
passCount: `${item.pass_count ?? 0}遍`,
passRate: formatRate(item.qualified_rate),
avgSpeed: item.avg_speed != null ? `${item.avg_speed}km/h` : '-',
avgSpeed: formatMetric(item.avg_speed, 'km/h'),
avgFrequency: '-',
avgAmplitude: '-',
avgTemperature: item.avg_paving_temp != null ? `${item.avg_paving_temp}℃` : '-',
avgTemperature: formatMetric(item.avg_paving_temp, '℃'),
avgCompaction: item.avg_compaction != null ? formatRate(item.avg_compaction) : '-',
exceptionCount: String(item.exception_count ?? 0),
}
}
function applyStoragePayload(data: Record<string, any>) {
reportData.value = {
deviceCode: data.deviceCode || '-',
time: data.time || '-',
status: data.status || '作业已完成',
duration: data.duration || '-',
areaCount: data.areaCount || '-',
passCount: data.passCount || '-',
passRate: data.passRate || '-',
avgSpeed: data.avgSpeed || '-',
avgFrequency: data.avgFrequency || '-',
avgAmplitude: data.avgAmplitude || '-',
avgTemperature: data.avgTemperature || '-',
avgCompaction: data.avgCompaction || '-',
exceptionCount: data.exceptionCount || '-',
}
/** 兼容 data 嵌套 / 扁平结构 */
function pickReportPayload(res: unknown): ReportItem | null {
if (!res || typeof res !== 'object')
return null
const raw = res as Record<string, unknown>
const nested = raw.data
if (nested && typeof nested === 'object' && 'reportId' in (nested as object))
return nested as ReportItem
if ('reportId' in raw)
return raw as unknown as ReportItem
return null
}
onLoad(async (options) => {
if (options?.key) {
const data = storage.get(options.key)
if (data)
applyStoragePayload(data)
storage.remove(options.key)
}
const id = Number(options?.id)
if (!id)
return
async function loadLocalFallback(id: number) {
if (!db.history_report.length || !db.project.length)
await db.loadAll()
const item = db.history_report.find(report => report.id === id)
if (item) {
rawReport.value = item
reportData.value = mapReportDetail(item)
apiReport.value = null
reportData.value = mapLocalReportDetail(item)
}
}
/** 优先请求详情接口,失败回退本地库 */
async function loadReportDetail() {
const id = reportId.value
if (!id)
return
try {
const res = await getReportDetail(id)
if (res.code === 200) {
const payload = pickReportPayload(res)
if (payload) {
apiReport.value = payload
rawReport.value = null
reportData.value = mapApiReportDetail(payload)
return
}
}
}
catch {
// 接口不通,走本地
}
await loadLocalFallback(id)
}
onLoad((options) => {
const id = Number(options?.reportId || options?.id)
reportId.value = id || null
})
onShow(() => {
loadReportDetail()
})
async function handleDownload() {
if (uploading.value)
return
const item = rawReport.value
if (!item) {
uni.showToast({ title: '暂无报告数据', icon: 'none' })
return
}
if (!db.project.length)
await db.loadAll()
const project = db.project.find(p => p.id === item.project_id)
const projectName = project?.project_name
if (!projectName) {
uni.showToast({ title: '项目信息不完整', icon: 'none' })
return
}
const workStartTime = formatApiDateTime(item.start_time)
const workEndTime = formatApiDateTime(item.end_time)
if (!workStartTime || !workEndTime) {
uni.showToast({ title: '作业时间不完整', icon: 'none' })
return
}
const deviceId = getDeviceId() || item.device_id || ''
if (!deviceId) {
uni.showToast({ title: '设备号缺失', icon: 'none' })
return
}
uploading.value = true
uni.showLoading({ title: '上传中...', mask: true })
try {
const res = await uploadReport({
deviceId,
projectName,
reportNo: buildReportNo(item),
workStartTime,
workEndTime,
constructionSection: item.construction_area,
compactArea: item.compaction_area,
qualificationRate: item.qualified_rate,
workDuration: item.duration,
passCount: item.pass_count,
avgSpeed: item.avg_speed,
avgTemperature: item.avg_paving_temp,
avgCompaction: item.avg_compaction,
exceptionCount: item.exception_count,
})
const ok = res.code === 200 || res.code === 0
if (!ok)
throw new Error(res.msg || '上传失败')
uni.showToast({ title: '上传成功', icon: 'success' })
}
catch (e: any) {
uni.showToast({ title: e?.message || '上传失败', icon: 'none' })
}
finally {
uploading.value = false
uni.hideLoading()
}
console.log('下载报告')
}
</script>

View File

@@ -1,6 +1,9 @@
<script setup lang="ts">
import type { ReportItem } from '@/api/report'
import type { HistoryReport } from '@/service/historyReport'
import { computed, onMounted, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { computed, ref } from 'vue'
import { getReportList } from '@/api/report'
import { useDbStore } from '@/store/modules/db'
definePage({
@@ -10,8 +13,27 @@ definePage({
},
})
/** 列表统一结构,便于接口 / 本地双数据源筛选展示 */
interface ReportListRow {
id?: number
title: string
deviceCode: string
status: string
time: string
area: string
duration: string
areaCount: string
passRate: string
projectId: string
startTime?: string
reportStatus: string
}
const db = useDbStore()
/** 非 null 表示接口可用,优先用接口数据 */
const apiReports = ref<ReportItem[] | null>(null)
const datetimerange = ref<string[]>([])
const reportStatus = ref('')
const area = ref('')
@@ -30,24 +52,48 @@ const reportStatusOptions = [
{ value: 'pending', text: '待审核' },
]
const listRows = computed<ReportListRow[]>(() => {
if (apiReports.value) {
return apiReports.value
.slice()
.sort((a, b) => (b.reportId ?? 0) - (a.reportId ?? 0))
.map(mapApiReportItem)
}
return db.history_report
.slice()
.sort((a, b) => (b.id ?? 0) - (a.id ?? 0))
.map(mapLocalReportItem)
})
const areaOptions = computed(() => {
const areas = [...new Set(db.history_report.map(item => item.construction_area).filter(Boolean))] as string[]
const areas = [...new Set(listRows.value.map(item => item.area).filter(v => v && v !== '-'))]
return [
{ value: 'all', text: '全部' },
...areas.map(text => ({ value: text, text })),
]
})
const projectOptions = computed(() => [
const projectOptions = computed(() => {
if (apiReports.value) {
const map = new Map<string, string>()
for (const item of apiReports.value) {
if (item.projectId != null)
map.set(String(item.projectId), item.projectName || String(item.projectId))
}
return [
{ value: 'all', text: '全部' },
...[...map.entries()].map(([value, text]) => ({ value, text })),
]
}
return [
{ value: 'all', text: '全部' },
...db.project.map(item => ({ value: String(item.id), text: item.project_name })),
])
]
})
const reports = computed(() => {
const query = appliedQuery.value
return db.history_report
.filter(item => matchReport(item, query))
.map(mapReportItem)
return listRows.value.filter(item => matchReport(item, query))
})
function isAll(value: string) {
@@ -55,16 +101,18 @@ function isAll(value: string) {
}
function matchReport(
item: HistoryReport,
item: ReportListRow,
query: { datetimerange: string[], reportStatus: string, area: string, project: string },
) {
if (!isAll(query.project) && String(item.project_id) !== query.project)
if (!isAll(query.project) && item.projectId !== query.project)
return false
if (!isAll(query.area) && item.construction_area !== query.area)
if (!isAll(query.area) && item.area !== query.area)
return false
if (query.reportStatus === 'pending')
return false
if (!matchDateRange(item.start_time, query.datetimerange))
if (query.reportStatus === 'completed' && item.reportStatus === '1')
return false
if (!matchDateRange(item.startTime, query.datetimerange))
return false
return true
}
@@ -109,23 +157,46 @@ function formatDateTimeRange(start?: string, end?: string) {
return `${datePart}${startHm}-${endDate.getFullYear()}年${pad(endDate.getMonth() + 1)}月${pad(endDate.getDate())}日${endHm}`
}
function formatDuration(value?: number) {
function formatDuration(value?: number | null) {
const hours = value ?? 0
const text = Number.isInteger(hours) ? String(hours) : hours.toFixed(1)
return `${text}H`
}
function formatArea(value?: number) {
function formatArea(value?: number | null) {
return `${value ?? 0}m²`
}
function formatRate(value?: number) {
function formatRate(value?: number | null) {
const rate = value ?? 0
const text = Number.isInteger(rate) ? String(rate) : rate.toFixed(1)
return `${text}%`
}
function mapReportItem(item: HistoryReport) {
function mapReportStatus(status?: ReportItem['reportStatus']) {
if (String(status) === '1')
return '有异常'
return '已完成'
}
function mapApiReportItem(item: ReportItem): ReportListRow {
return {
id: item.reportId,
title: item.reportNo || item.projectName || '碾压报告',
deviceCode: item.deviceId || '-',
status: mapReportStatus(item.reportStatus),
time: formatDateTimeRange(item.workStartTime, item.workEndTime),
area: item.constructionSection || '-',
duration: formatDuration(item.workDuration),
areaCount: formatArea(item.compactArea),
passRate: formatRate(item.qualificationRate),
projectId: item.projectId != null ? String(item.projectId) : '',
startTime: item.workStartTime,
reportStatus: String(item.reportStatus ?? '0'),
}
}
function mapLocalReportItem(item: HistoryReport): ReportListRow {
return {
id: item.id,
title: item.report_name,
@@ -136,6 +207,9 @@ function mapReportItem(item: HistoryReport) {
duration: formatDuration(item.duration),
areaCount: formatArea(item.compaction_area),
passRate: formatRate(item.qualified_rate),
projectId: item.project_id != null ? String(item.project_id) : '',
startTime: item.start_time,
reportStatus: '0',
}
}
@@ -148,9 +222,30 @@ function handleQuery() {
}
}
onMounted(async () => {
if (!db.history_report.length)
await db.loadAll()
/** 优先拉接口;code 非 200 或请求失败则回退本地 SQLite */
async function loadReports() {
// 本地库并行准备,不阻塞接口请求
const localTask = (!db.history_report.length || !db.project.length)
? db.loadAll().catch(() => undefined)
: Promise.resolve()
try {
const res = await getReportList()
if (res.code === 200 && Array.isArray(res.rows)) {
apiReports.value = res.rows
return
}
}
catch {
// 接口不通,走本地
}
apiReports.value = null
await localTask
}
onShow(() => {
loadReports()
})
</script>
@@ -213,7 +308,7 @@ onMounted(async () => {
<!-- 报告列表 -->
<view class="report-header">
<text class="report-title">
报告列表
报告列表11111
</text>
<text class="report-count">
共{{ reports.length }}条
@@ -226,8 +321,8 @@ onMounted(async () => {
</view>
<report-item
v-for="item in reports"
:id="item.id"
:key="item.id"
:report-id="item.id"
:title="item.title"
:device-code="item.deviceCode"
:status="item.status"