refactor(survey): 重构工单详情页与侧边栏样式,统一详情查看流程

1.  重构勘察工单详情逻辑,将弹窗详情改为直接跳转编辑页,支持只读查看
2.  更新API数据结构,新增回执URL与照片URL字段支持
3.  优化全局滚动条样式为iOS风格,适配深色侧边栏
4.  修复侧边栏布局问题,添加flex布局与滚动容器适配
5.  新增只读详情页面,展示测绘成果与复核记录
This commit is contained in:
andyesfly committed 2026-09-19 15:12:11 +08:00
1 parent 6b00f61550
commit 430e5c24f8
5 files changed
+520 -228

No files matched your search

+8 -183
View File
@@ -166,7 +166,7 @@
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'surveyNo'">
<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
v-if="priorityMeta(record.priority)"
class="chip"
@@ -253,12 +253,10 @@
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
<template #overlay>
<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="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 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>
</template>
@@ -407,67 +405,6 @@
</a-form>
</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>
</template>
@@ -488,8 +425,6 @@ import {
ThunderboltOutlined,
FileSearchOutlined,
CheckCircleOutlined,
CheckOutlined,
CloseOutlined,
} from '@ant-design/icons-vue';
import dayjs from 'dayjs';
import {
@@ -773,18 +708,16 @@ function handlePrimary(record: SurveyOrderInfo) {
const action = primaryAction(record);
if (!action) return;
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 message.info('生成项目方案功能暂未开放');
}
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 === 'urge') return handleUrge(record);
if (key === 'photos') return openDetail(record);
if (key === 'report') return message.info('查看勘察报告功能暂未开放');
if (key === 'detail') return openDetail(record);
if (key === 'cancel') return openCancel(record);
}
@@ -946,18 +879,10 @@ async function handleCancelSubmit() {
}
}
// ==================== 详情 ====================
const detailVisible = ref(false);
async function openDetail(record: SurveyOrderInfo) {
currentRecord.value = record;
detailVisible.value = true;
try {
const res = await surveyOrderApi.getDetail(record.id);
currentRecord.value = res.data;
} catch {
// 详情获取失败时保留列表行数据
}
// ==================== 详情 / 编辑 ====================
// 详情与编辑统一进编辑页;不可编辑状态由 form.vue 按 status 自动降级为只读
function goDetail(record: SurveyOrderInfo) {
router.push({ name: 'ProjectSurveyEdit', params: { id: record.id } });
}
// ==================== 展示辅助 ====================
@@ -984,11 +909,6 @@ function maskPhone(phone?: string) {
if (phone.length < 7) return phone;
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>
<style scoped>
@@ -1529,101 +1449,6 @@ function receiptLabel(type?: string) {
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 {
font-family: Consolas, Menlo, monospace;
color: #0054cd;