diff --git a/src/api/report/index.ts b/src/api/report/index.ts index 817d9d8..78c8391 100644 --- a/src/api/report/index.ts +++ b/src/api/report/index.ts @@ -31,17 +31,42 @@ export interface ReportItem { remark?: string | null } +/** POST /api/report/upload 请求体 */ export interface UploadReportParams { - /** 本地文件路径(uni.uploadFile) */ - filePath: string + /** 本机出厂 SN */ deviceId: string - projectId: string + /** 项目名称(文字,不是 ID);项目不存在时后端自动创建 */ + projectName: string + /** 报告编号,全局唯一,如 RTP-20251215-001 */ reportNo: string + /** 作业开始时间 yyyy-MM-dd HH:mm:ss */ workStartTime: string + /** 作业结束时间 yyyy-MM-dd HH:mm:ss */ workEndTime: string - compactArea: string - qualificationRate: string - constructionSection: string + /** 施工桩号,如 K1+200-K1+800 */ + constructionSection?: string + /** 压实面积(㎡) */ + compactArea?: number + /** 压实合格率(%) */ + qualificationRate?: number + /** 作业时长(小时) */ + workDuration?: number + /** 碾压遍数 */ + passCount?: number + /** 平均行驶速度(KM/h) */ + avgSpeed?: number + /** 平均振动频率(Hz) */ + avgFrequency?: number + /** 平均振幅(mm) */ + avgAmplitude?: number + /** 平均温度(℃) */ + avgTemperature?: number + /** 平均压实度 */ + avgCompaction?: number + /** 异常次数 */ + exceptionCount?: number + /** 检测结论 */ + conclusion?: string } /** GET /api/report/list 报告列表 */ @@ -61,38 +86,11 @@ export function getReportDetail(reportId: number | string) { }) } -/** - * POST /api/report/upload 上传报告文件 - * 使用 uni.uploadFile,适用于 App / 小程序本地文件路径 - */ +/** POST /api/report/upload 上传报告(JSON) */ export function uploadReport(data: UploadReportParams) { - const baseURL = import.meta.env.VITE_API_BASE_URL || '' - - return new Promise((resolve, reject) => { - uni.uploadFile({ - url: `${baseURL}/api/report/upload`, - filePath: data.filePath, - name: 'file', - formData: { - deviceId: data.deviceId, - projectId: data.projectId, - reportNo: data.reportNo, - workStartTime: data.workStartTime, - workEndTime: data.workEndTime, - compactArea: data.compactArea, - qualificationRate: data.qualificationRate, - constructionSection: data.constructionSection, - }, - success: (res) => { - try { - const result = typeof res.data === 'string' ? JSON.parse(res.data) : res.data - resolve(result) - } - catch (e) { - reject(e) - } - }, - fail: reject, - }) + return request({ + url: '/api/report/upload', + method: 'POST', + data, }) } diff --git a/src/composables/useDeviceAuth.ts b/src/composables/useDeviceAuth.ts index a005ac6..d0377df 100644 --- a/src/composables/useDeviceAuth.ts +++ b/src/composables/useDeviceAuth.ts @@ -94,13 +94,22 @@ let lastExpireShownAt = 0 /** * 进入软件:先查本地激活码 → 无码再查授权状态并写入 storage * 激活成功后再查一次状态写入 storage,形成闭环 - * 仅未授权(0)或已过期(2)时展示 unauthorized / activateNotice + * authStatus 为 1(已激活)时,unauthorized / activateNotice 一律不展示 */ export function useDeviceAuth(options?: { watchShow?: boolean }) { const authStatus = computed(() => toAuthStatus(authInfo.value?.authStatus)) const isActivated = computed(() => authStatus.value === '1') - /** 是否需要展示未授权提示组件 */ - const needAuthPrompt = computed(() => authStatus.value === '0' || authStatus.value === '2') + /** + * 是否需要展示未授权提示组件 + * + * 只有 authStatus === '1'(已激活)才隐藏,其余状态(0 未授权 / 2 已过期 / + * 3 未知 / null 未查到)一律展示提示,避免服务端返回未预期状态时 + * 未激活组件仍然可见。 + * + * 注意:authStatus 为 null 表示「查询尚未完成」,此时也视为需要提示 + * (与改造前行为一致,不会出现空白)。首次查询结束后 authReady 为 true。 + */ + const needAuthPrompt = computed(() => authStatus.value !== '1') const hasActivationCode = computed(() => !!activationCode.value.trim()) const deviceId = computed(() => authInfo.value?.deviceId || getDeviceId()) const remainingDays = computed(() => { @@ -198,7 +207,10 @@ export function useDeviceAuth(options?: { watchShow?: boolean }) { activationCode.value = localCode.trim() const cached = readCachedAuthInfo() if (cached) { + // 先用缓存让 UI 立即就绪 authInfo.value = cached + // 再异步向服务端核对一次,保证 authStatus 最新(如服务端已改为已激活) + fetchAuthStatus() return authInfo.value } // 有激活码但无缓存信息,补拉一次状态 @@ -257,9 +269,25 @@ export function useDeviceAuth(options?: { watchShow?: boolean }) { return res } - function refreshAuthStatus() { + /** + * 刷新授权状态: + * 先同步本地缓存(立即反映变化),再异步向服务端查询一次, + * 保证服务端已变为「已激活(1)」时,未授权提示组件能立即隐藏。 + */ + async function refreshAuthStatus(): Promise { + // 1) 先读本地缓存,保证 UI 即时响应 authInfo.value = readCachedAuthInfo() activationCode.value = storage.get(ACTIVATION_CODE_KEY, '') ?? '' + + // 2) 再查服务端,查到就覆盖(saveAuthInfo 内部会写回 storage) + const info = await fetchAuthStatus() + if (info) + return info + + // 3) 查询失败:若本地没有任何信息,按未授权兜底,避免一直停在初始态 + if (!authInfo.value) + saveAuthInfo(createUnauthorizedInfo()) + return authInfo.value } if (options?.watchShow) { @@ -267,7 +295,6 @@ export function useDeviceAuth(options?: { watchShow?: boolean }) { refreshAuthStatus() }) } - return { authInfo, authStatus, diff --git a/src/pages/Ahome/index.vue b/src/pages/Ahome/index.vue index 200811c..e5371fa 100644 --- a/src/pages/Ahome/index.vue +++ b/src/pages/Ahome/index.vue @@ -11,7 +11,7 @@ definePage({ }, }) -const { needAuthPrompt, initAuth, openExpireNoticeIfNeeded } = useDeviceAuth({ watchShow: true }) +const { needAuthPrompt, initAuth, refreshAuthStatus, openExpireNoticeIfNeeded } = useDeviceAuth({ watchShow: true }) async function checkExpireNotice() { await nextTick() @@ -26,9 +26,11 @@ onMounted(async () => { await checkExpireNotice() }) -/** 每次打开 / 回到首页:读 storage 判断是否弹到期提醒 */ -onShow(() => { - checkExpireNotice() +/** 每次打开 / 回到首页:重新查询授权状态 + 判断是否弹到期提醒 */ +onShow(async () => { + // 查询到 authStatus 为 1 时,needAuthPrompt 会自动变 false,隐藏未授权组件 + await refreshAuthStatus() + await checkExpireNotice() }) diff --git a/src/pages/report/detail/index.vue b/src/pages/report/detail/index.vue index 04fbc5d..cc07bbf 100644 --- a/src/pages/report/detail/index.vue +++ b/src/pages/report/detail/index.vue @@ -2,7 +2,9 @@ import type { HistoryReport } from '@/service/historyReport' import { onLoad } from '@dcloudio/uni-app' import { ref } from 'vue' +import { uploadReport } from '@/api/report' import { useDbStore } from '@/store/modules/db' +import { getDeviceId } from '@/utils/device' import storage from '@/utils/storage' definePage({ @@ -30,6 +32,8 @@ interface ReportDetailView { const db = useDbStore() const reportData = ref(null) +const rawReport = ref(null) +const uploading = ref(false) function parseDate(value?: string) { if (!value) @@ -38,8 +42,8 @@ function parseDate(value?: string) { return Number.isNaN(date.getTime()) ? null : date } -function pad(value: number) { - return String(value).padStart(2, '0') +function pad(value: number, length = 2) { + return String(value).padStart(length, '0') } function formatDateTimeRange(start?: string, end?: string) { @@ -60,6 +64,19 @@ 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) { const hours = value ?? 0 const text = Number.isInteger(hours) ? String(hours) : hours.toFixed(1) @@ -124,23 +141,88 @@ onLoad(async (options) => { if (!id) return - if (!db.history_report.length) + if (!db.history_report.length || !db.project.length) await db.loadAll() const item = db.history_report.find(report => report.id === id) - if (item) + if (item) { + rawReport.value = item reportData.value = mapReportDetail(item) + } }) -function handleDownload() { - console.log('点击下载') +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() + } }