refactor(survey): 重构工单详情页与侧边栏样式,统一详情查看流程
1. 重构勘察工单详情逻辑,将弹窗详情改为直接跳转编辑页,支持只读查看 2. 更新API数据结构,新增回执URL与照片URL字段支持 3. 优化全局滚动条样式为iOS风格,适配深色侧边栏 4. 修复侧边栏布局问题,添加flex布局与滚动容器适配 5. 新增只读详情页面,展示测绘成果与复核记录
This commit is contained in:
1 parent
6b00f61550
commit
430e5c24f8
5 files changed
+499
-207
No files matched your search
@@ -78,7 +78,10 @@ export interface SurveyOrderSopResult {
|
|||||||
surveyOrderId: number;
|
surveyOrderId: number;
|
||||||
sopItemId: number;
|
sopItemId: number;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
receiptValue?: string;
|
// 回执内容:photo/meter 类为照片对象 key 数组,form 类为单元素文本值数组
|
||||||
|
receiptValue?: string[];
|
||||||
|
// 详情/回执接口附带:照片类回执的预签名访问 URL(与 receiptValue 顺序一致)
|
||||||
|
receiptUrls?: string[];
|
||||||
remark?: string;
|
remark?: string;
|
||||||
checkedAt?: string;
|
checkedAt?: string;
|
||||||
// 由后端 join 的检查项信息
|
// 由后端 join 的检查项信息
|
||||||
@@ -127,6 +130,8 @@ export interface SurveyOrderInfo {
|
|||||||
estimatedFee?: number | string;
|
estimatedFee?: number | string;
|
||||||
actualFee?: number | string;
|
actualFee?: number | string;
|
||||||
photos?: string[];
|
photos?: string[];
|
||||||
|
// 详情附带:现场照片预签名访问 URL(与 photos 顺序一致)
|
||||||
|
photoUrls?: string[];
|
||||||
reviewRemark?: string;
|
reviewRemark?: string;
|
||||||
reviewedAt?: string;
|
reviewedAt?: string;
|
||||||
cancelReason?: string;
|
cancelReason?: string;
|
||||||
|
|||||||
@@ -265,28 +265,45 @@ ol {
|
|||||||
animation: slideInUp 0.3s ease-out;
|
animation: slideInUp 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 滚动条美化 ====================
|
// ==================== 滚动条美化(iOS 风格) ====================
|
||||||
|
// iOS 风格特征:极细(4px)、全圆角胶囊形、无轨道、半透明 thumb 悬浮在内容上
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 4px;
|
||||||
height: 8px;
|
height: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #f1f1f1;
|
background: transparent;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #c1c1c1;
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 4px;
|
border-radius: 999px;
|
||||||
|
|
||||||
&:hover {
|
&: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
|
// Firefox
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
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;
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible :width="240" :collapsed-width="80"
|
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible :width="240" :collapsed-width="80"
|
||||||
theme="dark" class="sider">
|
theme="dark" class="sider">
|
||||||
|
<div class="sider-inner">
|
||||||
<!-- Logo区域 -->
|
<!-- Logo区域 -->
|
||||||
<div class="logo" @click="$router.push('/dashboard')">
|
<div class="logo" @click="$router.push('/dashboard')">
|
||||||
<img src="@/assets/vue.svg" alt="logo" class="logo-img" />
|
<img src="@/assets/vue.svg" alt="logo" class="logo-img" />
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
<div class="collapse-btn" @click="toggleCollapse">
|
<div class="collapse-btn" @click="toggleCollapse">
|
||||||
<component :is="collapsed ? MenuUnfoldOutlined : MenuFoldOutlined" />
|
<component :is="collapsed ? MenuUnfoldOutlined : MenuFoldOutlined" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</a-layout-sider>
|
</a-layout-sider>
|
||||||
|
|
||||||
<a-layout>
|
<a-layout>
|
||||||
@@ -301,7 +303,19 @@ function handleUserMenuClick({ key }: { key: string }) {
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
|
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 {
|
.logo {
|
||||||
|
flex-shrink: 0;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -336,6 +350,10 @@ function handleUserMenuClick({ key }: { key: string }) {
|
|||||||
|
|
||||||
:deep(.ant-menu) {
|
:deep(.ant-menu) {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
// iconfont 图标统一:该图标集为彩色字体,颜色写死在字形里,无法用 color 覆盖,
|
// iconfont 图标统一:该图标集为彩色字体,颜色写死在字形里,无法用 color 覆盖,
|
||||||
@@ -363,10 +381,7 @@ function handleUserMenuClick({ key }: { key: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.collapse-btn {
|
.collapse-btn {
|
||||||
position: absolute;
|
flex-shrink: 0;
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 48px;
|
height: 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -15,10 +15,27 @@
|
|||||||
<div class="header-card">
|
<div class="header-card">
|
||||||
<div class="header-top">
|
<div class="header-top">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1 class="main-title">现场勘察派单向导</h1>
|
<div class="title-row">
|
||||||
<p class="desc">标准化派发配电房现场测绘任务,向共享电工推单并回传拓扑参数</p>
|
<h1 class="main-title">{{ isReadonly ? '勘察工单详情' : '现场勘察派单向导' }}</h1>
|
||||||
|
<span
|
||||||
|
v-if="statusMeta"
|
||||||
|
class="status-pill"
|
||||||
|
:style="{ color: statusMeta.color, background: statusMeta.bg }"
|
||||||
|
>
|
||||||
|
<i class="dot" :style="{ background: statusMeta.color }"></i>
|
||||||
|
{{ statusMeta.label }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="step-badge">STEP 0{{ currentStep }} / 03</span>
|
<p class="desc">
|
||||||
|
{{
|
||||||
|
isReadonly
|
||||||
|
? '该工单已流转至复核 / 归档环节,信息仅供查看;如需调整请联系技术负责人'
|
||||||
|
: '标准化派发配电房现场测绘任务,向共享电工推单并回传拓扑参数'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span v-if="isReadonly" class="step-badge ro">只读查看</span>
|
||||||
|
<span v-else class="step-badge">STEP 0{{ currentStep }} / 03</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step-bar">
|
<div class="step-bar">
|
||||||
@@ -67,6 +84,7 @@
|
|||||||
size="large"
|
size="large"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
option-filter-prop="label"
|
option-filter-prop="label"
|
||||||
|
:disabled="isReadonly"
|
||||||
:options="customerSelectOptions"
|
:options="customerSelectOptions"
|
||||||
@change="handleCustomerChange"
|
@change="handleCustomerChange"
|
||||||
/>
|
/>
|
||||||
@@ -90,6 +108,7 @@
|
|||||||
:class="{ err: errors.taskTitle }"
|
:class="{ err: errors.taskTitle }"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="128"
|
maxlength="128"
|
||||||
|
:disabled="isReadonly"
|
||||||
placeholder="如:实训楼智能配电改造一期工程现场测绘"
|
placeholder="如:实训楼智能配电改造一期工程现场测绘"
|
||||||
@input="clearError('taskTitle')"
|
@input="clearError('taskTitle')"
|
||||||
/>
|
/>
|
||||||
@@ -104,6 +123,7 @@
|
|||||||
:class="{ err: errors.siteLocation }"
|
:class="{ err: errors.siteLocation }"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="255"
|
maxlength="255"
|
||||||
|
:disabled="isReadonly"
|
||||||
placeholder="如:实训主楼 1F低压室 / 2~3F强电井"
|
placeholder="如:实训主楼 1F低压室 / 2~3F强电井"
|
||||||
@input="clearError('siteLocation')"
|
@input="clearError('siteLocation')"
|
||||||
/>
|
/>
|
||||||
@@ -117,6 +137,7 @@
|
|||||||
class="ipt mono-ipt"
|
class="ipt mono-ipt"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="128"
|
maxlength="128"
|
||||||
|
:disabled="isReadonly"
|
||||||
placeholder="如:380V / 400A MCCB (SCB10-630kVA)"
|
placeholder="如:380V / 400A MCCB (SCB10-630kVA)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,6 +151,7 @@
|
|||||||
:class="{ err: errors.loopCount }"
|
:class="{ err: errors.loopCount }"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
|
:disabled="isReadonly"
|
||||||
@input="clearError('loopCount')"
|
@input="clearError('loopCount')"
|
||||||
/>
|
/>
|
||||||
<span class="unit">路</span>
|
<span class="unit">路</span>
|
||||||
@@ -141,7 +163,7 @@
|
|||||||
<label class="field-label"><span>要求期望到场时限</span></label>
|
<label class="field-label"><span>要求期望到场时限</span></label>
|
||||||
<div class="inline-2">
|
<div class="inline-2">
|
||||||
<span class="unit unit-pre">接单后</span>
|
<span class="unit unit-pre">接单后</span>
|
||||||
<input v-model.number="formState.expectMinutes" class="ipt mono-ipt" type="number" min="0" />
|
<input v-model.number="formState.expectMinutes" class="ipt mono-ipt" type="number" min="0" :disabled="isReadonly" />
|
||||||
<span class="unit">分钟内</span>
|
<span class="unit">分钟内</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,6 +175,7 @@
|
|||||||
class="ipt"
|
class="ipt"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="128"
|
maxlength="128"
|
||||||
|
:disabled="isReadonly"
|
||||||
placeholder="如:程建业 (项目主管) - 138****9201"
|
placeholder="如:程建业 (项目主管) - 138****9201"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,6 +187,7 @@
|
|||||||
format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
||||||
size="large"
|
size="large"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:disabled="isReadonly"
|
||||||
:placeholder="['开始日期', '结束日期']"
|
:placeholder="['开始日期', '结束日期']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,6 +201,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="mode-pill"
|
class="mode-pill"
|
||||||
:class="{ active: formState.priority === p.value }"
|
:class="{ active: formState.priority === p.value }"
|
||||||
|
:disabled="isReadonly"
|
||||||
@click="formState.priority = p.value"
|
@click="formState.priority = p.value"
|
||||||
>
|
>
|
||||||
{{ p.label }}
|
{{ p.label }}
|
||||||
@@ -194,7 +219,7 @@
|
|||||||
<FileDoneOutlined class="sec-icon clr-secondary" />
|
<FileDoneOutlined class="sec-icon clr-secondary" />
|
||||||
<h2>第 2 步:勘察技术要点与回执标准</h2>
|
<h2>第 2 步:勘察技术要点与回执标准</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="tmpl-box">
|
<div v-if="!isReadonly" class="tmpl-box">
|
||||||
<span class="head-hint">SOP规范模板:</span>
|
<span class="head-hint">SOP规范模板:</span>
|
||||||
<button type="button" class="tmpl-btn" @click="applySopTemplate">
|
<button type="button" class="tmpl-btn" @click="applySopTemplate">
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
@@ -216,7 +241,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sop-add" @click="handleAddCustomItem">
|
<div v-if="!isReadonly" class="sop-add" @click="handleAddCustomItem">
|
||||||
<PlusOutlined class="sop-add-icon" />
|
<PlusOutlined class="sop-add-icon" />
|
||||||
<span>+ 自定义勘察检查项</span>
|
<span>+ 自定义勘察检查项</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,7 +267,7 @@
|
|||||||
v-for="w in workers"
|
v-for="w in workers"
|
||||||
:key="w.id"
|
:key="w.id"
|
||||||
class="cand-card"
|
class="cand-card"
|
||||||
:class="{ selected: formState.workerId === w.id }"
|
:class="{ selected: formState.workerId === w.id, readonly: isReadonly }"
|
||||||
@click="selectWorker(w)"
|
@click="selectWorker(w)"
|
||||||
>
|
>
|
||||||
<div class="cand-avatar" :class="{ muted: formState.workerId !== w.id }">
|
<div class="cand-avatar" :class="{ muted: formState.workerId !== w.id }">
|
||||||
@@ -256,7 +281,7 @@
|
|||||||
<span v-else class="cand-badge-plain">{{ w.workerTitle || '共享电工' }}</span>
|
<span v-else class="cand-badge-plain">{{ w.workerTitle || '共享电工' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<CheckCircleFilled v-if="formState.workerId === w.id" class="cand-check" />
|
<CheckCircleFilled v-if="formState.workerId === w.id" class="cand-check" />
|
||||||
<button v-else type="button" class="cand-switch" @click.stop="selectWorker(w)">改选此人</button>
|
<button v-else-if="!isReadonly" type="button" class="cand-switch" @click.stop="selectWorker(w)">改选此人</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="cand-meta">
|
<div class="cand-meta">
|
||||||
<span class="cand-rating"><StarFilled /> {{ Number(w.rating || 0).toFixed(2) }} 分</span>
|
<span class="cand-rating"><StarFilled /> {{ Number(w.rating || 0).toFixed(2) }} 分</span>
|
||||||
@@ -290,6 +315,7 @@
|
|||||||
v-model:value="formState.slaLevel"
|
v-model:value="formState.slaLevel"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 210px"
|
style="width: 210px"
|
||||||
|
:disabled="isReadonly"
|
||||||
:options="SLA_OPTIONS"
|
:options="SLA_OPTIONS"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,13 +328,134 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ==================== 只读:测绘成果与复核记录 ==================== -->
|
||||||
|
<section v-if="isReadonly" id="wizard-step-4" class="section">
|
||||||
|
<div class="section-head">
|
||||||
|
<div class="section-title">
|
||||||
|
<span class="bar bar-tertiary"></span>
|
||||||
|
<FileDoneOutlined class="sec-icon clr-tertiary" />
|
||||||
|
<h2>测绘成果与复核记录</h2>
|
||||||
|
</div>
|
||||||
|
<span class="head-hint">现场回执数据 · 仅供查看</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ro-body">
|
||||||
|
<!-- 成果概览 -->
|
||||||
|
<div class="ro-facts">
|
||||||
|
<div class="ro-fact">
|
||||||
|
<span class="ro-k">实际劳务费</span>
|
||||||
|
<span class="ro-v mono">{{ roActualFee }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ro-fact">
|
||||||
|
<span class="ro-k">测绘进度</span>
|
||||||
|
<span class="ro-v mono">{{ roProgressDone }} / {{ roProgressTotal }} 路</span>
|
||||||
|
</div>
|
||||||
|
<div class="ro-fact">
|
||||||
|
<span class="ro-k">SOP 回执</span>
|
||||||
|
<span class="ro-v mono">{{ roSopChecked }} / {{ roSopResults.length }} 项</span>
|
||||||
|
</div>
|
||||||
|
<div class="ro-fact">
|
||||||
|
<span class="ro-k">现场照片</span>
|
||||||
|
<span class="ro-v mono">{{ roPhotos.length }} 张</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="detailRecord?.workerName" class="ro-fact">
|
||||||
|
<span class="ro-k">指派电工</span>
|
||||||
|
<span class="ro-v">{{ detailRecord.workerName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 现场测绘完成度 -->
|
||||||
|
<div class="ro-progress">
|
||||||
|
<div class="ro-progress-head">
|
||||||
|
<span>现场测绘完成度</span>
|
||||||
|
<span class="mono">{{ roProgressPercent }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="ro-track">
|
||||||
|
<div class="ro-fill" :style="{ width: `${roProgressPercent}%` }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SOP 回执明细 -->
|
||||||
|
<div class="ro-block">
|
||||||
|
<div class="ro-block-title">SOP 回执明细</div>
|
||||||
|
<div v-if="roSopResults.length === 0" class="ro-empty">暂无 SOP 回执记录</div>
|
||||||
|
<div v-else class="ro-list">
|
||||||
|
<div v-for="(r, idx) in roSopResults" :key="r.id || idx" class="ro-sop-row">
|
||||||
|
<span class="ro-sop-check" :class="{ done: r.checked }">
|
||||||
|
<CheckOutlined v-if="r.checked" />
|
||||||
|
</span>
|
||||||
|
<span class="ro-sop-name">{{ r.itemName || r.sopItem?.itemName || `检查项 #${r.sopItemId}` }}</span>
|
||||||
|
<span class="ro-sop-tag">{{ receiptTypeLabel(r.receiptType || r.sopItem?.receiptType) }}</span>
|
||||||
|
<div v-if="r.receiptUrls && r.receiptUrls.length" class="ro-sop-thumbs">
|
||||||
|
<a-image v-for="(url, i) in r.receiptUrls" :key="i" :src="url" :width="56" :height="42" class="ro-sop-thumb" />
|
||||||
|
</div>
|
||||||
|
<span v-else class="ro-sop-value mono">{{ r.receiptValue?.length ? r.receiptValue.join('、') : r.remark || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 现场取证照片 -->
|
||||||
|
<div class="ro-block">
|
||||||
|
<div class="ro-block-title">现场取证照片</div>
|
||||||
|
<div v-if="roPhotos.length === 0" class="ro-empty">暂无现场照片</div>
|
||||||
|
<div v-else class="ro-photo-list">
|
||||||
|
<a-image-preview-group>
|
||||||
|
<a-image
|
||||||
|
v-for="(url, idx) in roPhotos"
|
||||||
|
:key="idx"
|
||||||
|
:src="url"
|
||||||
|
:width="120"
|
||||||
|
:height="90"
|
||||||
|
class="ro-photo"
|
||||||
|
/>
|
||||||
|
</a-image-preview-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复核 / 取消 / 备注 -->
|
||||||
|
<div class="ro-block">
|
||||||
|
<div class="ro-block-title">复核与处理记录</div>
|
||||||
|
<div v-if="!roHasNotes" class="ro-empty">暂无复核与备注记录</div>
|
||||||
|
<div v-else class="ro-notes">
|
||||||
|
<div v-if="detailRecord?.reviewRemark" class="ro-note review">
|
||||||
|
<div class="ro-note-head">
|
||||||
|
<span class="ro-note-tag">技术复核</span>
|
||||||
|
<span v-if="detailRecord?.reviewedAt" class="ro-note-time mono">
|
||||||
|
{{ formatDateTime(detailRecord.reviewedAt) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="ro-note-text">{{ detailRecord.reviewRemark }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="detailRecord?.cancelReason" class="ro-note cancel">
|
||||||
|
<div class="ro-note-head">
|
||||||
|
<span class="ro-note-tag">取消原因</span>
|
||||||
|
</div>
|
||||||
|
<div class="ro-note-text">{{ detailRecord.cancelReason }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="detailRecord?.remark" class="ro-note plain">
|
||||||
|
<div class="ro-note-head">
|
||||||
|
<span class="ro-note-tag">派单备注</span>
|
||||||
|
</div>
|
||||||
|
<div class="ro-note-text">{{ detailRecord.remark }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- ==================== 底部操作条 ==================== -->
|
<!-- ==================== 底部操作条 ==================== -->
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<div class="action-hint">
|
<div class="action-hint">
|
||||||
<CheckCircleFilled class="hint-icon" />
|
<CheckCircleFilled class="hint-icon" />
|
||||||
<span>向导已填报 {{ filledCount }}/3 核心项 · 数据直连共享电工协同网</span>
|
<span v-if="isReadonly">工单已流转至「{{ statusMeta?.label || '只读' }}」 · 当前为只读查看</span>
|
||||||
|
<span v-else>向导已填报 {{ filledCount }}/3 核心项 · 数据直连共享电工协同网</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-btns">
|
<div class="action-btns">
|
||||||
|
<a-button v-if="isReadonly" @click="goLedger">
|
||||||
|
<template #icon><ArrowLeftOutlined /></template>
|
||||||
|
返回勘察台账
|
||||||
|
</a-button>
|
||||||
|
<template v-else>
|
||||||
<a-button @click="goLedger">
|
<a-button @click="goLedger">
|
||||||
<template #icon><ArrowLeftOutlined /></template>
|
<template #icon><ArrowLeftOutlined /></template>
|
||||||
上一步
|
上一步
|
||||||
@@ -321,6 +468,7 @@
|
|||||||
<template #icon><SendOutlined /></template>
|
<template #icon><SendOutlined /></template>
|
||||||
确认并推送共享电工派单
|
确认并推送共享电工派单
|
||||||
</a-button>
|
</a-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -348,6 +496,7 @@ import {
|
|||||||
surveyOrderApi,
|
surveyOrderApi,
|
||||||
SOP_TEMPLATE_CODE,
|
SOP_TEMPLATE_CODE,
|
||||||
SOP_TEMPLATE_LABEL,
|
SOP_TEMPLATE_LABEL,
|
||||||
|
SURVEY_STATUS,
|
||||||
SURVEY_PRIORITY,
|
SURVEY_PRIORITY,
|
||||||
SURVEY_PRIORITY_OPTIONS,
|
SURVEY_PRIORITY_OPTIONS,
|
||||||
SURVEY_BASE_FEE,
|
SURVEY_BASE_FEE,
|
||||||
@@ -355,6 +504,7 @@ import {
|
|||||||
type SurveySopItem,
|
type SurveySopItem,
|
||||||
type SurveyQuote,
|
type SurveyQuote,
|
||||||
type SurveyOrderCreateParams,
|
type SurveyOrderCreateParams,
|
||||||
|
type SurveyOrderInfo,
|
||||||
} from '@/api/surveyOrder';
|
} from '@/api/surveyOrder';
|
||||||
import { workerApi, type WorkerRecommend } from '@/api/worker';
|
import { workerApi, type WorkerRecommend } from '@/api/worker';
|
||||||
import { customerApi, type CustomerInfo } from '@/api/customer';
|
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 editId = computed(() => Number(route.params.id || 0));
|
||||||
const isEdit = computed(() => editId.value > 0);
|
const isEdit = computed(() => editId.value > 0);
|
||||||
const pageTitle = computed(() => (isEdit.value ? '编辑勘察派单' : '新建勘察派单'));
|
|
||||||
|
// 详情与编辑共用本页:status 0 待指派 / 1 现场勘察中 可编辑,其余状态进入只读查看
|
||||||
|
const detailRecord = ref<SurveyOrderInfo | null>(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 surveyNo = ref('');
|
||||||
|
|
||||||
|
// ==================== 只读视图数据 ====================
|
||||||
|
const STATUS_META: Record<number, { label: string; color: string; bg: string }> = {
|
||||||
|
[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 loading = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
@@ -487,6 +682,7 @@ function handleCustomerChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectWorker(w: WorkerRecommend) {
|
function selectWorker(w: WorkerRecommend) {
|
||||||
|
if (isReadonly.value) return;
|
||||||
formState.workerId = w.id;
|
formState.workerId = w.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,6 +771,7 @@ async function loadDetail() {
|
|||||||
const res = await surveyOrderApi.getDetail(editId.value);
|
const res = await surveyOrderApi.getDetail(editId.value);
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
const d = res.data;
|
const d = res.data;
|
||||||
|
detailRecord.value = d;
|
||||||
surveyNo.value = d.surveyNo || '';
|
surveyNo.value = d.surveyNo || '';
|
||||||
formState.customerId = d.customerId;
|
formState.customerId = d.customerId;
|
||||||
formState.taskTitle = d.taskTitle || '';
|
formState.taskTitle = d.taskTitle || '';
|
||||||
@@ -791,6 +988,28 @@ onMounted(async () => {
|
|||||||
.header-left {
|
.header-left {
|
||||||
min-width: 0;
|
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 {
|
.main-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 23px;
|
font-size: 23px;
|
||||||
@@ -813,6 +1032,10 @@ onMounted(async () => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
.step-badge.ro {
|
||||||
|
background: rgba(148, 163, 184, 0.16);
|
||||||
|
color: #5b6b7e;
|
||||||
|
}
|
||||||
|
|
||||||
/* 步骤条 */
|
/* 步骤条 */
|
||||||
.step-bar {
|
.step-bar {
|
||||||
@@ -1026,6 +1249,12 @@ onMounted(async () => {
|
|||||||
.unit-pre {
|
.unit-pre {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
/* 只读态:保留原有布局,仅弱化控件视觉 */
|
||||||
|
.ipt:disabled {
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #5b6b7e;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* 客户 chips */
|
/* 客户 chips */
|
||||||
.customer-chips {
|
.customer-chips {
|
||||||
@@ -1086,6 +1315,16 @@ onMounted(async () => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 2px 8px rgba(0, 84, 205, 0.28);
|
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 ---------- */
|
/* ---------- 第 2 步:SOP ---------- */
|
||||||
.tmpl-box {
|
.tmpl-box {
|
||||||
@@ -1230,6 +1469,9 @@ onMounted(async () => {
|
|||||||
background: rgba(29, 111, 255, 0.08);
|
background: rgba(29, 111, 255, 0.08);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
.cand-card.readonly {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
.cand-avatar {
|
.cand-avatar {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
@@ -1388,6 +1630,194 @@ onMounted(async () => {
|
|||||||
color: #0054cd;
|
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 {
|
.action-bar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|||||||
@@ -166,7 +166,7 @@
|
|||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'surveyNo'">
|
<template v-if="column.key === 'surveyNo'">
|
||||||
<div class="no-cell">
|
<div class="no-cell">
|
||||||
<span class="survey-no" @click="openDetail(record)">{{ record.surveyNo }}</span>
|
<span class="survey-no" @click="goDetail(record)">{{ record.surveyNo }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="priorityMeta(record.priority)"
|
v-if="priorityMeta(record.priority)"
|
||||||
class="chip"
|
class="chip"
|
||||||
@@ -253,12 +253,10 @@
|
|||||||
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
|
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="({ key }) => handleMore(key as string, record)">
|
<a-menu @click="({ key }) => handleMore(key as string, record)">
|
||||||
<a-menu-item v-if="canEdit(record)" key="edit">编辑</a-menu-item>
|
|
||||||
<a-menu-item v-if="record.status === SURVEY_STATUS.SURVEYING" key="reassign">改派</a-menu-item>
|
<a-menu-item v-if="record.status === SURVEY_STATUS.SURVEYING" key="reassign">改派</a-menu-item>
|
||||||
<a-menu-item v-if="canUrge(record)" key="urge">催办</a-menu-item>
|
<a-menu-item v-if="canUrge(record)" key="urge">催办</a-menu-item>
|
||||||
<a-menu-item v-if="record.status === SURVEY_STATUS.PENDING_REVIEW" key="photos">取证照片</a-menu-item>
|
|
||||||
<a-menu-item v-if="record.status === SURVEY_STATUS.ARCHIVED" key="report">查看报告</a-menu-item>
|
<a-menu-item v-if="record.status === SURVEY_STATUS.ARCHIVED" key="report">查看报告</a-menu-item>
|
||||||
<a-menu-item key="detail">详情</a-menu-item>
|
<a-menu-item key="open">{{ canEdit(record) ? '编辑' : '详情' }}</a-menu-item>
|
||||||
<a-menu-item v-if="canCancel(record)" key="cancel" danger>取消</a-menu-item>
|
<a-menu-item v-if="canCancel(record)" key="cancel" danger>取消</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
@@ -407,67 +405,6 @@
|
|||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<!-- 工单详情 -->
|
|
||||||
<a-modal v-model:open="detailVisible" title="勘察工单详情" :footer="null" width="820px">
|
|
||||||
<div v-if="currentRecord" class="detail">
|
|
||||||
<div class="detail-grid">
|
|
||||||
<div class="detail-item"><span class="k">工单编号</span><span class="v mono">{{ currentRecord.surveyNo }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">当前状态</span><span class="v">{{ statusMeta(currentRecord.status).label }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">关联客户</span><span class="v">{{ currentRecord.customerName || currentRecord.customer?.customerName || '-' }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">勘察标题</span><span class="v">{{ currentRecord.taskTitle || '-' }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">物理区域</span><span class="v">{{ currentRecord.siteLocation || '-' }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">回路 / 规格</span><span class="v">{{ currentRecord.loopCount ?? 0 }} 路 · {{ currentRecord.loopSpec || '—' }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">责任业务员</span><span class="v">{{ currentRecord.salesmanName || '-' }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">指派电工</span><span class="v">{{ currentRecord.workerName || '待指派' }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">技术联系人</span><span class="v">{{ currentRecord.techContact || '-' }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">期望到场</span><span class="v">{{ formatDateTime(currentRecord.expectArrivalAt) }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">预估劳务费</span><span class="v">{{ money(currentRecord.estimatedFee) }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">SLA 等级</span><span class="v">{{ currentRecord.slaLevel || '-' }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">测绘进度</span><span class="v">{{ currentRecord.progressDone ?? 0 }} / {{ currentRecord.progressTotal ?? 0 }}</span></div>
|
|
||||||
<div class="detail-item"><span class="k">催办次数</span><span class="v">{{ currentRecord.urgeCount ?? 0 }}</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="currentRecord.status === SURVEY_STATUS.PENDING_REVIEW || currentRecord.status === SURVEY_STATUS.ARCHIVED" class="detail-block">
|
|
||||||
<div class="detail-label">SOP 测绘成果回执</div>
|
|
||||||
<div v-if="currentRecord.sopResults?.length" class="sop-list">
|
|
||||||
<div v-for="item in currentRecord.sopResults" :key="item.id" class="sop-row">
|
|
||||||
<span class="sop-check" :class="{ ok: item.checked }">
|
|
||||||
<CheckOutlined v-if="item.checked" />
|
|
||||||
<CloseOutlined v-else />
|
|
||||||
</span>
|
|
||||||
<span class="sop-name">{{ item.itemName || item.sopItem?.itemName || '—' }}</span>
|
|
||||||
<span class="sop-receipt">{{ receiptLabel(item.receiptType || item.sopItem?.receiptType) }}</span>
|
|
||||||
<span class="sop-value">{{ item.receiptValue || item.remark || '—' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="detail-text">—</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="detail-block">
|
|
||||||
<div class="detail-label">现场取证照片</div>
|
|
||||||
<div v-if="currentRecord.photos?.length" class="photo-list">
|
|
||||||
<a-image-preview-group>
|
|
||||||
<a-image
|
|
||||||
v-for="(url, idx) in currentRecord.photos"
|
|
||||||
:key="idx"
|
|
||||||
:src="url"
|
|
||||||
:width="110"
|
|
||||||
:height="80"
|
|
||||||
style="object-fit: cover; border-radius: 8px"
|
|
||||||
/>
|
|
||||||
</a-image-preview-group>
|
|
||||||
</div>
|
|
||||||
<div v-else class="detail-text">—</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="currentRecord.reviewRemark || currentRecord.cancelReason || currentRecord.remark" class="detail-block">
|
|
||||||
<div class="detail-label">复核 / 取消 / 备注</div>
|
|
||||||
<div v-if="currentRecord.reviewRemark" class="detail-text">复核意见:{{ currentRecord.reviewRemark }}</div>
|
|
||||||
<div v-if="currentRecord.cancelReason" class="detail-text">取消原因:{{ currentRecord.cancelReason }}</div>
|
|
||||||
<div v-if="currentRecord.remark" class="detail-text">备注:{{ currentRecord.remark }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -488,8 +425,6 @@ import {
|
|||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
FileSearchOutlined,
|
FileSearchOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
CheckOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
@@ -773,18 +708,16 @@ function handlePrimary(record: SurveyOrderInfo) {
|
|||||||
const action = primaryAction(record);
|
const action = primaryAction(record);
|
||||||
if (!action) return;
|
if (!action) return;
|
||||||
if (action.label === '指派电工') return openAssign(record, false);
|
if (action.label === '指派电工') return openAssign(record, false);
|
||||||
if (action.label === '进度跟踪') return openDetail(record);
|
if (action.label === '进度跟踪') return goDetail(record);
|
||||||
if (action.label === '审核测绘成果') return openReview(record);
|
if (action.label === '审核测绘成果') return openReview(record);
|
||||||
if (action.label === '生成方案') return message.info('生成项目方案功能暂未开放');
|
if (action.label === '生成方案') return message.info('生成项目方案功能暂未开放');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMore(key: string, record: SurveyOrderInfo) {
|
function handleMore(key: string, record: SurveyOrderInfo) {
|
||||||
if (key === 'edit') return router.push({ name: 'ProjectSurveyEdit', params: { id: record.id } });
|
if (key === 'open') return goDetail(record);
|
||||||
if (key === 'reassign') return openAssign(record, true);
|
if (key === 'reassign') return openAssign(record, true);
|
||||||
if (key === 'urge') return handleUrge(record);
|
if (key === 'urge') return handleUrge(record);
|
||||||
if (key === 'photos') return openDetail(record);
|
|
||||||
if (key === 'report') return message.info('查看勘察报告功能暂未开放');
|
if (key === 'report') return message.info('查看勘察报告功能暂未开放');
|
||||||
if (key === 'detail') return openDetail(record);
|
|
||||||
if (key === 'cancel') return openCancel(record);
|
if (key === 'cancel') return openCancel(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,18 +879,10 @@ async function handleCancelSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 详情 ====================
|
// ==================== 详情 / 编辑 ====================
|
||||||
const detailVisible = ref(false);
|
// 详情与编辑统一进编辑页;不可编辑状态由 form.vue 按 status 自动降级为只读
|
||||||
|
function goDetail(record: SurveyOrderInfo) {
|
||||||
async function openDetail(record: SurveyOrderInfo) {
|
router.push({ name: 'ProjectSurveyEdit', params: { id: record.id } });
|
||||||
currentRecord.value = record;
|
|
||||||
detailVisible.value = true;
|
|
||||||
try {
|
|
||||||
const res = await surveyOrderApi.getDetail(record.id);
|
|
||||||
currentRecord.value = res.data;
|
|
||||||
} catch {
|
|
||||||
// 详情获取失败时保留列表行数据
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 展示辅助 ====================
|
// ==================== 展示辅助 ====================
|
||||||
@@ -984,11 +909,6 @@ function maskPhone(phone?: string) {
|
|||||||
if (phone.length < 7) return phone;
|
if (phone.length < 7) return phone;
|
||||||
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
|
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function receiptLabel(type?: string) {
|
|
||||||
const map: Record<string, string> = { photo: '现场照片', form: '清单表单', meter: '测仪截图' };
|
|
||||||
return type ? map[type] || type : '—';
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -1529,101 +1449,6 @@ function receiptLabel(type?: string) {
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 详情弹窗 */
|
|
||||||
.detail-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 10px 20px;
|
|
||||||
padding: 4px 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 10px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-item .k {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 84px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #5b6b7e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-item .v {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #0d1c2c;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-block {
|
|
||||||
border-top: 1px solid #eef1f6;
|
|
||||||
padding-top: 12px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #5b6b7e;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-text {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #0d1c2c;
|
|
||||||
line-height: 1.7;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sop-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sop-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 20px 1fr 84px 1.2fr;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 12.5px;
|
|
||||||
color: #0d1c2c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sop-check {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sop-check.ok {
|
|
||||||
color: #059669;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sop-name {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sop-receipt {
|
|
||||||
font-size: 11.5px;
|
|
||||||
color: #8a97a8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sop-value {
|
|
||||||
font-size: 11.5px;
|
|
||||||
color: #5b6b7e;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mono {
|
.mono {
|
||||||
font-family: Consolas, Menlo, monospace;
|
font-family: Consolas, Menlo, monospace;
|
||||||
color: #0054cd;
|
color: #0054cd;
|
||||||
|
|||||||
Reference in new issue
Block a user