feat: 重构操作按钮为下拉菜单并新增工单详情弹窗

- 重构角色、菜单、项目方案、客户账号页面的操作按钮,将多个操作整合为下拉菜单
- 统一使用 Modal.confirm 替代 Popconfirm 解决下拉菜单内锚定问题
- 新增勘察工单详情弹窗,支持查看工单完整信息
- 调整操作列宽度和对齐方式,优化表格布局
- 统一中文 locale 配置,使用 dayjs 中文 locale
This commit is contained in:
andyesfly committed 2026-09-26 14:09:57 +08:00
1 parent ae79d5157b
commit e03fb019c5
6 files changed
+496 -229

No files matched your search

+9 -1
View File
@@ -1,8 +1,16 @@
<template>
<router-view />
<a-config-provider :locale="zhCN">
<router-view />
</a-config-provider>
</template>
<script setup lang="ts">
import zhCN from "ant-design-vue/es/locale/zh_CN";
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
// 组件库与日期组件统一使用中文
dayjs.locale("zh-cn");
</script>
<style>
+54 -28
View File
@@ -124,24 +124,18 @@
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要重置该账号的密码吗?" ok-text="确定" cancel-text="取消" @confirm="handleResetPassword(record.id)">
<a-button type="link" size="small">
<template #icon><KeyOutlined /></template>
重置密码
</a-button>
</a-popconfirm>
<a-popconfirm title="确定要删除该账号吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
<div class="row-actions">
<a-button type="primary" size="small" @click="handleEdit(record)">编辑</a-button>
<a-dropdown trigger="click">
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
<template #overlay>
<a-menu @click="({ key }) => handleMore(key as string, record)">
<a-menu-item key="resetPassword">重置密码</a-menu-item>
<a-menu-item key="delete" danger>删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
</template>
@@ -218,15 +212,8 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { message } from 'ant-design-vue';
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
DeleteOutlined,
EditOutlined,
KeyOutlined,
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined, DownOutlined } from '@ant-design/icons-vue';
import { customerApi, type CustomerInfo } from '@/api/customer';
import { customerAccountApi, type CustomerAccountInfo, type AccountQuery } from '@/api/customerAccount';
import { useTableScroll } from '@/composables/useTableScroll';
@@ -284,7 +271,7 @@ const columns = [
width: 100,
},
{ title: '最近登录时间', dataIndex: 'lastLoginAt', key: 'lastLoginAt', ellipsis: true, width: 180 },
{ title: '操作', key: 'action', width: 200, fixed: 'right' as const },
{ title: '操作', key: 'action', width: 150, align: 'center' as const, fixed: 'right' as const },
];
const pagination = computed(() => ({
@@ -475,6 +462,37 @@ async function handleResetPassword(id: number) {
}
}
// 「更多」下拉分发:确认类操作在 dropdown 内无法用 popconfirm 锚定,统一改用 Modal.confirm
function handleMore(key: string, record: CustomerAccountInfo) {
if (key === 'resetPassword') return confirmResetPassword(record);
if (key === 'delete') return confirmDelete(record);
}
function confirmResetPassword(record: CustomerAccountInfo) {
Modal.confirm({
title: '确认重置该账号的密码?',
content: `账号 ${record.phone} 的密码将重置为手机号后 6 位。`,
okText: '确认重置',
cancelText: '取消',
onOk() {
return handleResetPassword(record.id);
},
});
}
function confirmDelete(record: CustomerAccountInfo) {
Modal.confirm({
title: '确认删除该账号?',
content: `账号 ${record.phone} 将被删除。`,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return handleDelete(record.id);
},
});
}
function formatDateTime(dateStr?: string | null) {
if (!dateStr) return '-';
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
@@ -723,6 +741,14 @@ function formatDateTime(dateStr?: string | null) {
font-weight: 600;
}
.row-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
flex-wrap: nowrap;
}
:deep(.ant-btn-link) {
padding: 0 4px;
}
+41 -19
View File
@@ -60,22 +60,18 @@
<a-badge :count="(record.details || []).length" :number-style="{ backgroundColor: '#1677ff' }" />
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
<template #icon><SearchOutlined /></template>
明细
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-popconfirm title="确定要删除该方案吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
<a-button type="link" size="small" danger>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-popconfirm>
</a-space>
<div class="row-actions">
<a-button type="primary" size="small" @click="handleEdit(record)">编辑</a-button>
<a-dropdown trigger="click">
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
<template #overlay>
<a-menu @click="({ key }) => handleMore(key as string, record)">
<a-menu-item key="view">明细</a-menu-item>
<a-menu-item key="delete" danger>删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
</template>
@@ -183,8 +179,8 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined, DownOutlined } from '@ant-design/icons-vue';
import dayjs from 'dayjs';
import { projectPlanApi, type ProjectPlanInfo, type ProjectPlanQuery, type PlanDetail } from '@/api/projectPlan';
import { customerApi } from '@/api/customer';
@@ -227,7 +223,7 @@ const columns = [
{ title: '登记时间', key: 'registerTime', width: 110 },
{ title: '明细数', key: 'details', align: 'center' as const, width: 90 },
{ title: '状态', key: 'status', align: 'center' as const, width: 90 },
{ title: '操作', key: 'action', width: 180, fixed: 'right' as const },
{ title: '操作', key: 'action', width: 150, align: 'center' as const, fixed: 'right' as const },
];
const detailColumns = [
@@ -418,6 +414,25 @@ async function handleDelete(id: number) {
message.error('删除失败');
}
}
// 「更多」下拉分发:删除需二次确认,popconfirm 在 dropdown 内无法正常锚定,改用 Modal.confirm
function handleMore(key: string, record: ProjectPlanInfo) {
if (key === 'view') return handleView(record);
if (key === 'delete') return confirmDelete(record);
}
function confirmDelete(record: ProjectPlanInfo) {
Modal.confirm({
title: '确认删除该方案?',
content: `方案「${record.planNo}」及其明细将被删除。`,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return handleDelete(record.id);
},
});
}
</script>
<style scoped>
@@ -459,6 +474,13 @@ async function handleDelete(id: number) {
font-size: 14px;
height: 100%;
}
.row-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
flex-wrap: nowrap;
}
:deep(.ant-btn-link) {
padding: 0 4px;
}
+307 -138
View File
@@ -22,15 +22,21 @@
</div>
<div class="header-actions">
<a-button @click="handleExport">
<template #icon><DownloadOutlined /></template>
<template #icon>
<DownloadOutlined />
</template>
导出台账数据
</a-button>
<a-button @click="handleOpenBatchAssign">
<template #icon><TeamOutlined /></template>
<template #icon>
<TeamOutlined />
</template>
批量派工
</a-button>
<a-button type="primary" @click="handleAdd">
<template #icon><PlusOutlined /></template>
<template #icon>
<PlusOutlined />
</template>
发起勘察派单
</a-button>
</div>
@@ -63,55 +69,44 @@
<div class="filter-grid">
<div class="filter-item filter-item-search">
<label class="filter-label">模糊搜索</label>
<a-input
v-model:value="searchForm.keyword"
placeholder="工单编号、客户名称、物理建筑、业务员"
allow-clear
@pressEnter="handleSearch"
>
<template #prefix><SearchOutlined /></template>
<a-input v-model:value="searchForm.keyword" placeholder="工单编号、客户名称、物理建筑、业务员" allow-clear
@pressEnter="handleSearch">
<template #prefix>
<SearchOutlined />
</template>
</a-input>
</div>
<div class="filter-item">
<label class="filter-label">工单流程状态</label>
<a-select
v-model:value="searchForm.status"
placeholder="全部状态"
allow-clear
:options="statusOptions"
/>
<a-select v-model:value="searchForm.status" placeholder="全部状态" allow-clear :options="statusOptions" />
</div>
<div class="filter-item">
<label class="filter-label">指派电工</label>
<a-select
v-model:value="searchForm.workerId"
placeholder="全部电工"
allow-clear
show-search
option-filter-prop="label"
:options="workerOptions"
/>
<a-select v-model:value="searchForm.workerId" placeholder="全部电工" allow-clear show-search
option-filter-prop="label" :options="workerOptions" />
</div>
<div class="filter-item filter-item-date">
<label class="filter-label">计划到场窗口</label>
<a-range-picker
v-model:value="searchForm.planRange"
style="width: 100%"
:placeholder="['开始日期', '结束日期']"
/>
<a-range-picker v-model:value="searchForm.planRange" style="width: 100%" :placeholder="['开始日期', '结束日期']" />
</div>
<div class="filter-item filter-item-actions">
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="handleReset">
<template #icon><ReloadOutlined /></template>
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
<a-button type="link" @click="advanced = !advanced">
高级
<template #icon><FilterOutlined /></template>
<template #icon>
<FilterOutlined />
</template>
</a-button>
</div>
</div>
@@ -120,12 +115,7 @@
<div v-if="advanced" class="filter-grid filter-grid-adv">
<div class="filter-item">
<label class="filter-label">优先级</label>
<a-select
v-model:value="searchForm.priority"
placeholder="全部优先级"
allow-clear
:options="priorityOptions"
/>
<a-select v-model:value="searchForm.priority" placeholder="全部优先级" allow-clear :options="priorityOptions" />
</div>
</div>
</div>
@@ -147,31 +137,23 @@
<span class="legend"><i class="dot-amber"></i>加急 {{ stats.priorityCounts.expedited }}</span>
<span class="legend"><i class="dot-slate"></i>常规 {{ stats.priorityCounts.regular }}</span>
<a-button type="text" size="small" title="刷新列表" @click="handleRefresh">
<template #icon><ReloadOutlined /></template>
<template #icon>
<ReloadOutlined />
</template>
</a-button>
</div>
</div>
<div class="table-wrapper" ref="wrapperRef">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="false"
:row-selection="rowSelection"
:row-class-name="rowClassName"
row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }"
>
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="false"
:row-selection="rowSelection" :row-class-name="rowClassName" row-key="id"
:scroll="{ x: 'max-content', y: tableHeight }">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'surveyNo'">
<div class="no-cell">
<span class="survey-no" @click="goDetail(record)">{{ record.surveyNo }}</span>
<span
v-if="priorityMeta(record.priority)"
class="chip"
:style="chipStyle(priorityMeta(record.priority)!)"
>{{ priorityMeta(record.priority)!.label }}</span>
<span class="survey-no" @click="openDetail(record)">{{ record.surveyNo }}</span>
<span v-if="priorityMeta(record.priority)" class="chip"
:style="chipStyle(priorityMeta(record.priority)!)">{{ priorityMeta(record.priority)!.label }}</span>
</div>
</template>
@@ -243,21 +225,21 @@
<template v-else-if="column.key === 'action'">
<div class="row-actions">
<a-button
v-if="primaryAction(record)"
type="primary"
size="small"
@click="handlePrimary(record)"
>{{ primaryAction(record)!.label }}</a-button>
<a-button v-if="primaryAction(record)" type="primary" size="small"
@click="handlePrimary(record)">{{ primaryAction(record)!.label }}</a-button>
<a-dropdown trigger="click">
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
<a-button type="link" size="small">更多
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="({ key }) => handleMore(key as string, record)">
<a-menu-item key="detail">查看详情</a-menu-item>
<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.ARCHIVED" key="report">查看报告</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="record.status === SURVEY_STATUS.PENDING_REVIEW"
key="archive">报告归档</a-menu-item>
</a-menu>
</template>
</a-dropdown>
@@ -282,85 +264,48 @@
</a-button>
<a-button size="small" disabled>批量打印现场作业票</a-button>
</div>
<a-pagination
v-model:current="searchForm.page"
v-model:page-size="searchForm.pageSize"
:total="total"
:show-size-changer="true"
:show-quick-jumper="true"
:page-size-options="['10', '20', '50']"
:show-total="(t: number) => `共 ${t} 条`"
@change="handlePageChange"
/>
<a-pagination v-model:current="searchForm.page" v-model:page-size="searchForm.pageSize" :total="total"
:show-size-changer="true" :show-quick-jumper="true" :page-size-options="['10', '20', '50']"
:show-total="(t: number) => `共 ${t} 条`" @change="handlePageChange" />
</div>
</div>
</div>
<!-- 指派 / 改派电工 -->
<a-modal
v-model:open="assignVisible"
:title="assignForm.reassign ? '改派共享电工' : '指派共享电工'"
:confirm-loading="assignLoading"
width="480px"
@ok="handleAssignSubmit"
>
<a-modal v-model:open="assignVisible" :title="assignForm.reassign ? '改派共享电工' : '指派共享电工'"
:confirm-loading="assignLoading" width="480px" @ok="handleAssignSubmit">
<a-form :label-col="{ span: 7 }" :wrapper-col="{ span: 15 }">
<a-form-item label="工单编号">
<span class="mono">{{ currentRecord?.surveyNo }}</span>
</a-form-item>
<a-form-item label="指派电工" required>
<a-select
v-model:value="assignForm.workerId"
placeholder="请选择共享电工"
show-search
option-filter-prop="label"
:options="workerOptions"
/>
<a-select v-model:value="assignForm.workerId" placeholder="请选择共享电工" show-search option-filter-prop="label"
:options="workerOptions" />
</a-form-item>
<a-form-item label="预估劳务费">
<a-input-number
v-model:value="assignForm.estimatedFee"
:min="0"
:precision="2"
style="width: 100%"
placeholder="默认按基础工时费 + 回路补贴测算"
/>
<a-input-number v-model:value="assignForm.estimatedFee" :min="0" :precision="2" style="width: 100%"
placeholder="默认按基础工时费 + 回路补贴测算" />
</a-form-item>
</a-form>
</a-modal>
<!-- 批量指定电工 -->
<a-modal
v-model:open="batchAssignVisible"
title="批量指定电工"
:confirm-loading="batchAssignLoading"
width="480px"
@ok="handleBatchAssignSubmit"
>
<a-modal v-model:open="batchAssignVisible" title="批量指定电工" :confirm-loading="batchAssignLoading" width="480px"
@ok="handleBatchAssignSubmit">
<a-form :label-col="{ span: 7 }" :wrapper-col="{ span: 15 }">
<a-form-item label="已选工单">
<span class="mono">{{ selectedRowKeys.length }} 笔</span>
</a-form-item>
<a-form-item label="指派电工" required>
<a-select
v-model:value="batchWorkerId"
placeholder="请选择共享电工"
show-search
option-filter-prop="label"
:options="workerOptions"
/>
<a-select v-model:value="batchWorkerId" placeholder="请选择共享电工" show-search option-filter-prop="label"
:options="workerOptions" />
</a-form-item>
</a-form>
</a-modal>
<!-- 审核测绘成果 -->
<a-modal
v-model:open="reviewVisible"
title="审核测绘成果"
:confirm-loading="reviewLoading"
width="560px"
@ok="handleReviewSubmit"
>
<a-modal v-model:open="reviewVisible" title="审核测绘成果" :confirm-loading="reviewLoading" width="560px"
@ok="handleReviewSubmit">
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="工单编号">
<span class="mono">{{ currentRecord?.surveyNo }}</span>
@@ -372,39 +317,127 @@
</a-radio-group>
</a-form-item>
<a-form-item label="复核意见">
<a-textarea
v-model:value="reviewForm.remark"
:rows="3"
maxlength="500"
placeholder="请填写技术复核意见(驳回时必填)"
/>
<a-textarea v-model:value="reviewForm.remark" :rows="3" maxlength="500" placeholder="请填写技术复核意见(驳回时必填)" />
</a-form-item>
</a-form>
</a-modal>
<!-- 取消工单 -->
<a-modal
v-model:open="cancelVisible"
title="取消勘察工单"
:confirm-loading="cancelLoading"
width="480px"
@ok="handleCancelSubmit"
>
<a-modal v-model:open="cancelVisible" title="取消勘察工单" :confirm-loading="cancelLoading" width="480px"
@ok="handleCancelSubmit">
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="工单编号">
<span class="mono">{{ currentRecord?.surveyNo }}</span>
</a-form-item>
<a-form-item label="取消原因">
<a-textarea
v-model:value="cancelReason"
:rows="3"
maxlength="500"
placeholder="请填写取消原因"
/>
<a-textarea v-model:value="cancelReason" :rows="3" maxlength="500" placeholder="请填写取消原因" />
</a-form-item>
</a-form>
</a-modal>
<!-- 工单详情 -->
<a-modal v-model:open="detailVisible" title="勘察工单详情" :footer="null" width="860px">
<div v-if="detailRecord" class="detail">
<div class="detail-grid">
<div class="detail-item"><span class="k">工单编号</span><span class="v mono">{{ detailRecord.surveyNo }}</span>
</div>
<div class="detail-item"><span class="k">当前状态</span><span
class="v">{{ statusMeta(detailRecord.status).label }}</span></div>
<div class="detail-item"><span class="k">关联客户</span><span
class="v">{{ detailRecord.customer?.customerName || detailRecord.customerName || '-' }}</span></div>
<div class="detail-item"><span class="k">客户编号</span><span
class="v mono">{{ detailRecord.customer?.customerNo || '-' }}</span></div>
<div class="detail-item"><span class="k">任务主题</span><span class="v">{{ detailRecord.taskTitle || '-' }}</span>
</div>
<div class="detail-item"><span class="k">现场位置</span><span
class="v">{{ detailRecord.siteLocation || '-' }}</span>
</div>
<div class="detail-item"><span class="k">客户地址</span><span
class="v">{{ detailRecord.customer?.address || '-' }}</span></div>
<div class="detail-item"><span class="k">技术联系人</span><span
class="v">{{ detailRecord.techContact || '-' }}</span>
</div>
<div class="detail-item"><span class="k">回路规格</span><span class="v">{{ detailRecord.loopSpec || '-' }}</span>
</div>
<div class="detail-item"><span class="k">回路数量</span><span class="v mono">{{ detailRecord.loopCount ?? 0 }}
路</span></div>
<div class="detail-item"><span class="k">优先级</span><span
class="v">{{ priorityMeta(detailRecord.priority)?.label || '-' }}</span></div>
<div class="detail-item"><span class="k">责任业务员</span><span
class="v">{{ detailRecord.salesmanName || '-' }}</span>
</div>
<div class="detail-item"><span class="k">业务员电话</span><span
class="v mono">{{ detailRecord.salesmanPhone || '-' }}</span></div>
<div class="detail-item"><span class="k">指派电工</span><span
class="v">{{ detailRecord.workerName || '待指派' }}</span>
</div>
<div class="detail-item"><span class="k">电工资质</span><span
class="v">{{ detailRecord.workerCertInfo || '-' }}</span></div>
<div class="detail-item"><span class="k">计划时间</span><span
class="v">{{ formatRange(detailRecord.planStart, detailRecord.planEnd) }}</span></div>
<div class="detail-item"><span class="k">期望到场</span><span
class="v">{{ formatDateTime(detailRecord.expectArrivalAt) }}</span></div>
<div class="detail-item"><span class="k">预估劳务费</span><span
class="v mono">{{ money(detailRecord.estimatedFee) }}</span></div>
<div class="detail-item"><span class="k">实发劳务费</span><span
class="v mono">{{ money(detailRecord.actualFee) }}</span></div>
<div class="detail-item"><span class="k">测绘进度</span><span
class="v mono">{{ detailRecord.progressDone ?? 0 }}/{{
detailRecord.progressTotal ?? 0 }}</span></div>
<div class="detail-item"><span class="k">催办次数</span><span class="v">{{ detailRecord.urgeCount ?? 0 }} 次</span>
</div>
<div class="detail-item"><span class="k">复核时间</span><span
class="v">{{ formatDateTime(detailRecord.reviewedAt) }}</span></div>
</div>
<div class="detail-block">
<div class="detail-label">技术复核意见</div>
<div class="detail-text">{{ detailRecord.reviewRemark || '—' }}</div>
</div>
<div v-if="detailRecord.cancelReason" class="detail-block">
<div class="detail-label">取消原因</div>
<div class="detail-text">{{ detailRecord.cancelReason }}</div>
</div>
<div class="detail-block">
<div class="detail-label">工单备注</div>
<div class="detail-text">{{ detailRecord.remark || '—' }}</div>
</div>
<div class="detail-block">
<div class="detail-label">现场照片</div>
<div v-if="detailRecord.photoUrls?.length" class="photo-list">
<a-image-preview-group>
<a-image v-for="(url, idx) in detailRecord.photoUrls" :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="detailRecord.sopResults?.length" class="detail-block">
<div class="detail-label">
勘察回执({{ detailSopDone }} / {{ detailRecord.sopResults.length }} 项已完成)
</div>
<div v-for="row in detailRecord.sopResults" :key="row.id" class="sop-item">
<div class="sop-item-head">
<span class="sop-item-name">{{ row.itemName || row.sopItem?.itemName || '检查项' }}</span>
<span class="chip" :style="sopChipStyle(row.checked)">{{ row.checked ? '已回执' : '未回执' }}</span>
</div>
<div v-if="row.receiptUrls?.length" class="photo-list">
<a-image v-for="(url, idx) in row.receiptUrls" :key="idx" :src="url" :width="110" :height="80"
style="object-fit: cover; border-radius: 8px" />
</div>
<div v-else-if="row.receiptValue?.length" class="detail-text">
{{ row.receiptValue.join('、') }}
</div>
<div v-if="row.remark" class="sop-item-remark">备注:{{ row.remark }}</div>
</div>
</div>
</div>
</a-modal>
</div>
</template>
@@ -714,11 +747,31 @@ function handlePrimary(record: SurveyOrderInfo) {
}
function handleMore(key: string, record: SurveyOrderInfo) {
if (key === 'open') return goDetail(record);
if (key === 'detail') return openDetail(record);
if (key === 'edit') return goDetail(record);
if (key === 'reassign') return openAssign(record, true);
if (key === 'urge') return handleUrge(record);
if (key === 'report') return message.info('查看勘察报告功能暂未开放');
if (key === 'cancel') return openCancel(record);
if (key === 'archive') return handleArchive(record);
}
// 报告归档:待技术复核工单复核通过并归档
function handleArchive(record: SurveyOrderInfo) {
Modal.confirm({
title: '确认报告归档?',
content: `工单 ${record.surveyNo} 将复核通过并变更为「已归档」。`,
okText: '确认归档',
cancelText: '取消',
async onOk() {
try {
const res = await surveyOrderApi.review(record.id, { result: 'pass' });
message.success(res.message || '归档成功');
handleRefresh();
} catch {
// 拦截器已提示
}
},
});
}
// ==================== 指派 / 改派 ====================
@@ -880,7 +933,25 @@ async function handleCancelSubmit() {
}
// ==================== 详情 / 编辑 ====================
// 详情与编辑统一进编辑页;不可编辑状态由 form.vue 按 status 自动降级为只读
// 只读详情弹窗:点击编号或「更多 → 查看详情」触发,不跳转页面
const detailVisible = ref(false);
const detailRecord = ref<SurveyOrderInfo | null>(null);
const detailSopDone = computed(
() => (detailRecord.value?.sopResults || []).filter((row) => row.checked).length
);
async function openDetail(record: SurveyOrderInfo) {
detailRecord.value = record;
detailVisible.value = true;
try {
const res = await surveyOrderApi.getDetail(record.id);
detailRecord.value = res.data;
} catch {
// 详情获取失败时保留列表行数据
}
}
// 编辑进编辑页;不可编辑状态由 form.vue 按 status 自动降级为只读
function goDetail(record: SurveyOrderInfo) {
router.push({ name: 'ProjectSurveyEdit', params: { id: record.id } });
}
@@ -891,6 +962,21 @@ function formatDateTime(value?: string) {
return dayjs(value).format('YYYY-MM-DD HH:mm');
}
function formatRange(start?: string, end?: string) {
if (!start && !end) return '—';
const s = start ? dayjs(start).format('YYYY-MM-DD HH:mm') : '—';
const e = end ? dayjs(end).format('YYYY-MM-DD HH:mm') : '—';
return `${s} ~ ${e}`;
}
// SOP 回执完成状态标签配色
const sopChipStyle = (checked?: boolean) =>
chipStyle(
checked
? { color: '#059669', bg: '#d1fae5', border: '#a7f3d0' }
: { color: '#94a3b8', bg: '#f8fafc', border: '#e2e8f0' }
);
function money(value?: number | string) {
if (value == null || value === '') return '—';
const num = Number(value);
@@ -1455,6 +1541,89 @@ function maskPhone(phone?: string) {
font-weight: 600;
}
/* 详情弹窗 */
.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;
}
.photo-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.sop-item {
padding: 10px 0;
border-bottom: 1px dashed #eef1f6;
}
.sop-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.sop-item-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.sop-item-name {
font-size: 13px;
font-weight: 600;
color: #0d1c2c;
}
.sop-item-remark {
margin-top: 6px;
font-size: 12px;
color: #8a97a8;
}
/* AntD 覆盖 */
:deep(.ant-table) {
font-size: 13px;
+42 -16
View File
@@ -30,19 +30,18 @@
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleAdd(record)">
<PlusOutlined />新增子菜单
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
<EditOutlined />编辑
</a-button>
<a-popconfirm title="确定删除该菜单?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record)">
<a-button type="link" size="small" danger>
<DeleteOutlined />删除
</a-button>
</a-popconfirm>
</a-space>
<div class="row-actions">
<a-button type="primary" size="small" @click="handleEdit(record)">编辑</a-button>
<a-dropdown trigger="click">
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
<template #overlay>
<a-menu @click="({ key }) => handleMore(key as string, record)">
<a-menu-item key="addChild">新增子菜单</a-menu-item>
<a-menu-item key="delete" danger>删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
</template>
</a-table>
@@ -100,8 +99,8 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { PlusOutlined, DownOutlined } from '@ant-design/icons-vue';
import { menuApi } from '@/api/menu';
import { useTableScroll } from '@/composables/useTableScroll';
@@ -134,7 +133,7 @@ const columns = [
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80, align: 'center' as const },
{ title: '权限标识', dataIndex: 'permission', key: 'permission', width: 150 },
{ title: '可见性', dataIndex: 'visible', key: 'visible', width: 90, align: 'center' as const },
{ title: '操作', key: 'action', width: 240, fixed: 'right' as const },
{ title: '操作', key: 'action', width: 150, align: 'center' as const, fixed: 'right' as const },
];
// 上级菜单树(编辑时排除自身及后代节点,防止循环引用)
@@ -300,6 +299,25 @@ async function handleDelete(record: MenuItem) {
message.error('删除失败');
}
}
// 「更多」下拉分发:删除需二次确认,popconfirm 在 dropdown 内无法正常锚定,改用 Modal.confirm
function handleMore(key: string, record: MenuItem) {
if (key === 'addChild') return handleAdd(record);
if (key === 'delete') return confirmDelete(record);
}
function confirmDelete(record: MenuItem) {
Modal.confirm({
title: '确认删除该菜单?',
content: `菜单「${record.menuName}」将被删除。`,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return handleDelete(record);
},
});
}
</script>
<style scoped lang="less">
@@ -333,6 +351,14 @@ async function handleDelete(record: MenuItem) {
overflow: hidden;
}
.row-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
flex-wrap: nowrap;
}
:deep(.ant-btn-link) {
padding: 0 4px;
}
+43 -27
View File
@@ -49,24 +49,18 @@
</template>
<template v-if="column.dataIndex === 'action'">
<a-space>
<a-button type="link" size="small" @click="handlePermission(record)">
<KeyOutlined />权限配置
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
<EditOutlined />编辑
</a-button>
<a-popconfirm
title="确定删除该角色?"
ok-text="确定"
cancel-text="取消"
@confirm="handleDelete(record)"
>
<a-button type="link" size="small" danger>
<DeleteOutlined />删除
</a-button>
</a-popconfirm>
</a-space>
<div class="row-actions">
<a-button type="primary" size="small" @click="handlePermission(record)">权限配置</a-button>
<a-dropdown trigger="click">
<a-button type="link" size="small">更多 <DownOutlined /></a-button>
<template #overlay>
<a-menu @click="({ key }) => handleMore(key as string, record)">
<a-menu-item key="edit">编辑</a-menu-item>
<a-menu-item key="delete" danger>删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
</template>
</a-table>
@@ -138,14 +132,8 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { message } from 'ant-design-vue';
import {
PlusOutlined,
SearchOutlined,
KeyOutlined,
EditOutlined,
DeleteOutlined,
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { PlusOutlined, SearchOutlined, DownOutlined } from '@ant-design/icons-vue';
import { roleApi, type RoleInfo } from '@/api/role';
import { useTableScroll } from '@/composables/useTableScroll';
import dayjs from 'dayjs';
@@ -207,7 +195,8 @@ const columns = [
title: '操作',
dataIndex: 'action',
key: 'action',
width: 280,
width: 190,
align: 'center' as const,
fixed: 'right' as const,
},
];
@@ -345,6 +334,25 @@ async function handleDelete(record: RoleInfo) {
}
}
// 「更多」下拉分发:删除需二次确认,popconfirm 在 dropdown 内无法正常锚定,改用 Modal.confirm
function handleMore(key: string, record: RoleInfo) {
if (key === 'edit') return handleEdit(record);
if (key === 'delete') return confirmDelete(record);
}
function confirmDelete(record: RoleInfo) {
Modal.confirm({
title: '确认删除该角色?',
content: `角色「${record.roleName}」将被删除。`,
okText: '确认删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return handleDelete(record);
},
});
}
function handleViewUsers(_record: RoleInfo) {
message.info('查看用户功能开发中');
}
@@ -448,6 +456,14 @@ onMounted(() => {
height: 100%;
}
.row-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
flex-wrap: nowrap;
}
:deep(.ant-btn-link) {
padding: 0 4px;
}