'init'
This commit is contained in:
345
mp-sc-admin/src/pages/Appointments.vue
Normal file
345
mp-sc-admin/src/pages/Appointments.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-xl font-bold text-gray-800">预约管理</h1>
|
||||
</div>
|
||||
|
||||
<el-card shadow="never" class="mb-3">
|
||||
<el-row :gutter="12" align="middle">
|
||||
<el-col :span="4">
|
||||
<el-select v-model="statusFilter" placeholder="审批状态" clearable class="w-full">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待审核" :value="0" />
|
||||
<el-option label="已通过" :value="1" />
|
||||
<el-option label="已拒绝" :value="2" />
|
||||
<el-option label="已取消" :value="3" />
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="2">
|
||||
<el-button type="primary" @click="page = 1; fetchData()">查询</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<el-table :data="appointments" stripe class="w-full" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column label="外部来访人员" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="flex flex-col text-sm leading-relaxed">
|
||||
<template v-if="parseVisitors(row.visitor_info)?.length">
|
||||
<span class="text-gray-800">
|
||||
{{ parseVisitors(row.visitor_info)[0].name }}
|
||||
<span v-if="parseVisitors(row.visitor_info).length > 1" class="text-gray-400 font-normal">
|
||||
(等{{ parseVisitors(row.visitor_info).length }}人)
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="parseVisitors(row.visitor_info)[0]?.company" class="text-gray-400 text-xs">{{ parseVisitors(row.visitor_info)[0]?.company }}</span>
|
||||
</template>
|
||||
<span v-else class="text-gray-800">-</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="公司接待人员" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span>{{ parseEmployee(row.employee_info)?.name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来访事由" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.visit_purpose || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来访时间" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<span class="text-sm">{{ formatTime(row.visit_start_time) }} ~ {{ formatTime(row.visit_end_time) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="['warning','success','danger','info'][row.status]">
|
||||
{{ ['待审核','已通过','已拒绝','已取消'][row.status] }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="openDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-if="total > 0" class="p-4 flex justify-center">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total,prev,pager,next"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="预约详情" width="600px" :close-on-click-modal="false">
|
||||
<template v-if="detailRow">
|
||||
<div class="space-y-5">
|
||||
<!-- 基本信息 -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">基本信息</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-6 gap-y-2 text-sm bg-gray-50 rounded-lg p-4">
|
||||
<div><span class="text-gray-400">预约ID:</span><span class="text-gray-700">{{ detailRow.id }}</span></div>
|
||||
<div><span class="text-gray-400">来访事由:</span><span class="text-gray-700">{{ detailRow.visit_purpose || '-' }}</span></div>
|
||||
<div class="col-span-2">
|
||||
<span class="text-gray-400">来访时间:</span>
|
||||
<span class="text-gray-700">{{ formatTime(detailRow.visit_start_time) }} ~ {{ formatTime(detailRow.visit_end_time) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-400">状态:</span>
|
||||
<el-tag :type="['warning','success','danger','info'][detailRow.status]" size="small">
|
||||
{{ ['待审核','已通过','已拒绝','已取消'][detailRow.status] }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div><span class="text-gray-400">创建时间:</span><span class="text-gray-700">{{ formatTime(detailRow.created_at) }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 访客信息 -->
|
||||
<div v-if="parsedVisitors?.length">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">外部来访人员</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-2">
|
||||
<div v-for="(v, i) in parsedVisitors" :key="i" class="flex items-center gap-3 text-sm">
|
||||
<span class="text-gray-800 font-medium">{{ v.name }}</span>
|
||||
<span v-if="v.phone" class="text-gray-500">{{ v.phone }}</span>
|
||||
<span v-if="v.company" class="text-gray-400">{{ v.company }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 被访人 -->
|
||||
<div v-if="parsedEmployee">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">公司接待人员</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-sm">
|
||||
<span class="text-gray-800 font-medium">{{ parsedEmployee.name }}</span>
|
||||
<span v-if="parsedEmployee.phone" class="text-gray-500 ml-2">{{ parsedEmployee.phone }}</span>
|
||||
<span v-if="parsedEmployee.department" class="text-gray-400 ml-2">{{ parsedEmployee.department }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建人 -->
|
||||
<div v-if="parsedCreator">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">创建人</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-sm">
|
||||
<span class="text-gray-800 font-medium">{{ parsedCreator.name }}</span>
|
||||
<span v-if="parsedCreator.phone" class="text-gray-500 ml-2">{{ parsedCreator.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 携带物品 -->
|
||||
<div v-if="parsedGoods?.length">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">携带物品</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-1.5">
|
||||
<div v-for="(g, i) in parsedGoods" :key="i" class="text-sm">
|
||||
<span class="text-gray-800">{{ g.name }}</span>
|
||||
<span v-if="g.quantity" class="text-gray-500 ml-1">x{{ g.quantity }}</span>
|
||||
<span v-if="g.remark" class="text-gray-400 ml-2">{{ g.remark }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 车辆信息 -->
|
||||
<div v-if="parsedVehicles?.length">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">车辆信息</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-1.5">
|
||||
<div v-for="(v, i) in parsedVehicles" :key="i" class="text-sm">
|
||||
<span class="text-gray-400">车辆{{ i + 1 }}:</span>
|
||||
<span class="text-gray-800">{{ [v.brand, v.plate, v.color].filter(Boolean).join(' ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 到访区域 -->
|
||||
<div v-if="parsedAreas?.length">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">到访区域</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-700">{{ parsedAreas.join('、') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注 -->
|
||||
<div v-if="detailRow.remark">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">备注</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600">{{ detailRow.remark }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 审核意见 -->
|
||||
<div v-if="detailRow.comment">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">审核意见</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600">{{ detailRow.comment }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批进度 -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<div class="w-1 h-4 bg-blue-500 rounded" />
|
||||
<span class="text-sm font-medium text-gray-700">审批进度</span>
|
||||
</div>
|
||||
<div v-if="workflowTasks.length === 0 && !workflowLoading" class="text-sm text-gray-400 bg-gray-50 rounded-lg p-4">暂无审批记录</div>
|
||||
<div v-else-if="workflowLoading" class="text-sm text-gray-400 bg-gray-50 rounded-lg p-4">加载中...</div>
|
||||
<div v-else class="space-y-0">
|
||||
<div
|
||||
v-for="(t, idx) in workflowTasks"
|
||||
:key="t.id"
|
||||
class="flex gap-3 pb-4"
|
||||
:class="{ 'pb-0': idx === workflowTasks.length - 1 }"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full border-2 shrink-0 mt-1"
|
||||
:class="{
|
||||
'bg-green-500 border-green-500': t.status === 1,
|
||||
'bg-red-500 border-red-500': t.status === 2,
|
||||
'bg-orange-400 border-orange-400': t.status === 0,
|
||||
'bg-gray-200 border-gray-300': t.status === 3 || !t.status,
|
||||
}"
|
||||
/>
|
||||
<div v-if="idx < workflowTasks.length - 1" class="w-0.5 flex-1 mt-1"
|
||||
:class="t.status === 1 ? 'bg-green-300' : 'bg-gray-200'" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-gray-700">{{ t.node_name }}</span>
|
||||
<el-tag v-if="t.status === 0" type="warning" size="small">待审批</el-tag>
|
||||
<el-tag v-else-if="t.status === 1" type="success" size="small">已通过</el-tag>
|
||||
<el-tag v-else-if="t.status === 2" type="danger" size="small">已拒绝</el-tag>
|
||||
<el-tag v-else size="small" type="info">已取消</el-tag>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-0.5">
|
||||
<template v-if="t.assignee">{{ t.assignee.real_name || t.assignee.nickname }}</template>
|
||||
<template v-if="t.handled_at"> · {{ formatTime(t.handled_at) }}</template>
|
||||
</div>
|
||||
<div v-if="t.comment" class="text-xs text-gray-500 mt-0.5 bg-white rounded px-2 py-1 border">
|
||||
{{ t.comment }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const appointments = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const statusFilter = ref('')
|
||||
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailRow = ref<any>(null)
|
||||
const workflowTasks = ref<any[]>([])
|
||||
const workflowLoading = ref(false)
|
||||
const parsedVisitors = computed(() => detailRow.value ? parseVisitors(detailRow.value.visitor_info) : null)
|
||||
const parsedEmployee = computed(() => detailRow.value ? parseEmployee(detailRow.value.employee_info) : null)
|
||||
const parsedCreator = computed(() => detailRow.value ? parseCreator(detailRow.value.creator_info) : null)
|
||||
const parsedAreas = computed(() => detailRow.value ? parseAreas(detailRow.value.areas_info) : null)
|
||||
const parsedGoods = computed(() => detailRow.value ? parseGoods(detailRow.value.goods_info) : null)
|
||||
const parsedVehicles = computed(() => detailRow.value ? parseVehicles(detailRow.value.vehicle_info) : null)
|
||||
|
||||
function parseVisitors(info) {
|
||||
if (!info) return null
|
||||
try {
|
||||
const parsed = JSON.parse(info)
|
||||
// 对象格式:{ name, phone, company, visitors: [...] }
|
||||
if (!Array.isArray(parsed) && parsed?.visitors) {
|
||||
return parsed.visitors
|
||||
}
|
||||
// 已经是数组
|
||||
if (Array.isArray(parsed)) return parsed
|
||||
// 单个对象包装为数组
|
||||
return [parsed]
|
||||
} catch { return null }
|
||||
}
|
||||
function parseEmployee(info) { if (!info) return null; try { return JSON.parse(info) } catch { return null } }
|
||||
function parseCreator(info) { if (!info) return null; try { const p = JSON.parse(info); return p.name ? p : null } catch { return null } }
|
||||
function parseAreas(info) { if (!info) return null; try { return JSON.parse(info).map(a => a.area || a) } catch { return null } }
|
||||
function parseGoods(info) { if (!info) return null; try { return JSON.parse(info) } catch { return null } }
|
||||
function parseVehicles(info) { if (!info) return null; try { return JSON.parse(info) } catch { return null } }
|
||||
function formatTime(t) {
|
||||
if (!t) return '-'
|
||||
const d = new Date(t)
|
||||
const pad = n => n.toString().padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, page_size: pageSize.value }
|
||||
if (statusFilter.value !== '') params.status = statusFilter.value
|
||||
const res = await request.Get('/v1/admin/appointments', { params })
|
||||
appointments.value = res?.data?.list || []
|
||||
total.value = res?.data?.total || 0
|
||||
} catch { ElMessage.error('加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
// 加载工作流审批记录
|
||||
async function openDetail(row) {
|
||||
detailRow.value = row
|
||||
detailVisible.value = true
|
||||
workflowTasks.value = []
|
||||
workflowLoading.value = true
|
||||
try {
|
||||
// 查询工作流实例
|
||||
const instances = await request.Get('/v1/workflow/instances', {
|
||||
params: { business_type: 'appointment', business_id: row.id },
|
||||
}) as any
|
||||
const list = instances?.data?.list || []
|
||||
if (list.length > 0) {
|
||||
const detail = await request.Get(`/v1/workflow/instances/${list[0].id}`) as any
|
||||
workflowTasks.value = detail?.data?.tasks || []
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
finally { workflowLoading.value = false }
|
||||
}
|
||||
|
||||
onMounted(fetchData)
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user