diff --git a/client/src/api/surveyOrder.ts b/client/src/api/surveyOrder.ts index 388975b..806eebc 100644 --- a/client/src/api/surveyOrder.ts +++ b/client/src/api/surveyOrder.ts @@ -78,7 +78,10 @@ export interface SurveyOrderSopResult { surveyOrderId: number; sopItemId: number; checked: boolean; - receiptValue?: string; + // 回执内容:photo/meter 类为照片对象 key 数组,form 类为单元素文本值数组 + receiptValue?: string[]; + // 详情/回执接口附带:照片类回执的预签名访问 URL(与 receiptValue 顺序一致) + receiptUrls?: string[]; remark?: string; checkedAt?: string; // 由后端 join 的检查项信息 @@ -127,6 +130,8 @@ export interface SurveyOrderInfo { estimatedFee?: number | string; actualFee?: number | string; photos?: string[]; + // 详情附带:现场照片预签名访问 URL(与 photos 顺序一致) + photoUrls?: string[]; reviewRemark?: string; reviewedAt?: string; cancelReason?: string; diff --git a/client/src/assets/styles/global.less b/client/src/assets/styles/global.less index 3c7a512..b5ec855 100644 --- a/client/src/assets/styles/global.less +++ b/client/src/assets/styles/global.less @@ -265,28 +265,45 @@ ol { animation: slideInUp 0.3s ease-out; } -// ==================== 滚动条美化 ==================== +// ==================== 滚动条美化(iOS 风格) ==================== +// iOS 风格特征:极细(4px)、全圆角胶囊形、无轨道、半透明 thumb 悬浮在内容上 ::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 4px; + height: 4px; } ::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 4px; + background: transparent; } ::-webkit-scrollbar-thumb { - background: #c1c1c1; - border-radius: 4px; + background: rgba(0, 0, 0, 0.2); + border-radius: 999px; &:hover { - background: #a8a8a8; + background: rgba(0, 0, 0, 0.35); } } +// 深色背景下的滚动条变体(侧边栏等) +.sider-inner ::-webkit-scrollbar-thumb, +.ant-layout-sider ::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.25); +} + +.sider-inner ::-webkit-scrollbar-thumb:hover, +.ant-layout-sider ::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.4); +} + // Firefox * { scrollbar-width: thin; - scrollbar-color: #c1c1c1 #f1f1f1; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; +} + +// Firefox 深色变体 +.sider-inner, +.ant-layout-sider { + scrollbar-color: rgba(255, 255, 255, 0.25) transparent; } diff --git a/client/src/layouts/index.vue b/client/src/layouts/index.vue index 6600234..54f73bc 100644 --- a/client/src/layouts/index.vue +++ b/client/src/layouts/index.vue @@ -3,19 +3,21 @@ - - +
+ + - - + + - -
- + +
+ +
@@ -301,7 +303,19 @@ function handleUserMenuClick({ key }: { key: string }) { z-index: 100; box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35); + :deep(.ant-layout-sider-children) { + height: 100%; + } + + .sider-inner { + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + } + .logo { + flex-shrink: 0; height: 64px; display: flex; align-items: center; @@ -336,6 +350,10 @@ function handleUserMenuClick({ key }: { key: string }) { :deep(.ant-menu) { border-right: none; + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; } // iconfont 图标统一:该图标集为彩色字体,颜色写死在字形里,无法用 color 覆盖, @@ -363,10 +381,7 @@ function handleUserMenuClick({ key }: { key: string }) { } .collapse-btn { - position: absolute; - bottom: 0; - left: 0; - right: 0; + flex-shrink: 0; height: 48px; display: flex; align-items: center; diff --git a/client/src/views/project/survey/form.vue b/client/src/views/project/survey/form.vue index 4b1d3ea..9f44f34 100644 --- a/client/src/views/project/survey/form.vue +++ b/client/src/views/project/survey/form.vue @@ -15,10 +15,27 @@
-

现场勘察派单向导

-

标准化派发配电房现场测绘任务,向共享电工推单并回传拓扑参数

+
+

{{ isReadonly ? '勘察工单详情' : '现场勘察派单向导' }}

+ + + {{ statusMeta.label }} + +
+

+ {{ + isReadonly + ? '该工单已流转至复核 / 归档环节,信息仅供查看;如需调整请联系技术负责人' + : '标准化派发配电房现场测绘任务,向共享电工推单并回传拓扑参数' + }} +

- STEP 0{{ currentStep }} / 03 + 只读查看 + STEP 0{{ currentStep }} / 03
@@ -67,6 +84,7 @@ size="large" style="width: 100%" option-filter-prop="label" + :disabled="isReadonly" :options="customerSelectOptions" @change="handleCustomerChange" /> @@ -90,6 +108,7 @@ :class="{ err: errors.taskTitle }" type="text" maxlength="128" + :disabled="isReadonly" placeholder="如:实训楼智能配电改造一期工程现场测绘" @input="clearError('taskTitle')" /> @@ -104,6 +123,7 @@ :class="{ err: errors.siteLocation }" type="text" maxlength="255" + :disabled="isReadonly" placeholder="如:实训主楼 1F低压室 / 2~3F强电井" @input="clearError('siteLocation')" /> @@ -117,6 +137,7 @@ class="ipt mono-ipt" type="text" maxlength="128" + :disabled="isReadonly" placeholder="如:380V / 400A MCCB (SCB10-630kVA)" />
@@ -130,6 +151,7 @@ :class="{ err: errors.loopCount }" type="number" min="0" + :disabled="isReadonly" @input="clearError('loopCount')" /> 路 @@ -141,7 +163,7 @@
接单后 - + 分钟内
@@ -153,6 +175,7 @@ class="ipt" type="text" maxlength="128" + :disabled="isReadonly" placeholder="如:程建业 (项目主管) - 138****9201" />
@@ -164,6 +187,7 @@ format="YYYY-MM-DD" size="large" style="width: 100%" + :disabled="isReadonly" :placeholder="['开始日期', '结束日期']" /> @@ -177,6 +201,7 @@ type="button" class="mode-pill" :class="{ active: formState.priority === p.value }" + :disabled="isReadonly" @click="formState.priority = p.value" > {{ p.label }} @@ -194,7 +219,7 @@

第 2 步:勘察技术要点与回执标准

-
+
SOP规范模板:
-
+
+ 自定义勘察检查项
@@ -242,7 +267,7 @@ v-for="w in workers" :key="w.id" class="cand-card" - :class="{ selected: formState.workerId === w.id }" + :class="{ selected: formState.workerId === w.id, readonly: isReadonly }" @click="selectWorker(w)" >
@@ -256,7 +281,7 @@ {{ w.workerTitle || '共享电工' }}
- +
{{ Number(w.rating || 0).toFixed(2) }} 分 @@ -290,6 +315,7 @@ v-model:value="formState.slaLevel" size="small" style="width: 210px" + :disabled="isReadonly" :options="SLA_OPTIONS" />
@@ -302,25 +328,147 @@ + +
+
+
+ + +

测绘成果与复核记录

+
+ 现场回执数据 · 仅供查看 +
+ +
+ +
+
+ 实际劳务费 + {{ roActualFee }} +
+
+ 测绘进度 + {{ roProgressDone }} / {{ roProgressTotal }} 路 +
+
+ SOP 回执 + {{ roSopChecked }} / {{ roSopResults.length }} 项 +
+
+ 现场照片 + {{ roPhotos.length }} 张 +
+
+ 指派电工 + {{ detailRecord.workerName }} +
+
+ + +
+
+ 现场测绘完成度 + {{ roProgressPercent }}% +
+
+
+
+
+ + +
+
SOP 回执明细
+
暂无 SOP 回执记录
+
+
+ + + + {{ r.itemName || r.sopItem?.itemName || `检查项 #${r.sopItemId}` }} + {{ receiptTypeLabel(r.receiptType || r.sopItem?.receiptType) }} +
+ +
+ {{ r.receiptValue?.length ? r.receiptValue.join('、') : r.remark || '—' }} +
+
+
+ + +
+
现场取证照片
+
暂无现场照片
+
+ + + +
+
+ + +
+
复核与处理记录
+
暂无复核与备注记录
+
+
+
+ 技术复核 + + {{ formatDateTime(detailRecord.reviewedAt) }} + +
+
{{ detailRecord.reviewRemark }}
+
+
+
+ 取消原因 +
+
{{ detailRecord.cancelReason }}
+
+
+
+ 派单备注 +
+
{{ detailRecord.remark }}
+
+
+
+
+
+
- 向导已填报 {{ filledCount }}/3 核心项 · 数据直连共享电工协同网 + 工单已流转至「{{ statusMeta?.label || '只读' }}」 · 当前为只读查看 + 向导已填报 {{ filledCount }}/3 核心项 · 数据直连共享电工协同网
- + - 上一步 - - - - 保存为草稿 - - - - 确认并推送共享电工派单 + 返回勘察台账 +
@@ -348,6 +496,7 @@ import { surveyOrderApi, SOP_TEMPLATE_CODE, SOP_TEMPLATE_LABEL, + SURVEY_STATUS, SURVEY_PRIORITY, SURVEY_PRIORITY_OPTIONS, SURVEY_BASE_FEE, @@ -355,6 +504,7 @@ import { type SurveySopItem, type SurveyQuote, type SurveyOrderCreateParams, + type SurveyOrderInfo, } from '@/api/surveyOrder'; import { workerApi, type WorkerRecommend } from '@/api/worker'; import { customerApi, type CustomerInfo } from '@/api/customer'; @@ -387,9 +537,54 @@ const DEFAULT_EXPECT_MINUTES = 45; // ==================== 模式 ==================== const editId = computed(() => Number(route.params.id || 0)); const isEdit = computed(() => editId.value > 0); -const pageTitle = computed(() => (isEdit.value ? '编辑勘察派单' : '新建勘察派单')); + +// 详情与编辑共用本页:status 0 待指派 / 1 现场勘察中 可编辑,其余状态进入只读查看 +const detailRecord = ref(null); +const isReadonly = computed(() => { + const status = detailRecord.value?.status; + return status !== undefined && status !== SURVEY_STATUS.PENDING_ASSIGN && status !== SURVEY_STATUS.SURVEYING; +}); + +const pageTitle = computed(() => (isEdit.value ? (isReadonly.value ? '勘察工单详情' : '编辑勘察派单') : '新建勘察派单')); const surveyNo = ref(''); +// ==================== 只读视图数据 ==================== +const STATUS_META: Record = { + [SURVEY_STATUS.PENDING_ASSIGN]: { label: '待指派', color: '#d97706', bg: 'rgba(217, 119, 6, 0.1)' }, + [SURVEY_STATUS.SURVEYING]: { label: '现场勘察中', color: '#0054cd', bg: 'rgba(0, 84, 205, 0.1)' }, + [SURVEY_STATUS.PENDING_REVIEW]: { label: '待技术复核', color: '#712ae2', bg: 'rgba(113, 42, 226, 0.1)' }, + [SURVEY_STATUS.ARCHIVED]: { label: '已归档', color: '#00685f', bg: 'rgba(0, 104, 95, 0.1)' }, + [SURVEY_STATUS.CANCELED]: { label: '已取消', color: '#94a3b8', bg: 'rgba(148, 163, 184, 0.15)' }, +}; + +const statusMeta = computed(() => (detailRecord.value ? STATUS_META[detailRecord.value.status] || null : null)); +const roPhotos = computed(() => (detailRecord.value?.photoUrls || []).filter((u) => !!u)); +const roSopResults = computed(() => detailRecord.value?.sopResults || []); +const roSopChecked = computed(() => roSopResults.value.filter((r) => r.checked).length); +const roProgressDone = computed(() => Number(detailRecord.value?.progressDone) || 0); +const roProgressTotal = computed(() => Number(detailRecord.value?.progressTotal) || 0); +const roProgressPercent = computed(() => { + const total = roProgressTotal.value; + if (total <= 0) return 0; + return Math.min(100, Math.round((roProgressDone.value / total) * 100)); +}); +const roActualFee = computed(() => money(detailRecord.value?.actualFee ?? detailRecord.value?.estimatedFee)); +const roHasNotes = computed(() => { + const d = detailRecord.value; + return !!(d?.reviewRemark || d?.cancelReason || d?.remark); +}); + +function money(value?: number | string) { + if (value === undefined || value === null || value === '') return '—'; + const num = Number(value); + if (Number.isNaN(num)) return '—'; + return `¥ ${num.toFixed(2)}`; +} + +function formatDateTime(value?: string) { + return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—'; +} + // ==================== 表单状态 ==================== const loading = ref(false); const submitting = ref(false); @@ -487,6 +682,7 @@ function handleCustomerChange() { } function selectWorker(w: WorkerRecommend) { + if (isReadonly.value) return; formState.workerId = w.id; } @@ -575,6 +771,7 @@ async function loadDetail() { const res = await surveyOrderApi.getDetail(editId.value); if (res.code === 200 && res.data) { const d = res.data; + detailRecord.value = d; surveyNo.value = d.surveyNo || ''; formState.customerId = d.customerId; formState.taskTitle = d.taskTitle || ''; @@ -791,6 +988,28 @@ onMounted(async () => { .header-left { min-width: 0; } +.title-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} +.status-pill { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 10px; + border-radius: 9999px; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} +.status-pill .dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} .main-title { margin: 0; font-size: 23px; @@ -813,6 +1032,10 @@ onMounted(async () => { font-size: 12px; font-weight: 700; } +.step-badge.ro { + background: rgba(148, 163, 184, 0.16); + color: #5b6b7e; +} /* 步骤条 */ .step-bar { @@ -1026,6 +1249,12 @@ onMounted(async () => { .unit-pre { font-weight: 400; } +/* 只读态:保留原有布局,仅弱化控件视觉 */ +.ipt:disabled { + background: #f8fafc; + color: #5b6b7e; + cursor: not-allowed; +} /* 客户 chips */ .customer-chips { @@ -1086,6 +1315,16 @@ onMounted(async () => { color: #fff; box-shadow: 0 2px 8px rgba(0, 84, 205, 0.28); } +.mode-pill:disabled { + cursor: not-allowed; +} +.mode-pill:disabled:hover { + background: #eef4ff; +} +.mode-pill:disabled.active, +.mode-pill:disabled.active:hover { + background: #0054cd; +} /* ---------- 第 2 步:SOP ---------- */ .tmpl-box { @@ -1230,6 +1469,9 @@ onMounted(async () => { background: rgba(29, 111, 255, 0.08); opacity: 1; } +.cand-card.readonly { + cursor: default; +} .cand-avatar { width: 48px; height: 48px; @@ -1388,6 +1630,194 @@ onMounted(async () => { color: #0054cd; } +/* ---------- 只读:测绘成果与复核记录 ---------- */ +.ro-body { + display: flex; + flex-direction: column; + gap: 16px; + padding: 18px 20px 20px; +} +.ro-facts { + display: flex; + align-items: stretch; + gap: 12px; + flex-wrap: wrap; +} +.ro-fact { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 120px; + padding: 10px 16px; + border: 1px solid rgba(219, 228, 238, 0.5); + border-radius: 10px; + background: #eef4ff; +} +.ro-k { + font-size: 12px; + color: #5b6b7e; +} +.ro-v { + font-size: 15px; + font-weight: 700; + color: #0d1c2c; +} +.ro-progress { + display: flex; + flex-direction: column; + gap: 8px; +} +.ro-progress-head { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + font-weight: 700; + color: #5b6b7e; +} +.ro-track { + height: 8px; + border-radius: 9999px; + background: #e5efff; + overflow: hidden; +} +.ro-fill { + height: 100%; + border-radius: 9999px; + background: linear-gradient(90deg, #0054cd, #4a8cf7); + transition: width 0.3s; +} +.ro-block { + display: flex; + flex-direction: column; + gap: 10px; + padding-top: 14px; + border-top: 1px solid #eef2f8; +} +.ro-block-title { + font-size: 13px; + font-weight: 700; + color: #0d1c2c; +} +.ro-empty { + padding: 16px; + text-align: center; + font-size: 12px; + color: #94a3b8; + background: #f8fafc; + border-radius: 10px; +} +.ro-list { + display: flex; + flex-direction: column; + gap: 8px; +} +.ro-sop-row { + display: grid; + grid-template-columns: 20px 1fr 88px 1.2fr; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 8px; + background: #f8fafc; + font-size: 12.5px; + color: #0d1c2c; +} +.ro-sop-check { + width: 16px; + height: 16px; + border: 1px solid #dbe4ee; + border-radius: 4px; + background: #fff; + color: #fff; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; +} +.ro-sop-check.done { + background: #00685f; + border-color: #00685f; +} +.ro-sop-name { + font-weight: 700; +} +.ro-sop-tag { + font-size: 11.5px; + font-weight: 700; + color: #712ae2; +} +.ro-sop-value { + font-size: 11.5px; + color: #5b6b7e; + word-break: break-all; +} +.ro-sop-thumbs { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.ro-sop-thumb :deep(img) { + object-fit: cover; + border-radius: 6px; + border: 1px solid #dbe4ee; +} +.ro-photo-list { + display: flex; + flex-wrap: wrap; + gap: 10px; +} +.ro-photo :deep(img) { + object-fit: cover; + border-radius: 8px; +} +.ro-notes { + display: flex; + flex-direction: column; + gap: 10px; +} +.ro-note { + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px 14px; + border-left: 3px solid #dbe4ee; + border-radius: 10px; + background: #f8fafc; +} +.ro-note.review { + border-left-color: #0054cd; + background: rgba(29, 111, 255, 0.05); +} +.ro-note.cancel { + border-left-color: #dc2626; + background: rgba(220, 38, 38, 0.05); +} +.ro-note.plain { + border-left-color: #94a3b8; +} +.ro-note-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} +.ro-note-tag { + font-size: 11.5px; + font-weight: 700; + color: #5b6b7e; +} +.ro-note-time { + font-size: 11.5px; + color: #94a3b8; +} +.ro-note-text { + font-size: 13px; + line-height: 1.7; + color: #0d1c2c; + white-space: pre-wrap; +} + /* ---------- 底部操作条 ---------- */ .action-bar { position: sticky; diff --git a/client/src/views/project/survey/index.vue b/client/src/views/project/survey/index.vue index c259aa2..e67f7ad 100644 --- a/client/src/views/project/survey/index.vue +++ b/client/src/views/project/survey/index.vue @@ -166,7 +166,7 @@