This commit is contained in:
Leo_Ding 2025-10-17 19:24:05 +08:00
commit c17f6794a4
17 changed files with 3702 additions and 58 deletions

View File

@ -0,0 +1,22 @@
// 工单模块
import request from '@/utils/request'
// 工单列表-- 全部
export const getWorkOrderList = (params) => request.basic.get('/api/v1/orders', params)
// 异常工单列表
export const getAbnormalWorkOrderList = (params) => request.basic.get('/api/v1/orders/errors_order', params)
// 我下的工单
export const getMyWorkOrderList = (params) => request.basic.get('/api/v1/orders/my_order', params)
// 回访记录
export const getBackRecordList = (params) => request.basic.get('/api/v1/orders/back_order', params)
// 工单回访列表
export const getBackWorkOrderList = (params) => request.basic.get(`/api/v1/orders/noback_order`, params)
// 工单详情
export const getWorkOrderDetail = (id) => request.basic.get(`/api/v1/orders/${id}`)

View File

@ -40,6 +40,8 @@ export default {
mineWorderOrder: '我下的工单', mineWorderOrder: '我下的工单',
invalidWzorkOrder: '无效工单', invalidWzorkOrder: '无效工单',
abnormalWorkOrder: '异常工单', abnormalWorkOrder: '异常工单',
visitWorkOrder: '工单回访',
visitHistory: '回访记录',
serviceWorkOrder: '服务工单', serviceWorkOrder: '服务工单',
serviceMenu: '服务设施', serviceMenu: '服务设施',
serviceSites: '服务站点', serviceSites: '服务站点',

View File

@ -57,6 +57,28 @@ export default [
permission: '*', permission: '*',
}, },
}, },
{
path: 'visitWorkOrder/index.vue',
name: 'visitWorkOrder',
component: 'workorderMenu/visitWorkOrder/index.vue',
meta: {
title: '工单回访',
isMenu: true,
keepAlive: true,
permission: '*',
},
},
{
path: 'visitHistory/index.vue',
name: 'visitHistory',
component: 'workorderMenu/visitHistory/index.vue',
meta: {
title: '回访记录',
isMenu: true,
keepAlive: true,
permission: '*',
},
},
], ],
}, },
] ]

View File

@ -11,7 +11,7 @@
<a-col v-bind="colSpan"> <a-col v-bind="colSpan">
<a-form-item :label="'站点类型'" name="type"> <a-form-item :label="'站点类型'" name="type">
<a-select v-model:value="searchFormData.type" placeholder="请选择站点类型" allow-clear> <a-select v-model:value="searchFormData.stationType" placeholder="请选择站点类型" allow-clear>
<a-select-option v-for="item in dicsStore.dictOptions.Station_Type" :key="item.dval" <a-select-option v-for="item in dicsStore.dictOptions.Station_Type" :key="item.dval"
:value="item.dval"> :value="item.dval">
{{ item.introduction }} {{ item.introduction }}
@ -22,16 +22,13 @@
<a-col v-bind="colSpan"> <a-col v-bind="colSpan">
<a-form-item :label="'所在区域'" name="area"> <a-form-item :label="'所在区域'" name="area">
<AreaCascader v-model:value="searchFormData.area" @change="onAreaChange" /> <AreaCascader v-model:value="searchFormData.areaCodes" @change="onAreaChange" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col v-bind="colSpan"> <a-col v-bind="colSpan">
<a-form-item :label="'所在节点'" name="node"> <a-form-item label="组织所在区域" name="areaLabels">
<a-select v-model:value="searchFormData.node" @change="handleChange"> <NodeTree v-model:value="searchFormData.areaLabels" />
<a-select-option value="jack">已结单</a-select-option>
<a-select-option value="lucy">已作废</a-select-option>
</a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -180,7 +177,7 @@ import ExportRecordsModal from '@/components/ExportRecord/index.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import AreaCascader from '@/components/AreaCascader/index.vue' import AreaCascader from '@/components/AreaCascader/index.vue'
import NodeTree from '@/components/NodeTree/index.vue'
defineOptions({ defineOptions({
name: 'menu', name: 'menu',
@ -237,6 +234,7 @@ const { resetForm } = useForm()
const editDialogRef = ref() const editDialogRef = ref()
import { useDicsStore } from '@/store' import { useDicsStore } from '@/store'
const dicsStore = useDicsStore() const dicsStore = useDicsStore()
const areaCodes = ref([])
getList() getList()
@ -248,10 +246,13 @@ async function getList() {
try { try {
showLoading() showLoading()
const { pageSize, current } = paginationState const { pageSize, current } = paginationState
console.log("=====searchFormData",searchFormData.value.areaCodes)
const { success, data, total } = await apis.serviceMenu const { success, data, total } = await apis.serviceMenu
.getServiceSiteList({ .getServiceSiteList({
pageSize, pageSize,
current: current, current: current,
// areaCodes:areaCodes,
...searchFormData.value, ...searchFormData.value,
}) })
.catch(() => { .catch(() => {
@ -267,6 +268,7 @@ async function getList() {
hideLoading() hideLoading()
} }
} }
/** /**
* 搜索 * 搜索
*/ */
@ -631,6 +633,14 @@ const handleRetryExport = (record) => {
message.error('重新导出失败'); message.error('重新导出失败');
}); });
}; };
//
// <script setup>
const onAreaChange = (value, selectedOptions) => {
areaCodes.value = value;
console.log('value:', value);
console.log('selectedOptions:', selectedOptions); //
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -0,0 +1,249 @@
<template>
<a-modal
:width="920"
:open="modal.open"
:title="modal.title"
:footer="null"
@cancel="handleCancel"
wrapClassName="order-detail-modal"
>
<!-- 服务轨迹 -->
<a-descriptions bordered size="small" :column="2" class="mb-6">
<template #title>
<a-divider orientation="left" class="!mt-0">服务轨迹</a-divider>
</template>
<a-descriptions-item label="签入时间">
{{ detail.checkInTime }}
</a-descriptions-item>
<a-descriptions-item label="签入距离">
{{ detail.checkInDistance }}
</a-descriptions-item>
<a-descriptions-item label="签入地址" :span="2">
{{ detail.checkInAddress }}
</a-descriptions-item>
<a-descriptions-item label="签入图片" :span="2">
<div class="image-preview-group">
<a-space>
<a-image
v-for="(img, index) in detail.checkInImages"
:key="'in-' + index"
:src="img"
:width="100"
:height="100"
:preview-src-list="detail.checkInImages"
:preview-visible="false"
class="rounded"
/>
</a-space>
<span v-if="!detail.checkInImages?.length" class="text-gray-400"></span>
</div>
</a-descriptions-item>
<a-descriptions-item label="签出时间">
{{ detail.checkOutTime }}
</a-descriptions-item>
<a-descriptions-item label="签出距离">
{{ detail.checkOutDistance }}
</a-descriptions-item>
<a-descriptions-item label="签出地址" :span="2">
{{ detail.checkOutAddress }}
</a-descriptions-item>
<a-descriptions-item label="签出图片" :span="2">
<div class="image-preview-group">
<a-space>
<a-image
v-for="(img, index) in detail.checkOutImages"
:key="'out-' + index"
:src="img"
:width="100"
:height="100"
:preview-src-list="detail.checkOutImages"
:preview-visible="false"
class="rounded"
/>
</a-space>
<span v-if="!detail.checkOutImages?.length" class="text-gray-400"></span>
</div>
</a-descriptions-item>
<a-descriptions-item label="签出备注" :span="2">
{{ detail.checkOutRemark || '无' }}
</a-descriptions-item>
<a-descriptions-item label="实际服务时长" :span="2">
{{ detail.serviceDuration }}
</a-descriptions-item>
</a-descriptions>
<!-- 用户信息 -->
<a-descriptions bordered size="small" :column="2" class="mb-6">
<template #title>
<a-divider orientation="left">用户信息</a-divider>
</template>
<a-descriptions-item label="工单号">{{ detail.orderNo }}</a-descriptions-item>
<a-descriptions-item label="姓名">{{ detail.userName }}</a-descriptions-item>
<a-descriptions-item label="性别">{{ detail.gender }}</a-descriptions-item>
<a-descriptions-item label="年龄">{{ detail.age }} </a-descriptions-item>
<a-descriptions-item label="联系电话">{{ detail.phone }}</a-descriptions-item>
<a-descriptions-item label="身份证号">{{ detail.idCard }}</a-descriptions-item>
<a-descriptions-item label="评估等级" :span="2">
{{ detail.assessmentLevel || '未填写' }}
</a-descriptions-item>
</a-descriptions>
<!-- 任务信息 -->
<a-descriptions bordered size="small" :column="2" class="mb-6">
<template #title>
<a-divider orientation="left">任务信息</a-divider>
</template>
<a-descriptions-item label="护理员">{{ detail.caregiver }}</a-descriptions-item>
<a-descriptions-item label="护理等级">{{ detail.careLevel || '未填写' }}</a-descriptions-item>
<a-descriptions-item label="计划服务时间">{{ detail.plannedTime }}</a-descriptions-item>
<a-descriptions-item label="服务地址" :span="2">
{{ detail.serviceAddress }}
</a-descriptions-item>
</a-descriptions>
<!-- 服务项目 -->
<div class="mb-6">
<a-divider orientation="left">服务项目</a-divider>
<a-table
:dataSource="detail.serviceItems"
:columns="serviceColumns"
size="small"
:pagination="false"
bordered
class="compact-table"
/>
</div>
<!-- 异常工单处理信息 -->
<a-descriptions bordered size="small" :column="2">
<template #title>
<a-divider orientation="left">异常工单处理信息</a-divider>
</template>
<a-descriptions-item label="处理状态">{{ detail.exceptionStatus }}</a-descriptions-item>
<a-descriptions-item label="处理原因">{{ detail.exceptionReason || '无' }}</a-descriptions-item>
<a-descriptions-item label="处理人">{{ detail.exceptionHandler || '无' }}</a-descriptions-item>
<a-descriptions-item label="处理时间">{{ detail.exceptionTime || '无' }}</a-descriptions-item>
</a-descriptions>
</a-modal>
</template>
<script setup>
import { reactive } from 'vue'
import { useModal } from '@/hooks'
const { modal, hideModal } = useModal()
//
const detail = reactive({
checkInTime: '2025-10-15 11:44:01',
checkInDistance: '14996 m',
checkInAddress: '江苏省南通市通州区十总镇南通市通州区十总镇五总居8-128西北775米',
checkInImages: [
'https://akt.obs.cn-east-3.myhuaweicloud.com:443/hahacloud/saas/1813150290048446464/6a4ddb23-8733-4ca4-a1f9-b0f061a4ca0a.jpg?AccessKeyId=EBJWO1KETPFJKBWNCG5V&Expires=1761122523&Signature=bygou7kYVrQ6f9bDDWlpebs15Nc%3D',
'https://akt.obs.cn-east-3.myhuaweicloud.com:443/hahacloud/saas/1813150290048446464/11ce319c-705e-431a-a3a7-4a9bb8350fce.jpg?AccessKeyId=EBJWO1KETPFJKBWNCG5V&Expires=1761122523&Signature=cOHYxAxsTP0RQDgT2bNv2jjkWco%3D'
],
checkOutTime: '2025-10-15 12:44:43',
checkOutDistance: '14984 m',
checkOutAddress: '江苏省南通市通州区十总镇南通市通州区十总镇五总居8-128西北757米',
checkOutImages: [
'https://akt.obs.cn-east-3.myhuaweicloud.com:443/hahacloud/saas/1813150290048446464/3e7817e4-4d31-41d8-ba89-6532a5baea5d.jpg?AccessKeyId=EBJWO1KETPFJKBWNCG5V&Expires=1761122523&Signature=efu9s%2BjgIXdxZPU6sEH5OR0ipdE%3D',
'https://akt.obs.cn-east-3.myhuaweicloud.com:443/hahacloud/saas/1813150290048446464/9902435e-fd61-423c-9681-7f11bcab7ee8.jpg?AccessKeyId=EBJWO1KETPFJKBWNCG5V&Expires=1761122523&Signature=y204ac9D%2BiI%2BWtNOaOu9UylAezg%3D',
'https://akt.obs.cn-east-3.myhuaweicloud.com:443/hahacloud/saas/1813150290048446464/157a72f1-c081-4407-ab48-e38e25b38a24.jpg?AccessKeyId=EBJWO1KETPFJKBWNCG5V&Expires=1761122523&Signature=wvjINjdpPlG3QhkuDYIAISc20sY%3D'
],
checkOutRemark: '助洁理发,助餐剥花生',
serviceDuration: '60 分钟',
orderNo: '202510151043270345563994',
userName: '顾美田',
gender: '男',
age: '90',
phone: '18452439097',
idCard: '320624193507244576',
assessmentLevel: '',
caregiver: '于圣霞',
careLevel: '',
plannedTime: '2025-10-15 10:42:00',
serviceAddress: '江苏省南通市通州区十总镇五总社区居委会通州区五总居十七组3号',
serviceItems: [
{ itemCategoryName: '助乐服务', itemName: '精神关爱', careDuration: '60' }
],
exceptionStatus: '未处理',
exceptionReason: '',
exceptionHandler: '',
exceptionTime: ''
})
const serviceColumns = [
{ title: '服务项目分类', dataIndex: 'itemCategoryName', width: '30%' },
{ title: '服务项目名称', dataIndex: 'itemName', width: '40%' },
{ title: '服务时长(分钟)', dataIndex: 'careDuration', width: '30%' }
]
function handleCancel() {
hideModal()
}
function handleView() {
modal.value.open = true
modal.value.title = '工单详情'
}
defineExpose({
handleView
})
</script>
<style scoped>
/* 全局调整 modal 内部样式 */
.order-detail-modal :deep(.ant-descriptions-title) {
padding: 0;
}
.order-detail-modal :deep(.ant-descriptions-item-label) {
width: 120px;
font-weight: 600;
}
/* 图片预览区域 */
.image-preview-group {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
min-height: 32px;
}
.image-preview-group .ant-image {
border-radius: 4px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
.image-preview-group .text-gray-400 {
color: #8c8c8c;
font-style: italic;
}
/* 表格紧凑 */
.compact-table :deep(.ant-table-tbody > tr > td),
.compact-table :deep(.ant-table-thead > tr > th) {
padding: 8px 12px;
}
/* 区块间距 */
.mb-6 {
margin-bottom: 24px;
}
/* 覆盖 divider margin */
.order-detail-modal :deep(.ant-divider-horizontal) {
margin: 20px 0 16px;
}
</style>

View File

@ -0,0 +1,374 @@
<template>
<x-search-bar class="mb-8-2">
<template #default="{ gutter, colSpan }">
<a-form :label-col="{ style: { width: '100px' } }" :model="searchFormData" layout="inline">
<a-row :gutter="gutter">
<a-col v-bind="colSpan">
<a-form-item :label="'姓名'" name="name">
<a-input :placeholder="'请选择姓名'" v-model:value="searchFormData.name"></a-input>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item name="code">
<template #label>
{{ '身份证号' }}
<a-tooltip :title="'身份证号'">
<question-circle-outlined class="ml-4-1 color-placeholder" />
</a-tooltip>
</template>
<a-input :placeholder="'请输入身份证号'" v-model:value="searchFormData.code"></a-input>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item :label="'工单号'" name="name">
<a-input :placeholder="'请选择工单号'" v-model:value="searchFormData.name"></a-input>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="gutter" style="margin-top: 16px;">
<a-col :span="24" style="text-align: right;">
<a-space>
<a-button @click="handleResetSearch">{{ $t('button.reset') }}</a-button>
<a-button ghost type="primary" @click="handleSearch">
{{ $t('button.search') }}
</a-button>
</a-space>
</a-col>
</a-row>
</a-form>
</template>
</x-search-bar>
<a-card>
<x-action-bar class="mb-8-2">
<a-button v-action="'add'"
type="primary"
:disabled="selectedRowKeys.length === 0"
@click="handleBatchProcess">
批量处理(最多40条)
</a-button>
</x-action-bar>
<a-table
rowKey="id"
:loading="loading"
:pagination="true"
:columns="columns"
:data-source="listData"
:row-selection="rowSelection"
:scroll="{ x: 'max-content' }"
>
<template #bodyCell="{ column, record }">
<template v-if="'menuType' === column.key">
<!--菜单-->
<a-tag v-if="menuTypeEnum.is('page', record.type)" color="processing">
{{ menuTypeEnum.getDesc(record.type) }}
</a-tag>
<!--按钮-->
<a-tag v-if="menuTypeEnum.is('button', record.type)" color="success">
{{ menuTypeEnum.getDesc(record.type) }}
</a-tag>
</template>
<template v-if="'createAt' === column.key">
{{ formatUtcDateTime(record.created_at) }}
</template>
<template v-if="'statusType' === column.key">
<!--状态-->
<a-tag v-if="statusTypeEnum.is('enabled', record.status)" color="processing">
{{ statusTypeEnum.getDesc(record.status) }}
</a-tag>
<!--状态-->
<a-tag v-if="statusTypeEnum.is('disabled', record.status)" color="processing">
{{ statusTypeEnum.getDesc(record.status) }}
</a-tag>
</template>
<template v-if="'action' === column.key">
<x-action-button @click="$refs.viewDialogRef.handleView(record)">
<a-tooltip>
<template #title>详情</template>
详情
</a-tooltip>
</x-action-button>
<x-action-button @click="handleSingleProcess(record)">
<a-tooltip>
<template #title>处理</template>
处理
</a-tooltip>
</x-action-button>
</template>
</template>
</a-table>
</a-card>
<!-- 处理弹窗 -->
<a-modal
v-model:open="processModalVisible"
title="处理异常"
@ok="handleProcessSubmit"
@cancel="processModalVisible = false"
:confirm-loading="processSubmitting"
>
<a-form :model="processForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="处理结果" name="processStatus" required>
<a-select v-model:value="processForm.processStatus" placeholder="请选择处理结果">
<a-select-option value="handled">已处理</a-select-option>
<a-select-option value="ignored">忽略</a-select-option>
<!-- 可根据实际枚举扩展 -->
</a-select>
</a-form-item>
<a-form-item label="处理原因" name="processRemark">
<a-textarea
v-model:value="processForm.processRemark"
placeholder="请输入处理原因"
:rows="4"
allow-clear
/>
</a-form-item>
</a-form>
</a-modal>
<view-dialog @ok="onOk" ref="viewDialogRef" />
</template>
<script setup>
import { Modal, message } from 'ant-design-vue'
import { ref,reactive } from 'vue'
import { PlusOutlined, EditOutlined, DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons-vue'
import apis from '@/apis'
import { config } from '@/config'
import { menuTypeEnum, statusTypeEnum } from '@/enums/system'
import { usePagination, useForm } from '@/hooks'
import { formatUtcDateTime } from '@/utils/util'
import ViewDialog from './components/viewDialog.vue'
import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage'
import AreaCascader from '@/components/AreaCascader/index.vue'
import dayjs from 'dayjs'
defineOptions({
// eslint-disable-next-line vue/no-reserved-component-names
name: 'menu',
})
const { t } = useI18n() // t
const columns = ref([
{ title: '工单号', dataIndex: 'orderId', key: 'orderId', width: 180 },
{ title: '姓名', dataIndex: 'customerName', key: 'customerName', width: 120 },
{ title: '身份证号', dataIndex: 'identityNo', key: 'identityNo', width: 180 },
{ title: '异常情况', dataIndex: 'abnormalStatus', key: 'abnormalStatus', width: 120 },
{ title: '服务人员', dataIndex: 'serviceUserName', key: 'serviceUserName', width: 120 },
{ title: '服务项目', dataIndex: 'itemNames', key: 'itemNames', width: 200 },
{ title: '签入时间', dataIndex: 'realStartTime', key: 'realStartTime', width: 160 },
{ title: '签出时间', dataIndex: 'realEndTime', key: 'realEndTime', width: 160 },
{ title: '处理状态', dataIndex: 'processStatus', key: 'processStatus', width: 120 },
{ title: '处理原因', dataIndex: 'processRemark', key: 'processRemark', width: 160 },
{ title: '处理人', dataIndex: 'processUserName', key: 'processUserName', width: 120 },
{ title: '处理时间', dataIndex: 'processTime', key: 'processTime', width: 160 },
{ title: '服务地址', dataIndex: 'serviceAddress', key: 'serviceAddress', width: 200 },
{ title: '操作', key: 'action', fixed: 'right', width: 340 },
])
const { listData, loading, showLoading, hideLoading, searchFormData, paginationState, resetPagination } =
usePagination()
const { resetForm } = useForm()
const viewDialogRef = ref()
const selectedRowKeys = ref([]) // id
const selectedRows = ref([]) //
const rowSelection = ref({
onChange: (keys, rows) => {
if (keys.length > 40) {
message.warning('最多只能选择40条记录进行批量处理')
// 40
selectedRowKeys.value = keys.slice(0, 40)
selectedRows.value = rows.slice(0, 40)
return
}
selectedRowKeys.value = keys
selectedRows.value = rows
},
selectedRowKeys: selectedRowKeys,
})
//
const processModalVisible = ref(false)
const processSubmitting = ref(false)
const processForm = reactive({
processStatus: undefined,
processRemark: '',
})
let currentProcessRecords = [] //
//
function handleBatchProcess() {
if (selectedRowKeys.value.length === 0) return
if (selectedRowKeys.value.length > 40) {
message.warning('最多选择40条')
return
}
currentProcessRecords = [...selectedRows.value]
processForm.processStatus = undefined
processForm.processRemark = ''
processModalVisible.value = true
}
getMenuList()
/**
* 获取菜单列表
* @return {Promise<void>}
*/
async function getMenuList() {
try {
showLoading()
const { pageSize, current } = paginationState
const { success, data, total } = await apis.workOrder.getAbnormalWorkOrderList({
pageSize,
current: current,
...searchFormData.value,
})
//
const mappedData = data.map(item => ({
id: item.orderNum || item.id, // id orderNum
orderId: item.orderNum || '-', //
customerName: item.customerName || '-', //
identityNo: item.customerIdCard || '-', //
serviceUserName: item.serviceName || '-', //
itemNames: item.projects?.map(p => p.name).join('、') || '-', //
realStartTime: item.signInAt, // formatUtcDateTime
realEndTime: item.signOutAt, //
processStatus: item.processStatus === '1' ? '已处理' : item.processStatus === '0' ? '未处理' : '未知', //
processRemark: item.reason || '-', //
processUserName: item.userName || '-', //
processTime: dayjs(item.processAt).format('YYYY-MM-DD HH:mm:ss'), //
serviceAddress: item.detailAddress || '-', //
// 便 id
raw: item,
}))
listData.value = mappedData
paginationState.total = total
hideLoading()
} catch (error) {
console.error('获取工单列表失败:', error)
hideLoading()
message.error('获取数据失败')
}
}
/**
* 搜索
*/
function handleSearch() {
// resetForm()
resetPagination()
getMenuList()
}
/**
* 重置
*/
function handleResetSearch() {
searchFormData.value = {}
resetPagination()
getMenuList()
}
/**
* 删除
* @param id
*/
function handleDelete({ id }) {
Modal.confirm({
title: t('pages.system.menu.delTip'),
content: t('button.confirm'),
okText: t('button.confirm'),
onOk: () => {
return new Promise((resolve, reject) => {
; (async () => {
try {
const { success } = await apis.menu.delMenu(id).catch(() => {
throw new Error()
})
if (config('http.code.success') === success) {
resolve()
message.success(t('component.message.success.delete'))
await getMenuList()
}
} catch (error) {
reject()
}
})()
})
},
})
}
/**
* 编辑完成
*/
async function onOk() {
await getMenuList()
}
//
function handleSingleProcess(record) {
currentProcessRecords = [record]
processForm.processStatus = record.processStatus || undefined
processForm.processRemark = record.processRemark || ''
processModalVisible.value = true
}
//
async function handleProcessSubmit() {
const { processStatus } = processForm
if (!processStatus ) {
message.warning('请选择处理结果')
return
}
processSubmitting.value = true
// try {
// // API apis.order.batchProcess({ ids, processStatus, processRemark })
// const ids = currentProcessRecords.map(r => r.id)
// const { success } = await apis.order.batchProcess({
// ids,
// processStatus,
// processRemark,
// })
// if (config('http.code.success') === success) {
// message.success('')
// processModalVisible.value = false
// await getOrderList() //
// }
// } catch (error) {
// message.error('')
// } finally {
// processSubmitting.value = false
// }
}
async function handleView(record = {}) {
showModal({
type: 'view',
title: '查看详情',
})
const { data } = await apis.menu.getMenu(record.id).catch(() => {
throw new Error()
})
formData.value = cloneDeep(data)
formData.value.properties = formData.value.properties ? JSON.parse(formData.value.properties) : ''
formData.value.resources = formData.value.resources || (formData.value.resources = [])
platform.value=data.platform
}
</script>
<style lang="less" scoped></style>

View File

@ -34,7 +34,7 @@
<a-col v-bind="colSpan"> <a-col v-bind="colSpan">
<a-form-item :label="'所在区域'" name="name"> <a-form-item :label="'所在区域'" name="name">
<a-input :placeholder="'请选择区域'" v-model:value="searchFormData.name"></a-input> <AreaCascader v-model:value="searchFormData.currentNode" @change="onAreaChange" />
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -134,6 +134,7 @@ import { formatUtcDateTime } from '@/utils/util'
import EditDialog from './components/EditDialog.vue' import EditDialog from './components/EditDialog.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import AreaCascader from '@/components/AreaCascader/index.vue'
defineOptions({ defineOptions({
// eslint-disable-next-line vue/no-reserved-component-names // eslint-disable-next-line vue/no-reserved-component-names
name: 'menu', name: 'menu',
@ -169,25 +170,27 @@ getMenuList()
async function getMenuList() { async function getMenuList() {
try { try {
showLoading() showLoading()
// const { current } = paginationState const { pageSize, current } = paginationState
const platform = storage.local.getItem('platform') const params = {
const { data, success, total } = await apis.menu pageSize,
.getMenuList({ current,
...searchFormData.value, ...searchFormData.value
platform }
})
.catch(() => { params.status = 'Invalid_WorkerOrder'
throw new Error()
}) console.log("=====search params", params)
hideLoading()
const { success, data, total } = await apis.workOrder.getWorkOrderList(params)
if (config('http.code.success') === success) { if (config('http.code.success') === success) {
data.forEach((item) => {
item.name = t(item.code) || item.name
})
listData.value = data listData.value = data
paginationState.total = total paginationState.total = total
} }
} catch (error) { } catch (error) {
console.error('获取工单列表失败:', error)
message.error('获取数据失败')
} finally {
hideLoading() hideLoading()
} }
} }

View File

@ -41,15 +41,15 @@
</template> </template>
</x-search-bar> </x-search-bar>
<a-card> <a-card>
<x-action-bar class="mb-8-2"> <!-- <x-action-bar class="mb-8-2">
<a-button v-action="'add'" type="primary" @click="$refs.editDialogRef.handleCreate()"> <a-button v-action="'add'" type="primary" @click="$refs.editDialogRef.handleCreate()">
<template #icon> <template #icon>
<plus-outlined></plus-outlined> <plus-outlined></plus-outlined>
</template> </template>
{{ $t('pages.system.menu.add') }} {{ $t('pages.system.menu.add') }}
</a-button> </a-button>
</x-action-bar> </x-action-bar> -->
<a-table rowKey="id" :loading="loading" :pagination="true" :columns="columns" :data-source="listData"> <a-table rowKey="id" :loading="loading" :pagination="true" :columns="columns" :data-source="listData" :scroll="{ x: 'max-content' }">
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="'menuType' === column.key"> <template v-if="'menuType' === column.key">
<!--菜单--> <!--菜单-->
@ -123,21 +123,19 @@ defineOptions({
}) })
const { t } = useI18n() // t const { t } = useI18n() // t
const columns = ref([ const columns = ref([
{ title: '工单号', dataIndex: 'name', key: 'name', fixed: true, width: 280 }, { title: '工单号', dataIndex: 'orderId', key: 'orderId', width: 180 },
{ title: '服务对象', dataIndex: 'code', key: 'code', width: 240 }, { title: '服务对象', dataIndex: 'customerName', key: 'customerName', width: 150 },
{ title: '身份证号', dataIndex: 'type', key: 'menuType', width: 240 }, { title: '身份证号', dataIndex: 'identityNo', key: 'identityNo', width: 300 },
{ title: '联系方式1', dataIndex: 'status', key: 'statusType', width: 240 }, { title: '联系方式1', dataIndex: 'contact1', key: 'contact1', width: 150 },
{ title: '联系方式2', dataIndex: 'sequence', width: 240 }, { title: '联系方式2', dataIndex: 'contact2', key: 'contact2', width: 150 },
{ title: '计划服务时间', dataIndex: 'created_at', key: 'createAt', width: 240 }, { title: '计划服务时常(分钟)', dataIndex: 'workDuration', key: 'workDuration', width: 160 },
{ title: '计划服务时常(分钟)', dataIndex: 'created_at', key: 'createAt', width: 240 }, { title: '服务项目', dataIndex: 'itemNames', key: 'itemNames', width: 200 },
{ title: '服务项目', dataIndex: 'created_at', key: 'createAt', width: 240 }, { title: '所属区域', dataIndex: 'areaLabels', key: 'areaLabels', width: 200, customRender: ({ text }) => text?.join('、') || '-' },
{ title: '所属区域', dataIndex: 'created_at', key: 'createAt', width: 240 }, { title: '服务地址', dataIndex: 'serviceAddress', key: 'serviceAddress', width: 200 },
{ title: '服务地址', dataIndex: 'created_at', key: 'createAt', width: 240 }, { title: '服务人员', dataIndex: 'serviceUserName', key: 'serviceUserName', width: 150 },
{ title: '服务组织', dataIndex: 'created_at', key: 'createAt', width: 240 }, { title: '下单员', dataIndex: 'processUserName', key: 'processUserName', width: 150 },
{ title: '服务人员', dataIndex: 'created_at', key: 'createAt', width: 240 }, { title: '下单时间', dataIndex: 'createAt', key: 'createAt', width: 180, customRender: ({ record }) => formatUtcDateTime(record.raw.createAt) },
{ title: '下单员', dataIndex: 'created_at', key: 'createAt', width: 240 }, ]);
{ title: '下单时间', dataIndex: 'created_at', key: 'createAt', width: 240 },
])
const { listData, loading, showLoading, hideLoading, searchFormData, paginationState, resetPagination } = const { listData, loading, showLoading, hideLoading, searchFormData, paginationState, resetPagination } =
usePagination() usePagination()
const { resetForm } = useForm() const { resetForm } = useForm()
@ -152,26 +150,44 @@ getMenuList()
async function getMenuList() { async function getMenuList() {
try { try {
showLoading() showLoading()
// const { current } = paginationState const { pageSize, current } = paginationState
const platform = storage.local.getItem('platform')
const { data, success, total } = await apis.menu const { success, data, total } = await apis.workOrder.getMyWorkOrderList({
.getMenuList({ pageSize,
current: current,
...searchFormData.value, ...searchFormData.value,
platform
}) })
.catch(() => {
throw new Error() //
}) const mappedData = data.map(item => {
hideLoading() // 2 otherPhone1 otherPhone2
if (config('http.code.success') === success) { const contact2 = item.otherPhone1 || item.otherPhone2 || '-';
data.forEach((item) => {
item.name = t(item.code) || item.name return {
}) id: item.orderNum || item.customerIdCard, //
listData.value = data orderId: item.orderNum || '-',
customerName: item.customerName || '-',
identityNo: item.customerIdCard || '-',
contact1: item.contact1 || '-',
contact2: contact2,
workDuration: item.workDuration ? `${item.workDuration} 分钟` : '-',
itemNames: item.projects?.map(p => p.name).join('、') || '-',
areaLabels: item.areaLabels || [],
serviceAddress: item.detailAddress || '-',
serviceUserName: item.serviceName || '-',
processUserName: item.userName || '-',
createAt: item.createAt, //
raw: item, //
};
});
listData.value = mappedData
paginationState.total = total paginationState.total = total
}
} catch (error) {
hideLoading() hideLoading()
} catch (error) {
console.error('获取工单列表失败:', error)
hideLoading()
message.error('获取数据失败')
} }
} }
/** /**

View File

@ -0,0 +1,198 @@
<template>
<a-modal
:width="520"
:open="modal.open"
:title="modal.title"
:confirm-loading="modal.confirmLoading"
:after-close="onAfterClose"
:cancel-text="cancelText"
:ok-text="okText"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form layout="vertical" ref="formRef" :model="formData" :rules="formRules">
<!-- 签入时间 -->
<a-form-item
label="签入时间"
name="signTime"
required
>
<a-date-picker
v-model:value="formData.signTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
placeholder="选择时间"
style="width: 100%"
/>
</a-form-item>
<!-- 代签入原因 -->
<a-form-item
label="代签入原因"
name="reason"
required
>
<a-textarea
v-model:value="formData.reason"
placeholder="请填写1-100字的原因"
:maxlength="100"
:auto-size="{ minRows: 4, maxRows: 6 }"
/>
</a-form-item>
<!-- 代签入图片 -->
<a-form-item
label="代签入图片"
name="images"
>
<a-upload
v-model:file-list="fileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="dummyRequest"
:multiple="true"
accept="image/*"
>
<div>
<PlusOutlined />
<div style="margin-top: 8px">选择图片</div>
</div>
</a-upload>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { useForm } from '@/hooks'
import { useModal } from '@/hooks'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
const emit = defineEmits(['ok'])
const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
const { formData, formRef, formRules, resetForm } = useForm()
//
const fileList = ref([])
//
formData.value = reactive({
signTime: null,
reason: '',
images: [] // File URL
})
//
formRules.value = {
signTime: [
{ required: true, message: '请选择签入时间' }
],
reason: [
{ required: true, message: '请填写代签入原因' },
{ max: 100, message: '原因不能超过100字' }
]
}
//
const cancelText = ref('取消')
const okText = ref('确定')
//
const currentRecord = ref(null)
//
function showCheckModal(record) {
currentRecord.value = record
showModal({
type: 'cancel',
title: '代签入工单'
})
formData.value.signTime = null
formData.value.reason = ''
fileList.value = []
}
//
function beforeUpload(file) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type.startsWith('image/')
if (!isJpgOrPng) {
message.error('只能上传 JPG/PNG 等图片文件!')
return false
}
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
message.error('图片大小不能超过 5MB!')
return false
}
// fileListAnt Upload
return true
}
// Ant Design API
function dummyRequest(options) {
const { onSuccess, file } = options
setTimeout(() => {
onSuccess('ok', file)
}, 0)
}
//
async function handleOk() {
try {
const values = await formRef.value.validateFields()
showLoading()
//
const files = fileList.value
.filter(f => f.status === 'done')
.map(f => f.originFileObj || f)
// API
// await apis.workOrder.proxySignIn({
// id: currentRecord.value?.id,
// signTime: dayjs(formData.value.signTime).format('YYYY-MM-DD HH:mm:ss'),
// reason: formData.value.reason,
// images: files
// })
hideLoading()
hideModal()
emit('ok', {
signTime: formData.value.signTime,
reason: formData.value.reason,
images: files
})
} catch (error) {
hideLoading()
console.error('表单验证失败:', error)
}
}
//
function handleCancel() {
hideModal()
}
//
function onAfterClose() {
resetForm()
fileList.value = []
}
//
defineExpose({
showCheckModal
})
</script>
<style scoped>
/* 可选:调整上传区域样式 */
:deep(.ant-upload-select-picture-card) {
width: 104px;
height: 104px;
}
</style>

View File

@ -0,0 +1,299 @@
<template>
<a-modal
:width="780"
:open="modal.open"
:title="modal.title"
:confirm-loading="modal.confirmLoading"
:after-close="onAfterClose"
:cancel-text="cancelText"
:ok-text="okText"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
>
<!-- Row + Col 实现两列排版 -->
<a-row :gutter="24">
<!-- 服务对象只读 -->
<a-col :span="12">
<a-form-item label="服务对象" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-input v-model:value="formData.serviceTarget" disabled />
</a-form-item>
</a-col>
<!-- 服务人员下拉选择 -->
<a-col :span="12">
<a-form-item label="服务人员" name="serviceStaff" required :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-select
v-model:value="formData.serviceStaff"
placeholder="请选择服务人员"
:options="staffOptions"
/>
</a-form-item>
</a-col>
<!-- 服务项目单选下拉 -->
<a-col :span="12">
<a-form-item label="服务项目" name="serviceItems" required :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-select
v-model:value="formData.serviceItems"
placeholder="请选择服务项目"
:options="serviceItemOptions"
/>
</a-form-item>
</a-col>
<!-- 服务费用普通输入框 -->
<a-col :span="12">
<a-form-item label="服务费用" name="serviceFee" required :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-input
v-model:value="formData.serviceFee"
placeholder="请填写服务费用"
type="number"
/>
</a-form-item>
</a-col>
<!-- 计划日期范围占满一行 -->
<a-col :span="12">
<a-form-item label="计划开始日期" name="planDateRange" required :label-col="{ span:8}" :wrapper-col="{ span: 16 }">
<a-range-picker
v-model:value="formData.planDateRange"
format="YYYY-MM-DD"
:placeholder="['开始日期', '结束日期']"
/>
</a-form-item>
</a-col>
<!-- 计划开始时间 -->
<a-col :span="12">
<a-form-item label="计划开始时间" name="planStartTime" required :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
<a-time-picker
v-model:value="formData.planStartTime"
format="HH:mm:ss"
placeholder="请选择计划开始时间"
style="width: 100%"
/>
</a-form-item>
</a-col>
<!-- 要求工单时长分钟 -->
<a-col :span="12">
<a-form-item label="要求工单时长" name="requiredDuration" required :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
<a-input-number
v-model:value="formData.requiredDuration"
:min="0"
:step="1"
style="width: 80%"
placeholder="请填写要求工单时长"
/>
<span style="margin-left: 8px">分钟</span>
</a-form-item>
</a-col>
<!-- 服务地址级联选择占满一行 -->
<a-col :span="24">
<a-form-item label="服务地址" name="serviceArea" required :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
<a-cascader
v-model:value="formData.serviceArea"
:options="areaOptions"
change-on-select
placeholder="请选择服务地址"
style="width: 100%"
/>
</a-form-item>
</a-col>
<!-- 详细地址占满一行 -->
<a-col :span="24">
<a-form-item label="详细地址" name="detailAddress" required :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
<a-input v-model:value="formData.detailAddress" placeholder="请填写详细地址" />
</a-form-item>
</a-col>
<!-- 地图定位地址只读+图标占满一行 -->
<a-col :span="24">
<a-form-item label="地图定位地址" :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
<div style="display: flex; align-items: center;">
<span style="margin-right: 8px; cursor: pointer;" @click="openMap">📍</span>
<a-input v-model:value="formData.mapAddress" readonly />
</div>
</a-form-item>
</a-col>
<!-- 备注占满一行 -->
<a-col :span="24">
<a-form-item label="备注" :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
<a-textarea
v-model:value="formData.remark"
placeholder="请填写备注"
:auto-size="{ minRows: 2, maxRows: 4 }"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useForm } from '@/hooks'
import { useModal } from '@/hooks'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
//
const staffOptions = [
{ value: '邢光平', label: '邢光平' },
{ value: '张三', label: '张三' },
{ value: '李四', label: '李四' }
]
const serviceItemOptions = [
{ value: '兴趣活动', label: '兴趣活动' },
{ value: '生活照料', label: '生活照料' },
{ value: '康复训练', label: '康复训练' }
]
const areaOptions = [
{
value: '江苏省',
label: '江苏省',
children: [
{
value: '南通市',
label: '南通市',
children: [
{
value: '通州区',
label: '通州区',
children: [
{
value: '金新街道',
label: '金新街道',
children: [
{ value: '大石桥村委会', label: '大石桥村委会' }
]
}
]
}
]
}
]
}
]
// &
const emit = defineEmits(['ok'])
const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
const { formData, formRef, formRules, resetForm } = useForm()
//
formData.value = reactive({
serviceTarget: '袁正芬', //
serviceStaff: '邢光平', //
serviceItems: '兴趣活动', //
serviceFee: '40', //
planDateRange: [dayjs('2025-10-17'), dayjs('2025-10-31')], //
planStartTime: dayjs('00:03:00', 'HH:mm:ss'), //
requiredDuration: 90, //
serviceArea: ['江苏省', '南通市', '通州区', '金新街道', '大石桥村委会'], //
detailAddress: '南通市通州区川姜镇大石桥村九组131号', //
mapAddress: '', //
remark: '' //
})
//
formRules.value = {
serviceStaff: [{ required: true, message: '请选择服务人员' }],
serviceItems: [{ required: true, message: '请选择服务项目' }],
serviceFee: [
{ required: true, message: '请填写服务费用' },
{ pattern: /^\d+$/, message: '请输入有效数字' }
],
planDateRange: [{ required: true, message: '请选择计划日期范围' }],
planStartTime: [{ required: true, message: '请选择计划开始时间' }],
requiredDuration: [{ required: true, message: '请填写要求工单时长' }],
serviceArea: [{ required: true, message: '请选择服务地址' }],
detailAddress: [{ required: true, message: '请填写详细地址' }]
}
//
const cancelText = ref('取消')
const okText = ref('保存')
//
function openMap() {
message.info('地图定位功能待实现')
}
//
async function handleOk() {
try {
const values = await formRef.value.validateFields()
showLoading()
//
const payload = {
...values,
planStartDate: values.planDateRange?.[0]?.format('YYYY-MM-DD') || '',
planEndDate: values.planDateRange?.[1]?.format('YYYY-MM-DD') || '',
planStartTime: values.planStartTime?.format('HH:mm:ss') || ''
}
// await apis.workOrder.save(payload)
setTimeout(() => {
hideLoading()
hideModal()
emit('ok', payload)
message.success('保存成功')
}, 500)
} catch (error) {
console.error('表单验证失败:', error)
hideLoading()
}
}
//
function handleCancel() {
hideModal()
}
//
function onAfterClose() {
resetForm()
}
//
defineExpose({
showEditModal(record) {
// record formData
formData.value = reactive({
serviceTarget: record?.serviceTarget || '袁正芬',
serviceStaff: record?.serviceStaff || '邢光平',
serviceItems: record?.serviceItems || '兴趣活动',
serviceFee: record?.serviceFee || '40',
planDateRange: record?.planDateRange
? [dayjs(record.planStartDate), dayjs(record.planEndDate)]
: [dayjs('2025-10-17'), dayjs('2025-10-31')],
planStartTime: record?.planStartTime
? dayjs(record.planStartTime, 'HH:mm:ss')
: dayjs('00:03:00', 'HH:mm:ss'),
requiredDuration: record?.requiredDuration || 90,
serviceArea: record?.serviceArea || ['江苏省', '南通市', '通州区', '金新街道', '大石桥村委会'],
detailAddress: record?.detailAddress || '南通市通州区川姜镇大石桥村九组131号',
mapAddress: record?.mapAddress || '',
remark: record?.remark || ''
})
showModal({
type: 'edit',
title: '编辑工单'
})
}
})
</script>

View File

@ -0,0 +1,98 @@
<template>
<a-modal
:width="520"
:open="modal.open"
:title="modal.title"
:confirm-loading="modal.confirmLoading"
:after-close="onAfterClose"
:cancel-text="cancelText"
:ok-text="okText"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form layout="vertical" ref="formRef" :model="formData" :rules="formRules">
<a-form-item
:label="'作废原因'"
name="reason"
required
>
<a-textarea
v-model:value="formData.reason"
:placeholder="'请填写作废原因'"
:auto-size="{ minRows: 4, maxRows: 6 }"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref } from 'vue'
import { useForm } from '@/hooks'
import { useModal } from '@/hooks'
import { useI18n } from 'vue-i18n'
const emit = defineEmits(['ok'])
const { t } = useI18n()
const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
const { formData, formRef, formRules, resetForm } = useForm()
//
formRules.value = {
reason: [
{ required: true, message: '请填写作废原因' },
{ max: 500, message:'请填写作废原因' }
]
}
//
const cancelText = ref('取消')
const okText = ref('确定')
// props
const currentRecord = ref(null)
// showCancelModal record
function showCancelModal(record) {
currentRecord.value = record //
showModal({
type: 'cancel',
title:'作废工单'
})
formData.value.reason = ''
}
//
async function handleOk() {
try {
await formRef.value.validateFields()
showLoading()
// API
// await apis.workOrder.cancelWorkOrder({ id: props.workOrderId, reason: formData.value.reason })
hideLoading()
hideModal()
emit('ok', formData.value.reason) //
} catch (error) {
hideLoading()
}
}
//
function handleCancel() {
hideModal()
}
//
function onAfterClose() {
resetForm()
}
//
defineExpose({
showCancelModal
})
</script>

View File

@ -0,0 +1,593 @@
<template>
<a-modal
:width="900"
:open="modal.open"
title="查看工单"
:confirm-loading="modal.confirmLoading"
:after-close="onAfterClose"
cancel-text="关闭"
ok-text="确认"
:ok-button-props="{ disabled: true }"
@ok="handleOk"
@cancel="handleCancel"
>
<a-tabs default-active-key="1" :tab-bar-style="{ marginBottom: '16px' }">
<!-- 工单信息页签已替换为现有布局与字段 -->
<a-tab-pane key="1" tab="工单信息">
<a-form layout="vertical" ref="formRef" :model="formData">
<!-- 1. 工单信息 -->
<a-card class="mb-4" title="工单信息">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="工单号" name="workOrderNo">
<a-input v-model:value="formData.workOrderNo" disabled />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="工单状态" name="status">
<a-input v-model:value="formData.status" disabled />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="工单类型" name="type">
<div class="flex items-center">
<a-input v-model:value="formData.type" disabled style="flex: 1" />
<a-button type="link" disabled class="ml-2">修改</a-button>
</div>
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 2. 用户信息 -->
<a-card class="mb-4" title="用户信息">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="姓名" name="userName">
<a-input v-model:value="formData.userName" disabled />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="性别" name="gender">
<a-select v-model:value="formData.gender" disabled>
<a-select-option value="男"></a-select-option>
<a-select-option value="女"></a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="年龄" name="age">
<a-input-number v-model:value="formData.age" disabled style="width: 100%" :min="0" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="身份证号" name="idCard">
<a-input v-model:value="formData.idCard" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系电话" name="phone">
<a-input v-model:value="formData.phone" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="评估等级" name="assessmentLevel">
<a-input v-model:value="formData.assessmentLevel" disabled placeholder="" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 3. 服务信息 -->
<a-card class="mb-4" title="服务信息">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="护理员" name="nurseName">
<a-input v-model:value="formData.nurseName" disabled />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="护理等级" name="nurseLevel">
<a-input v-model:value="formData.nurseLevel" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="计划开始日期" name="planStartDate">
<a-date-picker v-model:value="formData.planStartDate" format="YYYY-MM-DD" disabled style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="计划结束日期" name="planEndDate">
<a-date-picker v-model:value="formData.planEndDate" format="YYYY-MM-DD" disabled style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="计划服务开始时间" name="planStartTime">
<a-time-picker v-model:value="formData.planStartTime" format="HH:mm:ss" disabled style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="服务地址" name="serviceAddress">
<a-input v-model:value="formData.serviceAddress" disabled />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="服务项目">
<a-button type="link" @click="showServiceItems" disabled>项目清单</a-button>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="服务内容" name="serviceContent">
<a-input v-model:value="formData.serviceContent" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="下单人" name="orderer">
<a-input v-model:value="formData.orderer" disabled />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="下单时间" name="orderTime">
<a-date-picker v-model:value="formData.orderTime" show-time format="YYYY-MM-DD HH:mm:ss" disabled style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 4. 签到信息新增模块匹配现有布局 -->
<a-card class="mb-4" title="签到信息">
<a-row :gutter="16">
<!-- 签入基础信息 -->
<a-col :span="12">
<a-form-item label="签入时间" name="checkInTime">
<a-input v-model:value="formData.checkInTime" disabled style="color: red" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="签入距离" name="checkInDistance">
<a-input v-model:value="formData.checkInDistance" disabled />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="签入地址" name="checkInAddress">
<a-input v-model:value="formData.checkInAddress" disabled style="color: red" />
</a-form-item>
</a-col>
<!-- 签入图片 -->
<a-col :span="24">
<a-form-item label="签入图片" name="checkInImage">
<div class="flex items-center">
<a-link v-if="formData.checkInImage" :href="formData.checkInImage" target="_blank" rel="noopener noreferrer">
查看图片
</a-link>
<span v-else>-</span>
<a-button type="primary" size="small" class="ml-4" disabled> </a-button>
</div>
</a-form-item>
</a-col>
<!-- 签入备注与代签入 -->
<a-col :span="24">
<a-form-item label="签入备注" name="checkInRemark">
<a-input v-model:value="formData.checkInRemark" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="代签入操作人" name="proxyCheckInOperator">
<a-input v-model:value="formData.proxyCheckInOperator" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="代签入时间" name="proxyCheckInTime">
<a-date-picker v-model:value="formData.proxyCheckInTime" show-time format="YYYY-MM-DD HH:mm:ss" disabled style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="代签入原因" name="proxyCheckInReason">
<a-input v-model:value="formData.proxyCheckInReason" disabled placeholder="" />
</a-form-item>
</a-col>
<!-- 录音与过程文件 -->
<a-col :span="12">
<a-form-item label="签入录音" name="checkInRecording">
<a-input v-model:value="formData.checkInRecording" disabled placeholder="无" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="进行中的图片" name="inProgressImage">
<div class="flex items-center">
<span v-if="!formData.inProgressImage">-</span>
<a-button type="primary" size="small" class="ml-2" disabled> </a-button>
</div>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="进行中的视频" name="inProgressVideo">
<a-input v-model:value="formData.inProgressVideo" disabled placeholder="" />
</a-form-item>
</a-col>
<!-- 签出与服务时长 -->
<a-col :span="12">
<a-form-item label="代签出操作人" name="proxyCheckOutOperator">
<a-input v-model:value="formData.proxyCheckOutOperator" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="代签出时间" name="proxyCheckOutTime">
<a-date-picker v-model:value="formData.proxyCheckOutTime" show-time format="YYYY-MM-DD HH:mm:ss" disabled style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="代签出原因" name="proxyCheckOutReason">
<a-input v-model:value="formData.proxyCheckOutReason" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="计划服务时长" name="plannedServiceDuration">
<a-input v-model:value="formData.plannedServiceDuration" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="标准服务时长" name="standardServiceDuration">
<a-input v-model:value="formData.standardServiceDuration" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="标准服务费用" name="standardServiceFee">
<a-input v-model:value="formData.standardServiceFee" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="实际服务时长" name="actualServiceDuration">
<a-input v-model:value="formData.actualServiceDuration" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="签出录音" name="checkOutRecording">
<a-input v-model:value="formData.checkOutRecording" disabled placeholder="无" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="全程录音" name="fullRecording">
<a-input v-model:value="formData.fullRecording" disabled placeholder="无" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 5. 服务小结与反馈 -->
<a-card class="mb-4" title="服务小结与反馈">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="服务小结" name="summary">
<a-input v-model:value="formData.summary" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="用户需求反馈" name="feedback">
<a-input v-model:value="formData.feedback" disabled placeholder="" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 6. 异常工单处理信息 -->
<a-card class="mb-4" title="异常工单处理信息">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="处理状态" name="exceptionStatus">
<a-input v-model:value="formData.exceptionStatus" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="处理原因" name="exceptionReason">
<a-input v-model:value="formData.exceptionReason" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="处理人" name="exceptionHandler">
<a-input v-model:value="formData.exceptionHandler" disabled placeholder="" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="处理时间" name="exceptionTime">
<a-date-picker v-model:value="formData.exceptionTime" show-time format="YYYY-MM-DD HH:mm:ss" disabled style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-card>
</a-form>
</a-tab-pane>
<!-- 服务评价页签完全保留原内容未修改 -->
<a-tab-pane key="2" tab="服务评价">
<a-space direction="vertical" style="width: 100%">
<!-- 回访评价 -->
<a-card title="回访评价" size="small">
<a-descriptions :column="{ xs: 1, sm: 2, md: 2 }" bordered>
<a-descriptions-item label="服务满意度">
{{ formData.visitSatisfaction || '-' }}
</a-descriptions-item>
<a-descriptions-item label="服务人员满意度">
{{ formData.staffSatisfaction || '-' }}
</a-descriptions-item>
<a-descriptions-item label="服务反馈" :span="2">
{{ formData.visitFeedback || '-' }}
</a-descriptions-item>
<a-descriptions-item label="回访人">
{{ formData.visitor || '-' }}
</a-descriptions-item>
<a-descriptions-item label="回访时间">
{{
formData.visitTime
? dayjs(formData.visitTime).format('YYYY-MM-DD HH:mm:ss')
: '-'
}}
</a-descriptions-item>
</a-descriptions>
</a-card>
<!-- 服务评价 -->
<a-card title="服务评价" size="small" style="margin-top: 16px">
<a-descriptions :column="{ xs: 1, sm: 2, md: 2 }" bordered>
<a-descriptions-item label="服务满意度">
{{ formData.serviceSatisfaction || '-' }}
</a-descriptions-item>
<a-descriptions-item label="评价渠道">
{{ formData.evaluationChannel || '-' }}
</a-descriptions-item>
<a-descriptions-item label="评价内容" :span="2">
{{ formData.evaluationContent || '-' }}
</a-descriptions-item>
<a-descriptions-item label="评价人">
{{ formData.evaluator || '-' }}
</a-descriptions-item>
<a-descriptions-item label="评价时间">
{{
formData.evaluationTime
? dayjs(formData.evaluationTime).format('YYYY-MM-DD HH:mm:ss')
: '-'
}}
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-space>
</a-tab-pane>
</a-tabs>
</a-modal>
</template>
<script setup>
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { useModal, useForm } from '@/hooks'
import dayjs from 'dayjs'
const emit = defineEmits(['ok'])
const { modal, showModal, hideModal } = useModal()
const { formData, formRef, resetForm } = useForm()
//
const initFormData = () => ({
//
workOrderNo: '',
status: '',
type: '',
userName: '',
gender: '',
age: null,
idCard: '',
phone: '',
assessmentLevel: '',
nurseName: '',
nurseLevel: '',
planStartDate: null,
planEndDate: null,
planStartTime: null,
serviceAddress: '',
serviceContent: '',
orderer: '',
orderTime: null,
summary: '',
feedback: '',
exceptionStatus: '',
exceptionReason: '',
exceptionHandler: '',
exceptionTime: null,
//
checkInTime: '', // 2025-10-17 11:19:59
checkInDistance: '', // 12163 m
checkInAddress: '', // ...
checkInImage: '', //
checkInRemark: '', //
proxyCheckInOperator: '', //
proxyCheckInTime: null, //
proxyCheckInReason: '', //
checkInRecording: '无', //
inProgressImage: '', //
inProgressVideo: '', //
proxyCheckOutOperator: '', //
proxyCheckOutTime: null, //
proxyCheckOutReason: '', //
plannedServiceDuration: '', // 90
standardServiceDuration: '', //
standardServiceFee: '', //
actualServiceDuration: '', //
checkOutRecording: '无', //
fullRecording: '无', //
//
visitSatisfaction: '',
staffSatisfaction: '',
visitFeedback: '',
visitor: '',
visitTime: null,
serviceSatisfaction: '',
evaluationContent: '',
evaluator: '',
evaluationTime: null,
evaluationChannel: '',
})
//
formData.value = initFormData()
// ================== Methods ==================
/**
* 查看工单唯一打开弹框的方法
* @param {Object} record - 工单数据需包含新增的签到信息字段
*/
function handleView(record = {}) {
showModal({ mode: 'view', title: '查看工单' })
loadRecord(record)
}
/**
* 加载工单数据到表单新增签到信息字段的赋值逻辑
* @param {Object} record - 工单数据
*/
function loadRecord(record) {
formData.value = {
...initFormData(),
//
workOrderNo: record.workOrderNo || '',
status: record.status || '',
type: record.type || '',
userName: record.userName || '',
gender: record.gender || '',
age: record.age || null,
idCard: record.idCard || '',
phone: record.phone || '',
assessmentLevel: record.assessmentLevel || '',
nurseName: record.nurseName || '',
nurseLevel: record.nurseLevel || '',
planStartDate: record.planStartDate ? dayjs(record.planStartDate) : null,
planEndDate: record.planEndDate ? dayjs(record.planEndDate) : null,
planStartTime: record.planStartTime ? dayjs(`1970-01-01 ${record.planStartTime}`) : null,
serviceAddress: record.serviceAddress || '',
serviceContent: record.serviceContent || '',
orderer: record.orderer || '',
orderTime: record.orderTime ? dayjs(record.orderTime) : null,
summary: record.summary || '',
feedback: record.feedback || '',
exceptionStatus: record.exceptionStatus || '',
exceptionReason: record.exceptionReason || '',
exceptionHandler: record.exceptionHandler || '',
exceptionTime: record.exceptionTime ? dayjs(record.exceptionTime) : null,
// record
checkInTime: record.checkInTime || '', // 2025-10-17 11:19:59
checkInDistance: record.checkInDistance || '', // 12163 m
checkInAddress: record.checkInAddress || '', //
checkInImage: record.checkInImage || '', //
checkInRemark: record.checkInRemark || '',
proxyCheckInOperator: record.proxyCheckInOperator || '',
proxyCheckInTime: record.proxyCheckInTime ? dayjs(record.proxyCheckInTime) : null,
proxyCheckInReason: record.proxyCheckInReason || '',
inProgressImage: record.inProgressImage || '',
inProgressVideo: record.inProgressVideo || '',
proxyCheckOutOperator: record.proxyCheckOutOperator || '',
proxyCheckOutTime: record.proxyCheckOutTime ? dayjs(record.proxyCheckOutTime) : null,
proxyCheckOutReason: record.proxyCheckOutReason || '',
plannedServiceDuration: record.plannedServiceDuration || '', // 90
standardServiceDuration: record.standardServiceDuration || '',
standardServiceFee: record.standardServiceFee || '',
actualServiceDuration: record.actualServiceDuration || '',
//
visitSatisfaction: record.visitSatisfaction || '',
staffSatisfaction: record.staffSatisfaction || '',
visitFeedback: record.visitFeedback || '',
visitor: record.visitor || '',
visitTime: record.visitTime ? dayjs(record.visitTime) : null,
serviceSatisfaction: record.serviceSatisfaction || '',
evaluationContent: record.evaluationContent || '',
evaluator: record.evaluator || '',
evaluationTime: record.evaluationTime ? dayjs(record.evaluationTime) : null,
evaluationChannel: record.evaluationChannel || '',
}
}
/**
* 确认按钮禁用状态仅保留逻辑
*/
function handleOk() {
hideModal()
emit('ok')
}
/**
* 取消/关闭按钮
*/
function handleCancel() {
hideModal()
}
/**
* 弹框关闭后重置表单
*/
function onAfterClose() {
resetForm()
}
/**
* 服务项目清单查看模式下禁用保留提示
*/
function showServiceItems() {
message.info('服务项目清单')
}
//
defineExpose({
handleView,
})
</script>
<style scoped>
.mb-4 {
margin-bottom: 16px;
}
.ant-descriptions-title {
font-weight: bold;
margin-bottom: 12px;
}
.ant-descriptions-view table {
width: 100%;
table-layout: fixed;
}
.ant-descriptions-item-label {
display: inline-block;
min-width: 100px;
font-weight: normal;
}
.ant-descriptions-item-content {
display: inline-block;
padding-left: 8px;
}
/* 适配新增的签入信息模块样式 */
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.ml-2 {
margin-left: 8px;
}
.ml-4 {
margin-left: 16px;
}
</style>

View File

@ -0,0 +1,585 @@
<template>
<!-- 用a-card包裹Tabs和搜索栏形成统一白色区域 -->
<a-card class="tab-search-container" :bordered="false">
<a-tabs v-model:activeKey="activeTabKey" @change="handleTabChange" style="margin-bottom: 16px;">
<a-tab-pane key="all" tab="全部" />
<a-tab-pane key="initialization" tab="初始化" />
<a-tab-pane key="pendingCheckin" tab="待签入" />
<a-tab-pane key="completed" tab="已结单" />
<a-tab-pane key="cancelled" tab="已作废" />
<a-tab-pane key="expiredUncheckedIn" tab="过期未签入" />
<a-tab-pane key="expiredUncheckedOut" tab="过期未签出" />
</a-tabs>
<x-search-bar class="mb-4">
<template #default="{ gutter, colSpan }">
<a-form :label-col="{ style: { width: '100px' } }" :model="searchFormData" layout="inline">
<!-- 基础查询字段 -->
<a-row :gutter="gutter">
<a-col v-bind="colSpan">
<a-form-item label="姓名" name="name">
<a-input v-model:value="searchFormData.name" placeholder="请输入姓名" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item name="code">
<template #label>
身份证号
<a-tooltip title="身份证号">
<question-circle-outlined class="ml-4-1 color-placeholder" />
</a-tooltip>
</template>
<a-input v-model:value="searchFormData.code" placeholder="请输入身份证号" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="实际服务日期" name="actualServiceTime">
<a-range-picker v-model:value="searchFormData.actualServiceTime" />
</a-form-item>
</a-col>
</a-row>
<!-- 高级查询折叠面板 -->
<a-collapse v-model:activeKey="advancedSearchVisible" ghost>
<a-collapse-panel key="1" :showArrow="false" style="padding: 0; border: none;">
<template #header></template>
<a-row :gutter="gutter" style="margin-top: 16px;">
<a-col v-bind="colSpan">
<a-form-item label="工单号" name="workOrderNo">
<a-input v-model:value="searchFormData.workOrderNo" placeholder="请输入工单号" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="所在区域" name="area">
<AreaCascader v-model:value="searchFormData.currentNode"
@change="onAreaChange" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="所在节点" name="node">
<a-select v-model:value="searchFormData.node" placeholder="请选择所在节点">
<a-select-option value="node1">节点1</a-select-option>
<a-select-option value="node2">节点2</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="服务项目" name="serviceProject">
<a-input v-model:value="searchFormData.serviceProject" placeholder="请输入服务项目" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="服务人员" name="staff">
<a-select v-model:value="searchFormData.staff" placeholder="请选择服务人员">
<a-select-option value="staff1">张三</a-select-option>
<a-select-option value="staff2">李四</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="计划日期" name="plannedDate">
<a-range-picker v-model:value="searchFormData.plannedDate" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="服务项目编号" name="projectCode">
<a-select v-model:value="searchFormData.projectCode" placeholder="请选择项目编号">
<a-select-option value="P001">P001</a-select-option>
<a-select-option value="P002">P002</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="服务对象分类" name="clientCategory">
<a-select v-model:value="searchFormData.clientCategory" placeholder="请选择分类">
<a-select-option value="elderly">老年人</a-select-option>
<a-select-option value="disabled">残障人士</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
<!-- 操作按钮 -->
<a-row :gutter="gutter" style="margin-top: 16px;">
<a-col :span="24" style="text-align: right;">
<a-space>
<a-button @click="handleResetSearch">{{ $t('button.reset') }}</a-button>
<a-button ghost type="primary" @click="handleSearch">
{{ $t('button.search') }}
</a-button>
<a-button type="link" @click="toggleAdvancedSearch"
style="color: #1890ff; border: none; padding: 0; font-size: 14px;">
{{ advancedSearchVisible.length ? '收起' : '高级查询' }}
<template #icon>
<down-outlined v-if="advancedSearchVisible.length" />
<up-outlined v-else />
</template>
</a-button>
</a-space>
</a-col>
</a-row>
</a-form>
</template>
</x-search-bar>
</a-card>
<!-- 表格区域 -->
<a-card style="margin-top: 16px;">
<x-action-bar class="mb-8-2">
<a-button v-action="'add'" type="primary" @click="$refs.editDialogRef.handleCreate()"
style="margin-right: 20px;">
<template #icon>
<!-- <plus-outlined /> -->
</template>
{{ '导出' }}
</a-button>
<a-button v-action="'add'" type="primary" @click="$refs.editDialogRef.handleCreate()">
<template #icon>
<!-- <plus-outlined /> -->
</template>
{{ '导出记录' }}
</a-button>
</x-action-bar>
<a-table rowKey="id" :loading="loading" :pagination="true" :columns="currentColumns" :data-source="listData"
:scroll="{ x: 'max-content' }">
<!-- 表格列渲染逻辑保持不变 -->
<template #bodyCell="{ column, record }">
<template v-if="'menuType' === column.key">
<a-tag v-if="menuTypeEnum.is('page', record.type)" color="processing">
{{ menuTypeEnum.getDesc(record.type) }}
</a-tag>
<a-tag v-if="menuTypeEnum.is('button', record.type)" color="success">
{{ menuTypeEnum.getDesc(record.type) }}
</a-tag>
</template>
<template v-if="'createAt' === column.key">
{{ formatUtcDateTime(record.created_at) }}
</template>
<template v-if="'statusType' === column.key">
<a-tag v-if="statusTypeEnum.is('enabled', record.status)" color="processing">
{{ statusTypeEnum.getDesc(record.status) }}
</a-tag>
<a-tag v-if="statusTypeEnum.is('disabled', record.status)" color="default">
{{ statusTypeEnum.getDesc(record.status) }}
</a-tag>
</template>
<template v-if="'action' === column.key">
<template v-if="['all', 'initialization', 'pendingCheckin'].includes(activeTabKey)">
<x-action-button @click="$refs.cancelDialogRef.showCancelModal(record)">
<a-tooltip><template #title>
{{ '作废' }}</template>作废
</a-tooltip>
</x-action-button>
</template>
<template v-if="['all', 'initialization'].includes(activeTabKey)">
<x-action-button @click="$refs.editDialogRef.showEditModal(record)">
<a-tooltip><template #title>编辑</template>编辑</a-tooltip>
</x-action-button>
</template>
<template v-if="['all', 'completed', 'cancelled'].includes(activeTabKey)">
<x-action-button @click="$refs.viewDialogRef.handleView(record)">
<a-tooltip><template #title>详情</template>详情</a-tooltip>
</x-action-button>
</template>
<template v-if="['all', 'expiredUncheckedIn', 'expiredUncheckedOut'].includes(activeTabKey)">
<x-action-button @click="$refs.checkDialogRef.showCheckModal(record)">
<a-tooltip><template #title>待签入</template>待签入</a-tooltip>
</x-action-button>
</template>
</template>
</template>
</a-table>
</a-card>
<edit-dialog @ok="onOk" ref="editDialogRef" />
<CancelWorkOrder @ok="onOk" ref="cancelDialogRef" />
<view-dialog @ok="onOk" ref="viewDialogRef" />
<check-dialog @ok="onOk" ref="checkDialogRef" />
<!-- 作废确认模态 -->
<!-- <a-modal
v-model:open="cancelModalVisible"
title="作废确认"
@ok="confirmCancel"
@cancel="cancelModalVisible = false"
>
<p>确定要作废该工单吗此操作不可撤销</p>
<a-textarea
v-model:value="cancelReason"
placeholder="请输入作废原因(可选)"
:rows="4"
/>
</a-modal> -->
</template>
<script setup>
import { DownOutlined, UpOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue'
import { Modal, message } from 'ant-design-vue'
import { ref, computed } from 'vue'
import apis from '@/apis'
import { config } from '@/config'
import { menuTypeEnum, statusTypeEnum } from '@/enums/system'
import { usePagination, useForm } from '@/hooks'
import { formatUtcDateTime } from '@/utils/util'
import EditDialog from './components/EditDialog.vue'
import CancelWorkOrder from './components/InvalidatedDialog.vue'
import ViewDialog from './components/ViewDialog.vue'
import CheckDialog from './components/CheckDialog.vue'
import { useI18n } from 'vue-i18n'
import storage from '@/utils/storage'
import AreaCascader from '@/components/AreaCascader/index.vue'
import dayjs from 'dayjs'
defineOptions({
name: 'menu',
})
const { t } = useI18n()
const activeTabKey = ref('all') // ""
//
const cancelModalVisible = ref(false)
const cancelRecord = ref(null)
const cancelReason = ref('')
//
const columnConfigs = ref({
all: [
// { title: '', dataIndex: 'orderNum', key: 'orderNum', fixed: true, width: 280 },
{ title: '服务对象', dataIndex: 'customerName', key: 'customerName', width: 140 },
{ title: '服务对象身份证', dataIndex: 'customerIdCard', key: 'customerIdCard', width: 240 },
{
title: '服务对象分类', dataIndex: 'labels', key: 'labels', width: 140,
render: (labels) => Array.isArray(labels) ? labels.join(', ') : ''
},
{ title: '服务人员', dataIndex: 'serviceName', key: 'serviceName', width: 140 },
{
title: '计划日期',
dataIndex: 'plannedStartDate',
key: 'plannedStartDate',
width: 240,
customRender: ({ text, record }) => {
if (!text) return '';
try {
// dayjs
const formatted = dayjs(text).format('YYYY-MM-DD HH:mm:ss');
return formatted;
} catch (e) {
console.error('日期格式化失败:', text, e);
return text; // return ''
}
}
},
{
title: '服务项目分类',
// projects[0].categoryType
render: (_, record) => record.projects?.[0]?.categoryType || record.projects?.[0]?.categoryMethod || '',
key: 'serviceCategory',
width: 240
},
{
title: '服务项目',
render: (_, record) => record.projects?.[0]?.name || '',
key: 'serviceNameProject',
width: 240
},
{
title: '服务费用',
render: (_, record) => record.projects?.[0]?.price ?? 0,
key: 'servicePrice',
width: 140
},
{
title: '实际支付费用',
// actualPaid
render: (_, record) => record.projects?.[0]?.price ?? 0,
key: 'actualPaid',
width: 140
},
{
title: '计划服务时间(分钟)',
dataIndex: 'workDuration',
key: 'workDuration',
width: 240
},
{ title: '服务状态', dataIndex: 'status', key: 'status', width: 180 },
{ title: '操作', key: 'action', width: 440, fixed: 'right' }
],
initialization: [
{ title: '工单号', dataIndex: 'name', key: 'name', fixed: true, width: 280 },
{ title: '服务对象', dataIndex: 'code', key: 'code', width: 240 },
{ title: '服务项目', dataIndex: 'project', key: 'project', width: 240 },
{ title: '计划服务时间', dataIndex: 'scheduledTime', key: 'scheduledTime', width: 240 },
{ title: '服务组织', dataIndex: 'organization', key: 'organization', width: 240 },
{ title: '操作', key: 'action', width: 240, fixed: 'right' }
],
pendingCheckin: [
{ title: '工单号', dataIndex: 'name', key: 'name', fixed: true, width: 280 },
{ title: '服务对象', dataIndex: 'code', key: 'code', width: 240 },
{ title: '服务地址', dataIndex: 'address', key: 'address', width: 300 },
{ title: '计划服务时间', dataIndex: 'scheduledTime', key: 'scheduledTime', width: 240 },
{ title: '服务人员', dataIndex: 'staff', key: 'staff', width: 240 },
{ title: '操作', key: 'action', width: 240, fixed: 'right' }
],
completed: [
{ title: '工单号', dataIndex: 'name', key: 'name', fixed: true, width: 280 },
{ title: '服务对象', dataIndex: 'code', key: 'code', width: 240 },
{ title: '服务项目', dataIndex: 'project', key: 'project', width: 240 },
{ title: '实际服务时间', dataIndex: 'actualTime', key: 'actualTime', width: 240 },
{ title: '服务人员', dataIndex: 'staff', key: 'staff', width: 240 },
{ title: '操作', key: 'action', width: 240, fixed: 'right' }
],
cancelled: [
{ title: '工单号', dataIndex: 'name', key: 'name', fixed: true, width: 280 },
{ title: '服务对象', dataIndex: 'code', key: 'code', width: 240 },
{ title: '作废原因', dataIndex: 'cancelReason', key: 'cancelReason', width: 300 },
{ title: '作废时间', dataIndex: 'cancelTime', key: 'cancelTime', width: 240 },
{ title: '操作人', dataIndex: 'operator', key: 'operator', width: 240 },
{ title: '操作', key: 'action', width: 240, fixed: 'right' }
],
expiredUncheckedIn: [
{ title: '工单号', dataIndex: 'name', key: 'name', fixed: true, width: 280 },
{ title: '服务对象', dataIndex: 'code', key: 'code', width: 240 },
{ title: '计划服务时间', dataIndex: 'scheduledTime', key: 'scheduledTime', width: 240 },
{ title: '逾期天数', dataIndex: 'overdueDays', key: 'overdueDays', width: 180 },
{ title: '服务人员', dataIndex: 'staff', key: 'staff', width: 240 },
{ title: '操作', key: 'action', width: 240, fixed: 'right' }
],
expiredUncheckedOut: [
{ title: '工单号', dataIndex: 'name', key: 'name', fixed: true, width: 280 },
{ title: '服务对象', dataIndex: 'code', key: 'code', width: 240 },
{ title: '签入时间', dataIndex: 'checkinTime', key: 'checkinTime', width: 240 },
{ title: '逾期天数', dataIndex: 'overdueDays', key: 'overdueDays', width: 180 },
{ title: '服务人员', dataIndex: 'staff', key: 'staff', width: 240 },
{ title: '操作', key: 'action', width: 240, fixed: 'right' }
]
})
//
const currentColumns = computed(() => {
return columnConfigs.value[activeTabKey.value] || columnConfigs.value.all
})
const { listData, loading, showLoading, hideLoading, searchFormData, paginationState, resetPagination } =
usePagination()
const { resetForm } = useForm()
const editDialogRef = ref()
const viewDialogRef = ref()
const checkDialogRef = ref()
//
const advancedSearchVisible = ref([]) //
const tabKeyToStatusMap = {
all: undefined, // status
initialization: 'Initialize',
pendingCheckin: 'Pending_Check-in',
completed: 'Closed',
cancelled: 'Cancelled',
expiredUncheckedIn: 'Overdue_Unchecked-in',
expiredUncheckedOut: 'Overdue_Unchecked-out'
}
//
getMenuList()
/**
* 获取菜单列表
* @return {Promise<void>}
*/
async function getMenuList() {
try {
showLoading()
const { pageSize, current } = paginationState
// tab status
const status = tabKeyToStatusMap[activeTabKey.value]
const params = {
pageSize,
current,
...searchFormData.value
}
// status undefined
if (status !== undefined) {
params.status = status
}
console.log("=====search params", params)
const { success, data, total } = await apis.workOrder.getWorkOrderList(params)
if (config('http.code.success') === success) {
listData.value = data
paginationState.total = total
}
} catch (error) {
console.error('获取工单列表失败:', error)
message.error('获取数据失败')
} finally {
hideLoading()
}
}
/**
* 标签页切换事件
*/
function handleTabChange(key) {
//
// Initialize
// Pending_Check-in
// Pending_Check-out
// Closed
// Cancelled
// Overdue_Unchecked-in
// Overdue_Unchecked-out
console.log('active tab key:', key)
activeTabKey.value = key
resetPagination()
getMenuList() //
}
/**
* 搜索
*/
function handleSearch() {
resetPagination()
getMenuList()
}
/**
* 重置
*/
function handleResetSearch() {
searchFormData.value = {}
resetPagination()
getMenuList()
}
/**
* 显示作废确认模态框
*/
function showCancelModal(record) {
cancelRecord.value = record
cancelReason.value = ''
cancelModalVisible.value = true
}
/**
* 确认作废操作
*/
async function confirmCancel() {
if (!cancelRecord.value) return
try {
const { success } = await apis.menu.cancelMenu({
id: cancelRecord.value.id,
reason: cancelReason.value
}).catch(() => {
throw new Error()
})
if (config('http.code.success') === success) {
message.success('工单已作废')
cancelModalVisible.value = false
await getMenuList()
}
} catch (error) {
message.error('作废失败')
}
}
/**
* 查看详情
* @param record
*/
function handleDetail(record) {
console.log('查看详情:', record)
//
}
/**
* 续期操作针对过期工单
* @param record
*/
function handleRenew(record) {
Modal.confirm({
title: t('pages.system.renewConfirm'),
content: t('pages.system.renewTip'),
okText: t('button.confirm'),
cancelText: t('button.cancel'),
onOk: async () => {
try {
const { success } = await apis.menu.renewMenu(record.id)
if (config('http.code.success') === success) {
message.success(t('component.message.success.renew'))
await getMenuList()
}
} catch (error) {
message.error(t('component.message.error.renew'))
}
}
})
}
/**
* 编辑完成
*/
async function onOk() {
await getMenuList()
}
//
function handleChange(value) {
console.log('Selected value:', value)
}
//
function onAreaChange(value) {
console.log('Area changed:', value)
}
//
function toggleAdvancedSearch() {
advancedSearchVisible.value = advancedSearchVisible.value.length ? [] : ['1']
}
</script>
<style lang="less" scoped>
.tab-search-container {
background: #fff; //
padding: 16px; //
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); //
margin-bottom: 16px; //
}
// style
.ant-btn-link:hover {
color: #0050b3;
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,338 @@
<template>
<x-search-bar class="mb-4">
<template #default="{ gutter, colSpan }">
<a-form :label-col="{ style: { width: '130px' } }" :model="searchFormData" layout="inline">
<!-- 基础查询字段 -->
<a-row :gutter="gutter">
<a-col v-bind="colSpan">
<a-form-item label="所在区域" name="area">
<AreaCascader v-model:value="searchFormData.currentNode" @change="onAreaChange" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="服务满意度" name="node">
<a-select v-model:value="searchFormData.node" placeholder="请选择满意度">
<a-select-option v-for="item in dicsStore.dictOptions.Satisfaction_Type"
:key="item.dval" :value="item.dval">
{{ item.introduction }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="服务人员满意度" name="node">
<a-select v-model:value="searchFormData.node" placeholder="请选择服务人员满意度">
<a-select-option v-for="item in dicsStore.dictOptions.Satisfaction_Type"
:key="item.dval" :value="item.dval">
{{ item.introduction }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<!-- 高级查询折叠面板 -->
<a-collapse v-model:activeKey="advancedSearchVisible" ghost>
<a-collapse-panel key="1" :showArrow="false" style="padding: 0; border: none;">
<template #header></template>
<a-row :gutter="gutter" style="margin-top: 16px;">
<a-form-item label="回访时间" name="plannedDate" style="white-space: nowrap;">
<a-range-picker v-model:value="searchFormData.plannedDate" style="width: 220px;" />
</a-form-item>
<a-col v-bind="colSpan">
<a-form-item label="回访类型" name="node">
<a-select v-model:value="searchFormData.node" placeholder="请选择所在节点">
<a-select-option v-for="item in dicsStore.dictOptions.Visit_Type"
:key="item.dval" :value="item.dval">
{{ item.introduction }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="回访人" name="serviceProject">
<a-input v-model:value="searchFormData.serviceProject" placeholder="请输入服务项目" />
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
<!-- 操作按钮 -->
<a-row :gutter="gutter" style="margin-top: 16px;">
<a-col :span="24" style="text-align: right;">
<a-space>
<a-button @click="handleResetSearch">{{ $t('button.reset') }}</a-button>
<a-button ghost type="primary" @click="handleSearch">
{{ $t('button.search') }}
</a-button>
<a-button type="link" @click="toggleAdvancedSearch"
style="color: #1890ff; border: none; padding: 0; font-size: 14px;">
{{ advancedSearchVisible.length ? '收起' : '高级查询' }}
<template #icon>
<down-outlined v-if="advancedSearchVisible.length" />
<up-outlined v-else />
</template>
</a-button>
</a-space>
</a-col>
</a-row>
</a-form>
</template>
</x-search-bar>
<a-row :gutter="8" :wrap="false">
<a-col flex="auto">
<a-card title="服务人员列表">
<div style="margin-bottom: 20px;">
<a-space>
<!-- <a-button type="primary" @click="$refs.editDialogRef.handleCreate(record)">新建</a-button> -->
<a-button type="primary">导出</a-button>
<a-button type="dashed">导出记录</a-button>
</a-space>
</div>
<a-table :columns="columns" :data-source="listData" bordered="true" :loading="loading"
:pagination="paginationState" :scroll="{ x: 'max-content' }" @change="onTableChange">
<template #bodyCell="{ index, column, record }">
<template v-if="column.key === 'serialNumber'">
<span>{{ index + 1 }}</span>
</template>
</template>
</a-table>
</a-card>
</a-col>
</a-row>
<!-- <edit-dialog ref="editDialogRef" @ok="onOk"></edit-dialog>
<detail ref="detailRef"></detail> -->
</template>
<script setup>
import { DownOutlined, UpOutlined } from '@ant-design/icons-vue'
import AreaCascader from '@/components/AreaCascader/index.vue'
import { message, Modal } from 'ant-design-vue'
import { ref } from 'vue'
import apis from '@/apis'
import { config } from '@/config'
import { usePagination } from '@/hooks'
import { useI18n } from 'vue-i18n'
import { useDicsStore } from '@/store'
import dayjs from 'dayjs'
import NodeTree from '@/components/NodeTree/index.vue'
defineOptions({
name: 'allocation',
})
const dicsStore = useDicsStore()
const columns = [
{
title: '序号',
dataIndex: 'serialNumber',
key: 'serialNumber',
align: 'center',
width: 80,
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
align: 'center',
width: 100,
},
{
title: '性别',
dataIndex: 'gender',
key: 'gender',
align: 'center',
width: 80,
},
{
title: '电话',
dataIndex: 'phone',
key: 'phone',
align: 'center',
width: 80,
},
{
title: '身份证号',
dataIndex: 'idCard',
key: 'idCard',
align: 'center',
width: 180,
},
{
title: '护理人员类型',
dataIndex: 'serviceType',
key: 'serviceType',
align: 'center',
width: 120,
},
{
title: '用工形式',
dataIndex: 'workType',
key: 'workType',
align: 'center',
width: 120,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 120,
},
// --- ---
{
title: '护理人员类型',
dataIndex: 'serviceType',
key: 'serviceType',
align: 'center',
width: 130,
},
{
title: '所属服务组织',
dataIndex: 'stationName',
key: 'stationName',
align: 'center',
width: 120,
},
// {
// title: '',
// dataIndex: 'areaLabels',
// key: 'areaLabels',
// align: 'center',
// width: 120,
// },
];
const { t } = useI18n() // t
const { listData, loading, showLoading, hideLoading, paginationState, resetPagination, searchFormData } = usePagination()
const editDialogRef = ref()
const detailRef = ref()
//
const advancedSearchVisible = ref([]) //
getPageList()
async function getPageList() {
console.log("===1")
try {
const { pageSize, current } = paginationState
const { success, data, total } = await apis.workOrder
.getBackRecordList({
pageSize,
current: current,
...searchFormData.value,
})
listData.value = data
paginationState.total = total
console.log("===",data)
.catch(() => {
throw new Error()
})
if (config('http.code.success') === success) {
listData.value = data
paginationState.total = total
}
} catch (error) {
}
}
/**核销 */
const checkHandler = (record) => {
Modal.confirm({
title: '即将核销是否继续',
content: t('button.confirm'),
okText: t('button.confirm'),
onOk: async () => {
const params = {
...record,
status: 'success'
}
const { success } = await apis.productOrder.updateItem(params.id, params).catch(() => {
// throw new Error()
})
if (config('http.code.success') === success) {
// resolve()
message.success('核销成功')
await getPageList()
}
},
})
}
/**
* 删除
*/
function handleDelete({ id }) {
Modal.confirm({
title: t('pages.system.user.delTip'),
content: t('button.confirm'),
okText: t('button.confirm'),
onOk: () => {
return new Promise((resolve, reject) => {
; (async () => {
try {
const { success } = await apis.productOrder.delItem(id).catch(() => {
throw new Error()
})
if (config('http.code.success') === success) {
resolve()
message.success(t('component.message.success.delete'))
await getPageList()
}
} catch (error) {
reject()
}
})()
})
},
})
}
/**
* 分页
*/
function onTableChange({ current, pageSize }) {
paginationState.current = current
paginationState.pageSize = pageSize
getPageList()
}
/**
* 搜索
*/
function handleSearch() {
resetPagination()
getPageList()
}
/**
* 重置
*/
function handleResetSearch() {
searchFormData.value = {}
resetPagination()
getPageList()
}
/**
* 编辑完成
*/
async function onOk() {
await getPageList()
}
//
function toggleAdvancedSearch() {
advancedSearchVisible.value = advancedSearchVisible.value.length ? [] : ['1']
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,404 @@
<template>
<a-modal
:width="900"
:open="modal.open"
title="查看工单"
:confirm-loading="modal.confirmLoading"
:after-close="onAfterClose"
cancel-text="关闭"
ok-text="确认"
:ok-button-props="{ disabled: true }"
@ok="handleOk"
@cancel="handleCancel"
>
<a-tabs default-active-key="1" :tab-bar-style="{ marginBottom: '16px' }">
<!-- 工单信息页签 -->
<a-tab-pane key="1" tab="工单信息">
<a-form layout="vertical" ref="formRef" :model="formData">
<!-- 工单信息 -->
<a-card class="mb-4" title="工单信息">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="工单号" name="workOrderNo">
<a-input v-model:value="formData.workOrderNo" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="工单状态" name="status">
<a-input v-model:value="formData.status" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="工单类型" name="type">
<a-input v-model:value="formData.type" disabled />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 用户信息 -->
<a-card class="mb-4" title="用户信息">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="姓名" name="userName">
<a-input v-model:value="formData.userName" disabled />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="性别" name="gender">
<a-select v-model:value="formData.gender" disabled>
<a-select-option value="男"></a-select-option>
<a-select-option value="女"></a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="年龄" name="age">
<a-input-number v-model:value="formData.age" disabled style="width: 100%" :min="0" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="身份证号" name="idCard">
<a-input v-model:value="formData.idCard" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系电话" name="phone">
<a-input v-model:value="formData.phone" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="评估等级" name="assessmentLevel">
<a-input v-model:value="formData.assessmentLevel" disabled />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 服务信息 -->
<a-card class="mb-4" title="服务信息">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="护理员" name="nurseName">
<a-input v-model:value="formData.nurseName" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="护理等级" name="nurseLevel">
<a-input v-model:value="formData.nurseLevel" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="计划开始日期" name="planStartDate">
<a-date-picker v-model:value="formData.planStartDate" format="YYYY-MM-DD" disabled style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="计划结束日期" name="planEndDate">
<a-date-picker v-model:value="formData.planEndDate" format="YYYY-MM-DD" disabled style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="计划服务开始时间" name="planStartTime">
<a-time-picker v-model:value="formData.planStartTime" format="HH:mm" disabled style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="服务地址" name="serviceAddress">
<a-input v-model:value="formData.serviceAddress" disabled />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="服务项目">
<a-button type="link" @click="showServiceItems" disabled>项目清单</a-button>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="服务内容" name="serviceContent">
<a-textarea v-model:value="formData.serviceContent" disabled rows="3" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="下单人" name="orderer">
<a-input v-model:value="formData.orderer" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="下单时间" name="orderTime">
<a-date-picker v-model:value="formData.orderTime" show-time format="YYYY-MM-DD HH:mm:ss" disabled style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 服务小结与反馈 -->
<a-card class="mb-4" title="服务小结与反馈">
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="服务小结" name="summary">
<a-textarea v-model:value="formData.summary" disabled rows="3" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="用户需求反馈" name="feedback">
<a-textarea v-model:value="formData.feedback" disabled rows="3" />
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 异常处理信息 -->
<a-card class="mb-4" title="异常工单处理信息">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="处理状态" name="exceptionStatus">
<a-input v-model:value="formData.exceptionStatus" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="处理原因" name="exceptionReason">
<a-input v-model:value="formData.exceptionReason" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="处理人" name="exceptionHandler">
<a-input v-model:value="formData.exceptionHandler" disabled />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="处理时间" name="exceptionTime">
<a-date-picker v-model:value="formData.exceptionTime" show-time format="YYYY-MM-DD HH:mm:ss" disabled style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-card>
</a-form>
</a-tab-pane>
<!-- 服务评价页签 -->
<a-tab-pane key="2" tab="服务评价">
<a-space direction="vertical" style="width: 100%">
<!-- 回访评价 -->
<a-card title="回访评价" size="small">
<a-descriptions :column="{ xs: 1, sm: 2, md: 2 }" bordered>
<a-descriptions-item label="服务满意度">
{{ formData.visitSatisfaction || '-' }}
</a-descriptions-item>
<a-descriptions-item label="服务人员满意度">
{{ formData.staffSatisfaction || '-' }}
</a-descriptions-item>
<a-descriptions-item label="服务反馈" :span="2">
{{ formData.visitFeedback || '-' }}
</a-descriptions-item>
<a-descriptions-item label="回访人">
{{ formData.visitor || '-' }}
</a-descriptions-item>
<a-descriptions-item label="回访时间">
{{
formData.visitTime
? dayjs(formData.visitTime).format('YYYY-MM-DD HH:mm:ss')
: '-'
}}
</a-descriptions-item>
</a-descriptions>
</a-card>
<!-- 服务评价 -->
<a-card title="服务评价" size="small" style="margin-top: 16px">
<a-descriptions :column="{ xs: 1, sm: 2, md: 2 }" bordered>
<a-descriptions-item label="服务满意度">
{{ formData.serviceSatisfaction || '-' }}
</a-descriptions-item>
<a-descriptions-item label="评价渠道">
{{ formData.evaluationChannel || '-' }}
</a-descriptions-item>
<a-descriptions-item label="评价内容" :span="2">
{{ formData.evaluationContent || '-' }}
</a-descriptions-item>
<a-descriptions-item label="评价人">
{{ formData.evaluator || '-' }}
</a-descriptions-item>
<a-descriptions-item label="评价时间">
{{
formData.evaluationTime
? dayjs(formData.evaluationTime).format('YYYY-MM-DD HH:mm:ss')
: '-'
}}
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-space>
</a-tab-pane>
</a-tabs>
</a-modal>
</template>
<script setup>
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { useModal, useForm } from '@/hooks'
import dayjs from 'dayjs'
const emit = defineEmits(['ok'])
const { modal, showModal, hideModal } = useModal()
const { formData, formRef, resetForm } = useForm()
//
const initFormData = () => ({
workOrderNo: '',
status: '',
type: '',
userName: '',
gender: '',
age: null,
idCard: '',
phone: '',
assessmentLevel: '',
nurseName: '',
nurseLevel: '',
planStartDate: null,
planEndDate: null,
planStartTime: null,
serviceAddress: '',
serviceContent: '',
orderer: '',
orderTime: null,
summary: '',
feedback: '',
exceptionStatus: '',
exceptionReason: '',
exceptionHandler: '',
exceptionTime: null,
//
visitSatisfaction: '',
staffSatisfaction: '',
visitFeedback: '',
visitor: '',
visitTime: null,
serviceSatisfaction: '',
evaluationContent: '',
evaluator: '',
evaluationTime: null,
evaluationChannel: '',
})
//
formData.value = initFormData()
// ================== Methods ==================
/**
* 查看工单唯一打开弹框的方法
* @param {Object} record - 工单数据
*/
function handleView(record = {}) {
showModal({ mode: 'view', title: '查看工单' })
loadRecord(record)
}
/**
* 加载工单数据到表单
* @param {Object} record - 工单数据
*/
function loadRecord(record) {
formData.value = {
...initFormData(),
workOrderNo: record.workOrderNo || '',
status: record.status || '',
type: record.type || '',
userName: record.userName || '',
gender: record.gender || '',
age: record.age || null,
idCard: record.idCard || '',
phone: record.phone || '',
assessmentLevel: record.assessmentLevel || '',
nurseName: record.nurseName || '',
nurseLevel: record.nurseLevel || '',
planStartDate: record.planStartDate ? dayjs(record.planStartDate) : null,
planEndDate: record.planEndDate ? dayjs(record.planEndDate) : null,
planStartTime: record.planStartTime ? dayjs(`1970-01-01 ${record.planStartTime}`) : null,
serviceAddress: record.serviceAddress || '',
serviceContent: record.serviceContent || '',
orderer: record.orderer || '',
orderTime: record.orderTime ? dayjs(record.orderTime) : null,
summary: record.summary || '',
feedback: record.feedback || '',
exceptionStatus: record.exceptionStatus || '',
exceptionReason: record.exceptionReason || '',
exceptionHandler: record.exceptionHandler || '',
exceptionTime: record.exceptionTime ? dayjs(record.exceptionTime) : null,
//
visitSatisfaction: record.visitSatisfaction || '',
staffSatisfaction: record.staffSatisfaction || '',
visitFeedback: record.visitFeedback || '',
visitor: record.visitor || '',
visitTime: record.visitTime ? dayjs(record.visitTime) : null,
serviceSatisfaction: record.serviceSatisfaction || '',
evaluationContent: record.evaluationContent || '',
evaluator: record.evaluator || '',
evaluationTime: record.evaluationTime ? dayjs(record.evaluationTime) : null,
evaluationChannel: record.evaluationChannel || '',
}
}
/**
* 确认按钮禁用状态仅保留逻辑
*/
function handleOk() {
hideModal()
emit('ok')
}
/**
* 取消/关闭按钮
*/
function handleCancel() {
hideModal()
}
/**
* 弹框关闭后重置表单
*/
function onAfterClose() {
resetForm()
}
/**
* 服务项目清单查看模式下禁用保留提示
*/
function showServiceItems() {
message.info('服务项目清单')
}
//
defineExpose({
handleView,
})
</script>
<style scoped>
.mb-4 {
margin-bottom: 16px;
}
.ant-descriptions-title {
font-weight: bold;
margin-bottom: 12px;
}
.ant-descriptions-view table {
width: 100%;
table-layout: fixed;
}
.ant-descriptions-item-label {
display: inline-block;
min-width: 100px;
font-weight: normal;
}
.ant-descriptions-item-content {
display: inline-block;
padding-left: 8px;
}
</style>

View File

@ -0,0 +1,98 @@
<template>
<a-modal v-model:visible="visible" title="工单回访" @cancel="handleCancel" @ok="handleOk"
:confirm-loading="confirmLoading">
<a-form :model="formData" ref="formRef" :rules="formRules">
<!-- 满意度 tooltip 提示 -->
<!-- 满意度 -->
<a-form-item label="满意度" name="satisfaction" :required="true">
<a-tooltip title="对服务整体满意程度的评价" placement="right" trigger="hover">
<span>
<a-rate v-model:value="formData.satisfaction" allow-half />
</span>
</a-tooltip>
</a-form-item>
<!-- 服务人员评分 -->
<a-form-item label="服务人员" name="staffSatisfaction" :required="true">
<a-tooltip title="对服务人员态度、专业能力等的评价" placement="right" trigger="hover">
<span>
<a-rate v-model:value="formData.staffSatisfaction" allow-half />
</span>
</a-tooltip>
</a-form-item>
<!-- 服务反馈非必填 -->
<a-form-item label="服务反馈">
<a-textarea v-model:value="formData.feedback" placeholder="客观地评价及记录有助于用户决策"
:auto-size="{ minRows: 3, maxRows: 6 }" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
//
const visible = ref(false)
//
const confirmLoading = ref(false)
//
const formRef = ref()
//
const formData = reactive({
satisfaction: null, //
staffSatisfaction: null, //
feedback: '' //
})
//
const formRules = {
satisfaction: [
{ required: true, message: '请选择满意度', trigger: 'change' }
],
staffSatisfaction: [
{ required: true, message: '请选择服务人员满意度', trigger: 'change' }
]
}
//
const emit = defineEmits(['ok', 'cancel'])
//
const handleOk = async () => {
try {
//
await formRef.value.validate()
confirmLoading.value = true
//
setTimeout(() => {
confirmLoading.value = false
visible.value = false
emit('ok', formData) //
message.success('回访提交成功')
}, 500)
} catch (error) {
console.error('表单验证失败:', error)
}
}
// /
const handleCancel = () => {
visible.value = false
emit('cancel')
}
//
defineExpose({
handleOpen: (record) => {
formRef.value?.resetFields() //
formData.satisfaction = null
formData.staffSatisfaction = null
formData.feedback = ''
visible.value = true
}
})
</script>

View File

@ -0,0 +1,333 @@
<template>
<x-search-bar class="mb-4">
<template #default="{ gutter, colSpan }">
<a-form :label-col="{ style: { width: '130px' } }" :model="searchFormData">
<!-- 基础查询字段 -->
<a-row :gutter="gutter">
<a-col v-bind="colSpan">
<a-form-item label="姓名" name="name">
<a-input v-model:value="searchFormData.name" placeholder="请输入姓名" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="身份证号" name="idCard">
<a-input v-model:value="searchFormData.idCard" placeholder="请输入身份证号" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="异常类型" name="exceptionType">
<a-select v-model:value="searchFormData.exceptionType" placeholder="请选择异常类型">
<a-select-option value="type1">类型1</a-select-option>
<a-select-option value="type2">类型2</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<!-- 高级查询折叠面板 -->
<a-collapse v-model:activeKey="advancedSearchVisible" ghost>
<a-collapse-panel key="1" :showArrow="false" style="padding: 0; border: none;">
<template #header></template>
<a-row :gutter="gutter" style="margin-top: 16px;">
<a-col v-bind="colSpan">
<a-form-item label="实际服务日期" name="plannedDate">
<a-range-picker v-model:value="searchFormData.plannedDate" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="实际服务时长(小时)" name="serviceDuration" style="white-space: nowrap;">
<a-input-number v-model:value="searchFormData.serviceDuration" :min="0"
placeholder="请输入时长" style="width: 100%" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="工单号" name="serviceOrderNo">
<a-input v-model:value="searchFormData.serviceOrderNo" placeholder="请输入工单号" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="所属服务组织" name="serviceOrg">
<a-select v-model:value="searchFormData.serviceOrg" placeholder="请选择服务组织">
<a-select-option value="org1">组织1</a-select-option>
<a-select-option value="org2">组织2</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="所在区域" name="currentNode">
<AreaCascader v-model:value="searchFormData.currentNode" @change="onAreaChange" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="服务满意度" name="satisfaction">
<a-select v-model:value="searchFormData.satisfaction" placeholder="请选择满意度">
<a-select-option value="5">非常满意</a-select-option>
<a-select-option value="4">满意</a-select-option>
<a-select-option value="3">一般</a-select-option>
<a-select-option value="2">不满意</a-select-option>
<a-select-option value="1">非常不满意</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="服务人员满意度" name="staffSatisfaction">
<a-select v-model:value="searchFormData.staffSatisfaction" placeholder="请选择满意度">
<a-select-option value="5">非常满意</a-select-option>
<a-select-option value="4">满意</a-select-option>
<a-select-option value="3">一般</a-select-option>
<a-select-option value="2">不满意</a-select-option>
<a-select-option value="1">非常不满意</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="是否回访" name="isVisited">
<a-select v-model:value="searchFormData.isVisited" placeholder="请选择">
<a-select-option :value="true"></a-select-option>
<a-select-option :value="false"></a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-collapse-panel>
</a-collapse>
<!-- 操作按钮 -->
<a-row :gutter="gutter" style="margin-top: 16px;">
<a-col :span="24" style="text-align: right;">
<a-space>
<a-button @click="handleResetSearch">{{ $t('button.reset') }}</a-button>
<a-button ghost type="primary" @click="handleSearch">
{{ $t('button.search') }}
</a-button>
<a-button type="link" @click="toggleAdvancedSearch"
style="color: #1890ff; border: none; padding: 0; font-size: 14px;">
{{ advancedSearchVisible.length ? '收起' : '高级查询' }}
<template #icon>
<down-outlined v-if="advancedSearchVisible.length" />
<up-outlined v-else />
</template>
</a-button>
</a-space>
</a-col>
</a-row>
</a-form>
</template>
</x-search-bar>
<a-row :gutter="8" :wrap="false" style="margin-top: 20px;">
<a-col flex="auto">
<a-card title="回访列表">
<div style="margin-bottom: 20px;">
<a-space>
<a-button type="primary">导入</a-button>
<a-button type="primary">导入记录</a-button>
<a-button type="primary">导出</a-button>
<a-button type="primary">导出记录</a-button>
</a-space>
</div>
<a-table rowKey="id" :loading="loading" :pagination="true" :columns="columns" :data-source="listData"
:scroll="{ x: 'max-content' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'serialNumber'">
<span>{{ index + 1 }}</span>
</template>
<template v-else-if="column.dataIndex === 'otherPhone1'">
<span>{{ record.otherPhone1 || record.otherPhone2 || '—' }}</span>
</template>
<template v-else-if="column.dataIndex === 'plannedServiceStartDate'">
<span>{{ formatDate(record.plannedServiceStartDate) }}</span>
</template>
<template v-else-if="column.key === 'action'">
<x-action-button @click="$refs.editDialogRef.handleView(record)">
<span>详情</span>
</x-action-button>
<x-action-button @click="handleVisit(record)">
回访
</x-action-button>
</template>
</template>
</a-table>
</a-card>
</a-col>
</a-row>
<edit-dialog @ok="onOk" ref="editDialogRef" />
<!-- 新增回访弹框 -->
<visit-dialog @ok="onVisitOk" ref="visitDialogRef" />
</template>
<script setup>
import { DownOutlined, UpOutlined } from '@ant-design/icons-vue'
import AreaCascader from '@/components/AreaCascader/index.vue'
import { message, Modal } from 'ant-design-vue'
import { ref } from 'vue'
import apis from '@/apis'
import { config } from '@/config'
import { usePagination } from '@/hooks'
import { useI18n } from 'vue-i18n'
import { useDicsStore } from '@/store'
import EditDialog from './components/EditDialog.vue'
// 访
import VisitDialog from './components/VisitDialog.vue'
import dayjs from 'dayjs'
defineOptions({
name: 'allocation',
})
const dicsStore = useDicsStore()
const { t } = useI18n()
//
const columns = [
{ title: '订单号', dataIndex: 'orderNum', key: 'orderNum', align: 'center', width: 80 },
{ title: '服务对象姓名', dataIndex: 'customerName', key: 'customerName', align: 'center', width: 100 },
{ title: '电话', dataIndex: 'contact1', key: 'contact1', align: 'center', width: 120 },
{ title: '电话1', dataIndex: 'otherPhone1', key: 'otherPhone1', align: 'center', width: 120 },
{ title: '电话2', dataIndex: 'otherPhone2', key: 'otherPhone2', align: 'center', width: 120 },
{ title: '身份证号', dataIndex: 'customerIdCard', key: 'customerIdCard', align: 'center', width: 240 },
{ title: '地址', dataIndex: 'detailAddress', key: 'detailAddress', align: 'center', width: 240 },
{ title: '服务人员姓名', dataIndex: 'serviceName', key: 'serviceName', align: 'center', width: 120 },
{ title: '服务项目名称', dataIndex: 'workType', key: 'workType', align: 'center', width: 120 },
{ title: '计划服务开始时间', dataIndex: 'plannedServiceStartDate', key: 'plannedServiceStartDate', align: 'center', width: 100 },
{ title: '状态', dataIndex: 'status', key: 'status', align: 'center', width: 150 },
{ title: '操作', key: 'action', width: 140, fixed: 'right' }
]
//
const {
listData,
loading,
paginationState,
resetPagination,
searchFormData,
} = usePagination()
// searchFormData
searchFormData.value = {
name: '',
idCard: '',
exceptionType: undefined,
serviceOrderNo: '',
serviceOrg: undefined,
satisfaction: undefined,
staffSatisfaction: undefined,
isVisited: undefined,
plannedDate: [],
serviceDuration: undefined,
currentNode: [],
}
const editDialogRef = ref()
const detailRef = ref()
const advancedSearchVisible = ref([]) //
// 访
const visitDialogRef = ref()
//
const totalWidth = columns.reduce((sum, col) => sum + (col.width || 100), 0);
// mock
async function getPageList() {
loading.value = true
try {
const { pageSize, current } = paginationState
const params = {
pageSize,
current,
...searchFormData.value,
}
if (Array.isArray(params.plannedDate) && params.plannedDate.length === 0) {
delete params.plannedDate
}
const { success, data, total } = await apis.workOrder.getBackWorkOrderList(params)
listData.value = data
paginationState.total = total
} catch (error) {
console.error('获取列表失败:', error)
message.error('加载数据失败')
} finally {
loading.value = false
}
}
//
function handleSearch() {
resetPagination()
getPageList()
}
//
function handleResetSearch() {
searchFormData.value = {
name: '',
idCard: '',
exceptionType: undefined,
serviceOrderNo: '',
serviceOrg: undefined,
satisfaction: undefined,
staffSatisfaction: undefined,
isVisited: undefined,
plannedDate: [],
serviceDuration: undefined,
currentNode: [],
}
resetPagination()
getPageList()
}
//
function onTableChange(pagination) {
paginationState.current = pagination.current
paginationState.pageSize = pagination.pageSize
getPageList()
}
//
function toggleAdvancedSearch() {
advancedSearchVisible.value = advancedSearchVisible.value.length ? [] : ['1']
}
//
function onAreaChange(value) {
console.log('区域变更:', value)
}
//
function handleDetail(record) {
console.log('查看详情:', record)
message.info(`查看【${record.name}】的详情`)
//
// detailRef.value?.open(record)
}
// 访访
function handleVisit(record) {
visitDialogRef.value.handleOpen(record) // 访
}
// 访
function onVisitOk(formData) {
console.log('回访表单数据:', formData)
message.success('回访提交成功')
getPageList() //
}
//
getPageList()
</script>
<style lang="less" scoped></style>