feat: 重构操作按钮为下拉菜单并新增工单详情弹窗
- 重构角色、菜单、项目方案、客户账号页面的操作按钮,将多个操作整合为下拉菜单 - 统一使用 Modal.confirm 替代 Popconfirm 解决下拉菜单内锚定问题 - 新增勘察工单详情弹窗,支持查看工单完整信息 - 调整操作列宽度和对齐方式,优化表格布局 - 统一中文 locale 配置,使用 dayjs 中文 locale
This commit is contained in:
1 parent
ae79d5157b
commit
e03fb019c5
6 files changed
+496
-229
No files matched your search
+9
-1
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user