Files
sc-lktx-mp/mp-sc-admin/src/pages/Appointments.vue
huangjin fe3ad20fe2 'init'
2026-06-29 17:34:59 +08:00

346 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>