新增
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取企业微信客户列表
|
||||
export function qywxCustomerLists(params: any) {
|
||||
return request.get({ url: '/qywx.customer/lists', params })
|
||||
}
|
||||
|
||||
// 同步企业微信客户
|
||||
export function qywxCustomerSync() {
|
||||
return request.post({ url: '/qywx.customer/sync' })
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
export function qywxCustomerStats() {
|
||||
return request.get({ url: '/qywx.customer/stats' })
|
||||
}
|
||||
|
||||
// 获取同步设置
|
||||
export function qywxSyncSettingsGet() {
|
||||
return request.get({ url: '/qywx.customer/getSyncSettings' })
|
||||
}
|
||||
|
||||
// 保存同步设置
|
||||
export function qywxSyncSettingsSave(params: any) {
|
||||
return request.post({ url: '/qywx.customer/saveSyncSettings', params })
|
||||
}
|
||||
@@ -243,7 +243,7 @@ export function prescriptionOrderLists(params: any) {
|
||||
return request.get({ url: '/tcm.prescriptionOrder/lists', params })
|
||||
}
|
||||
|
||||
/** 诊单下可关联的已支付 zyt_order(已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选) */
|
||||
/** 诊单下可关联的支付单(待支付/已支付的 zyt_order,已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选) */
|
||||
export function prescriptionOrderPaidPayOrders(params: {
|
||||
diagnosis_id: number
|
||||
prescription_order_id?: number
|
||||
@@ -313,6 +313,14 @@ export function prescriptionOrderAddPayOrder(params: {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/addPayOrder', params })
|
||||
}
|
||||
|
||||
/** 为「已发货」订单关联已有支付单并重置支付审核为待审核 */
|
||||
export function prescriptionOrderLinkPayOrder(params: {
|
||||
id: number
|
||||
pay_order_id: number
|
||||
}) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/linkPayOrder', params })
|
||||
}
|
||||
|
||||
/** 将「已发货」且支付审核通过的订单标记为「已完成」 */
|
||||
export function prescriptionOrderComplete(params: { id: number }) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/complete', params })
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="chat-window-header"
|
||||
@mousedown="onHeaderMouseDown"
|
||||
>
|
||||
<span class="chat-window-title" :title="patientName">与 {{ patientName }} 聊天</span>
|
||||
<span class="chat-window-title" :title="patientName">与 {{ patientName }} 通讯</span>
|
||||
<div class="chat-header-actions" @mousedown.stop>
|
||||
<el-button type="danger" link class="chat-close-btn" @click="handleClose">
|
||||
<el-icon><Close /></el-icon>
|
||||
@@ -74,7 +74,8 @@
|
||||
</div>
|
||||
<!-- 挂载 TUICallKit 组件(可拖动,仅在通话时显示) -->
|
||||
<div
|
||||
v-if="isCallReady && showCallKitWindow"
|
||||
v-if="isCallReady"
|
||||
v-show="showCallKitWindow"
|
||||
ref="callKitWrapperRef"
|
||||
class="chat-dialog-call-kit-wrapper"
|
||||
:style="callKitStyle"
|
||||
|
||||
@@ -16,25 +16,49 @@ export function formatBizTime(t: unknown): string | undefined {
|
||||
}
|
||||
|
||||
export function parseRtcInner(inner: Record<string, unknown>): FriendlyParse {
|
||||
const callType = inner.call_type
|
||||
const callType = inner.call_type ?? inner.callType ?? 2
|
||||
const callTypeLabel =
|
||||
callType === 2 ? '视频通话' : callType === 1 ? '语音通话' : '通话'
|
||||
const cmd = String(inner.cmd || '')
|
||||
callType === 2 ? '视频' : callType === 1 ? '语音通话' : '通话'
|
||||
|
||||
let cmd = String(inner.cmd || inner.action || '').toLowerCase()
|
||||
if (!cmd) {
|
||||
const at = Number(inner.actionType ?? inner.action_type ?? inner.call_action)
|
||||
if (at === 1) cmd = 'invite'
|
||||
else if (at === 2) cmd = 'cancel'
|
||||
else if (at === 3) cmd = 'accept'
|
||||
else if (at === 4) cmd = 'reject'
|
||||
else if (at === 5) cmd = 'timeout'
|
||||
else if (at === 6) cmd = 'hangup'
|
||||
else if (at === 7) cmd = 'linebusy'
|
||||
}
|
||||
|
||||
const cmdMap: Record<string, string> = {
|
||||
hangup: '已结束',
|
||||
invite: '发起通话',
|
||||
linebusy: '对方忙线',
|
||||
cancel: '已取消',
|
||||
reject: '已拒绝',
|
||||
cancel: '已取消呼叫',
|
||||
reject: '已拒绝接听',
|
||||
accept: '已接听',
|
||||
timeout: '无人接听'
|
||||
}
|
||||
const action = cmdMap[cmd] || (cmd ? `状态:${cmd}` : '')
|
||||
const main = action ? `${callTypeLabel} · ${action}` : callTypeLabel
|
||||
|
||||
// 如果有通话时长,追加显示
|
||||
const duration = Number(inner.duration ?? inner.call_duration)
|
||||
const durationText = (duration > 0 && (action === '已结束' || action === '已挂断')) ? ` (${Math.floor(duration / 60)}分${duration % 60}秒)` : ''
|
||||
|
||||
const main = action ? `${callTypeLabel} · ${action}${durationText}` : callTypeLabel
|
||||
const parts: string[] = []
|
||||
if (inner.inviter) parts.push(`发起方 ${inner.inviter}`)
|
||||
if (inner.invitee) parts.push(`对方 ${inner.invitee}`)
|
||||
if (inner.groupID) parts.push(`群组 ${inner.groupID}`)
|
||||
//if (inner.inviter) parts.push(`发起方: ${inner.inviter}`)
|
||||
if (inner.invitee) parts.push(`对方: ${inner.invitee}`)
|
||||
if (inner.groupID) parts.push(`群组: ${inner.groupID}`)
|
||||
|
||||
// 强制输出兜底调试信息(仅未识别到 action 时触发)
|
||||
if (!action) {
|
||||
const dbg = JSON.stringify(inner).replace(/"/g, '')
|
||||
parts.push(`(原始参数: ${dbg})`)
|
||||
}
|
||||
|
||||
return {
|
||||
main,
|
||||
sub: parts.length ? parts.join(',') : undefined,
|
||||
@@ -83,19 +107,18 @@ export function parseImBusinessPayload(raw: string): FriendlyParse | null {
|
||||
return { main: '问诊已完成', sub: formatBizTime(o.time), tag: '诊室' }
|
||||
}
|
||||
|
||||
if (o.businessID === 1 || o.businessID === '1') {
|
||||
if (o.businessID === 1 || o.businessID === '1' || o.businessID === 'av_call') {
|
||||
let inner: unknown = o.data
|
||||
if (typeof inner === 'string') {
|
||||
try {
|
||||
inner = JSON.parse(inner)
|
||||
} catch {
|
||||
return { main: '通话信令', sub: '内层数据解析失败', tag: '通话' }
|
||||
// fallback
|
||||
}
|
||||
}
|
||||
if (inner && typeof inner === 'object') {
|
||||
return parseRtcInner(inner as Record<string, unknown>)
|
||||
}
|
||||
return null
|
||||
// 深远兼容:合并外层字段和 data 层字段,避免各端版本取参有遗漏
|
||||
const merged = { ...o, ...(typeof inner === 'object' && inner ? inner : {}) }
|
||||
return parseRtcInner(merged as Record<string, unknown>)
|
||||
}
|
||||
|
||||
if (o.businessID === 'rtc_call' || o.cmd) {
|
||||
|
||||
@@ -160,6 +160,11 @@
|
||||
<div class="text-xs text-gray-500 mt-0.5">{{ row.prescription_date || '—' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="医助" min-width="90">
|
||||
<template #default="{ row }">
|
||||
<div class="text-sm">{{ row.assistant_name || '—' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatListCreateTime(row.create_time) }}
|
||||
@@ -189,7 +194,7 @@
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="userCanAudit() && row.audit_status === 0 && row.void_status !== 1"
|
||||
v-if="row.audit_status === 0 && row.void_status !== 1"
|
||||
type="warning"
|
||||
link
|
||||
v-perms="['cf.prescription/audit']"
|
||||
@@ -889,14 +894,14 @@
|
||||
<span>关联收款</span>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<el-form-item label="已支付单">
|
||||
<el-form-item label="关联支付单">
|
||||
<el-select
|
||||
v-model="createOrderForm.pay_order_ids"
|
||||
class="w-full"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="多选本诊单下已支付收款单"
|
||||
placeholder="多选本诊单下的支付单"
|
||||
:loading="createOrderPaidOrdersLoading"
|
||||
>
|
||||
<el-option
|
||||
@@ -919,7 +924,7 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div class="text-xs text-gray-500 mt-1.5 leading-relaxed">
|
||||
主管或本诊单医助可见该诊单全部已支付单,否则仅本人创建的收款单。已被其他有效业务订单占用的支付单不会出现在此列表。
|
||||
主管或本诊单医助可见该诊单全部支付单(待支付/已支付),否则仅本人创建的收款单。已被其他有效业务订单占用的支付单不会出现在此列表。
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+274
-137
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div class="statistics-page">
|
||||
<el-card shadow="never" class="!border-none">
|
||||
<!-- 筛选区域 -->
|
||||
<!-- 筛选区域卡片 -->
|
||||
<el-card class="mb-4" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">筛选条件</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :inline="true" :model="queryParams" class="filter-form">
|
||||
<el-form-item label="医生">
|
||||
<el-select
|
||||
@@ -48,123 +53,97 @@
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 统计表格 -->
|
||||
<el-card shadow="never" class="!border-none mt-4">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="statisticsList"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '600' }"
|
||||
>
|
||||
<el-table-column prop="doctor_name" label="医生姓名" width="120" fixed />
|
||||
|
||||
<el-table-column label="部门统计" min-width="250" fixed>
|
||||
<template #default="{ row }">
|
||||
<div class="dept-stats">
|
||||
<el-tag
|
||||
v-for="(item, index) in parseDeptStats(row.dept_stats)"
|
||||
:key="index"
|
||||
type="primary"
|
||||
size="small"
|
||||
class="stat-tag"
|
||||
>
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
<span v-if="!row.dept_stats || row.dept_stats === '未分配'" class="text-gray-400">未分配</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="渠道统计" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<div class="channel-stats">
|
||||
<el-tag
|
||||
v-for="(item, index) in parseChannelStats(row.channel_stats)"
|
||||
:key="index"
|
||||
type="success"
|
||||
size="small"
|
||||
class="stat-tag"
|
||||
>
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
<span v-if="!row.channel_stats || row.channel_stats === '未设置'" class="text-gray-400">未设置</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="total_count" label="总诊单" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="font-semibold text-blue-600">{{ row.total_count }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="diagnosis_count" label="诊单数" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
content="统计期内该医生挂号涉及的去重诊单数,用于计算成交率"
|
||||
placement="top"
|
||||
>
|
||||
<span class="text-gray-700">{{ row.diagnosis_count ?? 0 }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="deal_count" label="成交单" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip content="上述诊单中已开有效处方(未删除、未作废)的数量" placement="top">
|
||||
<el-tag type="primary" size="small">{{ row.deal_count ?? 0 }}</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="deal_rate" label="成交率" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="getCompletionRateClass(Number(row.deal_rate ?? 0))">
|
||||
{{ row.deal_rate ?? 0 }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="registered_count" label="已挂号" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" size="small">{{ row.registered_count }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="completed_count" label="已完成" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success" size="small">{{ row.completed_count }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="missed_count" label="已过号" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="warning" size="small">{{ row.missed_count }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="cancelled_count" label="已取消" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="danger" size="small">{{ row.cancelled_count }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="completion_rate" label="完成率" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="getCompletionRateClass(row.completion_rate)">
|
||||
{{ row.completion_rate }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="time_range" label="统计时间" min-width="200" />
|
||||
|
||||
<template #empty>
|
||||
<el-empty description="暂无统计数据" />
|
||||
</template>
|
||||
</el-table>
|
||||
<!-- 统计数据卡片 -->
|
||||
<el-card v-loading="loading" class="mb-4" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">医生统计数据</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="statisticsList.length > 0">
|
||||
<el-table
|
||||
:data="statisticsList"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '600' }"
|
||||
>
|
||||
<el-table-column prop="doctor_name" label="医生姓名" width="120" />
|
||||
|
||||
<!-- 部门和渠道状态明细 -->
|
||||
<el-table-column label="状态明细" min-width="700">
|
||||
<template #default="{ row }">
|
||||
<div class="status-overview">
|
||||
<!-- 部门状态 -->
|
||||
<div class="status-section">
|
||||
<h4 class="section-title">部门状态</h4>
|
||||
<div v-if="row.dept_status_details && row.dept_status_details.length > 0">
|
||||
<div class="dept-grid">
|
||||
<div
|
||||
v-for="(dept, index) in row.dept_status_details"
|
||||
:key="index"
|
||||
class="dept-item"
|
||||
>
|
||||
<div class="dept-header">
|
||||
<span class="dept-name">{{ dept.dept_name }}</span>
|
||||
<span class="dept-total">({{ getDeptTotalCount(dept) }})</span>
|
||||
</div>
|
||||
<div class="status-tags">
|
||||
<el-tag
|
||||
v-for="(count, status) in dept.statuses"
|
||||
:key="status"
|
||||
:type="getStatusType(status)"
|
||||
size="small"
|
||||
class="compact-tag"
|
||||
>
|
||||
{{ getStatusName(status) }}: {{ count }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">暂无部门状态明细</span>
|
||||
</div>
|
||||
|
||||
<!-- 渠道状态 -->
|
||||
<div class="status-section">
|
||||
<h4 class="section-title">渠道状态</h4>
|
||||
<div v-if="row.channel_status_details && row.channel_status_details.length > 0">
|
||||
<div class="channel-grid">
|
||||
<div
|
||||
v-for="(channel, index) in row.channel_status_details"
|
||||
:key="index"
|
||||
class="channel-item"
|
||||
>
|
||||
<div class="channel-header">
|
||||
<span class="channel-name">{{ channel.channel_name }}</span>
|
||||
<span class="channel-total">({{ getChannelTotalCount(channel) }})</span>
|
||||
</div>
|
||||
<div class="status-tags">
|
||||
<el-tag
|
||||
v-for="(count, status) in channel.statuses"
|
||||
:key="status"
|
||||
:type="getStatusType(status)"
|
||||
size="small"
|
||||
class="compact-tag"
|
||||
>
|
||||
{{ getStatusName(status) }}: {{ count }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">暂无渠道状态明细</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="time_range" label="统计时间" min-width="200" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else class="empty-data">
|
||||
<el-empty description="暂无统计数据" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -213,6 +192,17 @@ const getStatistics = async () => {
|
||||
|
||||
const res = await getDoctorStatistics(params)
|
||||
statisticsList.value = res.lists || []
|
||||
|
||||
// 为每个医生添加必要的属性
|
||||
statisticsList.value.forEach(item => {
|
||||
// 确保数据结构完整
|
||||
if (!item.dept_status_details) {
|
||||
item.dept_status_details = []
|
||||
}
|
||||
if (!item.channel_status_details) {
|
||||
item.channel_status_details = []
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('获取统计数据错误:', error)
|
||||
ElMessage.error(error.msg || '获取统计数据失败')
|
||||
@@ -234,23 +224,38 @@ const handleReset = () => {
|
||||
getStatistics()
|
||||
}
|
||||
|
||||
const getCompletionRateClass = (rate: number) => {
|
||||
if (rate >= 80) return 'text-green-600 font-semibold'
|
||||
if (rate >= 60) return 'text-blue-600 font-semibold'
|
||||
if (rate >= 40) return 'text-orange-600 font-semibold'
|
||||
return 'text-red-600 font-semibold'
|
||||
// 计算部门总数量
|
||||
const getDeptTotalCount = (dept: any) => {
|
||||
if (!dept.statuses) return 0
|
||||
return Object.values(dept.statuses).reduce((total: number, count: number) => total + count, 0)
|
||||
}
|
||||
|
||||
// 解析部门统计字符串为数组
|
||||
const parseDeptStats = (stats: string) => {
|
||||
if (!stats || stats === '未分配') return []
|
||||
return stats.split(' ').filter(item => item.trim())
|
||||
// 计算渠道总数量
|
||||
const getChannelTotalCount = (channel: any) => {
|
||||
if (!channel.statuses) return 0
|
||||
return Object.values(channel.statuses).reduce((total: number, count: number) => total + count, 0)
|
||||
}
|
||||
|
||||
// 解析渠道统计字符串为数组
|
||||
const parseChannelStats = (stats: string) => {
|
||||
if (!stats || stats === '未设置') return []
|
||||
return stats.split(' ').filter(item => item.trim())
|
||||
// 获取状态名称
|
||||
const getStatusName = (status: string | number) => {
|
||||
const statusMap: Record<string | number, string> = {
|
||||
1: '已挂号',
|
||||
2: '已取消',
|
||||
3: '已完成',
|
||||
4: '已过号'
|
||||
}
|
||||
return statusMap[status] || `状态${status}`
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: string | number) => {
|
||||
const typeMap: Record<string | number, string> = {
|
||||
1: 'info',
|
||||
2: 'danger',
|
||||
3: 'success',
|
||||
4: 'warning'
|
||||
}
|
||||
return typeMap[status] || 'default'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -262,23 +267,155 @@ onMounted(() => {
|
||||
<style scoped lang="scss">
|
||||
.statistics-page {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
padding: 10px 0;
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dept-stats,
|
||||
.channel-stats {
|
||||
.status-overview {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.dept-grid, .channel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dept-item, .channel-item {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.dept-header, .channel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dept-name, .channel-name {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.dept-total, .channel-total {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.status-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-tag {
|
||||
font-weight: 500;
|
||||
.compact-tag {
|
||||
font-size: 11px !important;
|
||||
padding: 2px 8px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.empty-data {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.el-table) {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-table th) {
|
||||
background-color: #f5f7fa !important;
|
||||
font-weight: 600 !important;
|
||||
color: #606266 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table tr:hover) {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
/* 卡片样式优化 */
|
||||
:deep(.el-card) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1) !important;
|
||||
margin-bottom: 16px !important;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-card:hover) {
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
/* 折叠面板样式优化 */
|
||||
:deep(.el-collapse-item__header) {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
padding: 10px 20px;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__content) {
|
||||
padding: 10px 20px 15px !important;
|
||||
background-color: #f9f9f9;
|
||||
border-top: 1px solid #ebeef5;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
/* 表格单元格对齐 */
|
||||
:deep(.el-table__cell) {
|
||||
text-align: left !important;
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
/* 确保折叠面板从顶部开始显示 */
|
||||
:deep(.el-table__row) {
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
/* 去除默认的垂直居中样式 */
|
||||
:deep(.el-table__row > td) {
|
||||
vertical-align: top !important;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<div class="qywx-customer-page p-4">
|
||||
<el-card shadow="never" class="mb-4">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-lg font-semibold">企业微信客户管理</span>
|
||||
<div class="flex gap-2">
|
||||
<el-button type="primary" :loading="syncing" @click="syncCustomers">
|
||||
<template #icon><Refresh /></template>
|
||||
立即同步
|
||||
</el-button>
|
||||
<el-button @click="showSyncSettings = true">
|
||||
<template #icon><Setting /></template>
|
||||
同步设置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
title="功能说明"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="text-sm">
|
||||
<p>自动从企业微信同步外部联系人(客户)信息,包括客户基本信息、跟进人等。</p>
|
||||
<p class="mt-1">同步后的数据可用于患者信息匹配和自动关联。</p>
|
||||
</div>
|
||||
</el-alert>
|
||||
|
||||
<div class="grid grid-cols-4 gap-4 mb-4">
|
||||
<el-card shadow="hover">
|
||||
<div class="text-gray-500 text-sm mb-1">总客户数</div>
|
||||
<div class="text-2xl font-bold text-primary">{{ stats.total }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="text-gray-500 text-sm mb-1">今日新增</div>
|
||||
<div class="text-2xl font-bold text-success">{{ stats.today }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="text-gray-500 text-sm mb-1">最后同步</div>
|
||||
<div class="text-sm font-medium">{{ stats.lastSync || '未同步' }}</div>
|
||||
</el-card>
|
||||
<el-card shadow="hover">
|
||||
<div class="text-gray-500 text-sm mb-1">同步状态</div>
|
||||
<el-tag :type="stats.syncStatus === 'success' ? 'success' : 'info'" size="small">
|
||||
{{ stats.syncStatusText }}
|
||||
</el-tag>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never">
|
||||
<el-form :model="queryParams" inline class="mb-4">
|
||||
<el-form-item label="客户名称">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="搜索客户名称"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="跟进人">
|
||||
<el-input
|
||||
v-model="queryParams.follow_user"
|
||||
placeholder="跟进人姓名"
|
||||
clearable
|
||||
@keyup.enter="resetPage"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="pager.loading" :data="pager.lists" stripe>
|
||||
<el-table-column label="客户信息" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-avatar :size="40" :src="row.avatar" />
|
||||
<div>
|
||||
<div class="font-medium">{{ row.name }}</div>
|
||||
<div class="text-xs text-gray-400">{{ row.external_userid }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.type === 1 ? '微信' : '企微' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" width="60">
|
||||
<template #default="{ row }">
|
||||
{{ row.gender === 1 ? '男' : row.gender === 2 ? '女' : '未知' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="跟进人" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.follow_users && row.follow_users.length">
|
||||
<el-tag
|
||||
v-for="(user, idx) in row.follow_users"
|
||||
:key="idx"
|
||||
size="small"
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ user.name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="添加时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.update_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="viewDetail(row)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<pagination v-model="pager" @change="getLists" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 同步设置对话框 -->
|
||||
<el-dialog
|
||||
v-model="showSyncSettings"
|
||||
title="同步设置"
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="syncSettings" label-width="120px">
|
||||
<el-form-item label="自动同步">
|
||||
<el-switch v-model="syncSettings.auto_sync" />
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
开启后将定时自动同步企业微信客户数据
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="同步间隔">
|
||||
<el-select v-model="syncSettings.interval" :disabled="!syncSettings.auto_sync">
|
||||
<el-option label="每小时" :value="3600" />
|
||||
<el-option label="每2小时" :value="7200" />
|
||||
<el-option label="每4小时" :value="14400" />
|
||||
<el-option label="每6小时" :value="21600" />
|
||||
<el-option label="每12小时" :value="43200" />
|
||||
<el-option label="每天" :value="86400" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showSyncSettings = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveSyncSettings">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 客户详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="showDetail"
|
||||
title="客户详情"
|
||||
width="600px"
|
||||
>
|
||||
<el-descriptions v-if="currentCustomer" :column="2" border>
|
||||
<el-descriptions-item label="客户名称">{{ currentCustomer.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="External ID">{{ currentCustomer.external_userid }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">
|
||||
{{ currentCustomer.gender === 1 ? '男' : currentCustomer.gender === 2 ? '女' : '未知' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
{{ currentCustomer.type === 1 ? '微信用户' : '企业微信用户' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="企业名称" :span="2">
|
||||
{{ currentCustomer.corp_name || '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="职位" :span="2">
|
||||
{{ currentCustomer.position || '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="添加时间" :span="2">
|
||||
{{ formatTime(currentCustomer.create_time) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
{{ formatTime(currentCustomer.update_time) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { Refresh, Setting } from '@element-plus/icons-vue'
|
||||
import { usePaging } from '@/hooks/usePaging'
|
||||
import feedback from '@/utils/feedback'
|
||||
import {
|
||||
qywxCustomerLists,
|
||||
qywxCustomerSync,
|
||||
qywxCustomerStats,
|
||||
qywxSyncSettingsGet,
|
||||
qywxSyncSettingsSave
|
||||
} from '@/api/qywx'
|
||||
|
||||
const syncing = ref(false)
|
||||
const showSyncSettings = ref(false)
|
||||
const showDetail = ref(false)
|
||||
const currentCustomer = ref<any>(null)
|
||||
|
||||
const stats = reactive({
|
||||
total: 0,
|
||||
today: 0,
|
||||
lastSync: '',
|
||||
syncStatus: 'idle',
|
||||
syncStatusText: '未同步'
|
||||
})
|
||||
|
||||
const syncSettings = reactive({
|
||||
auto_sync: false,
|
||||
interval: 3600
|
||||
})
|
||||
|
||||
const queryParams = reactive({
|
||||
name: '',
|
||||
follow_user: ''
|
||||
})
|
||||
|
||||
let syncTimer: number | null = null
|
||||
|
||||
async function fetchLists(params: Record<string, unknown>) {
|
||||
return qywxCustomerLists(params)
|
||||
}
|
||||
|
||||
const { pager, getLists, resetPage, resetParams } = usePaging({
|
||||
fetchFun: fetchLists,
|
||||
params: queryParams
|
||||
})
|
||||
|
||||
function handleReset() {
|
||||
queryParams.name = ''
|
||||
queryParams.follow_user = ''
|
||||
resetParams()
|
||||
}
|
||||
|
||||
async function syncCustomers() {
|
||||
syncing.value = true
|
||||
try {
|
||||
await qywxCustomerSync()
|
||||
feedback.msgSuccess('同步成功')
|
||||
await loadStats()
|
||||
await getLists()
|
||||
} catch (e: any) {
|
||||
feedback.msgError(e?.message || '同步失败')
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res: any = await qywxCustomerStats()
|
||||
Object.assign(stats, res.data || res)
|
||||
} catch (e) {
|
||||
console.error('加载统计失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSyncSettings() {
|
||||
try {
|
||||
const res: any = await qywxSyncSettingsGet()
|
||||
Object.assign(syncSettings, res.data || res)
|
||||
|
||||
// 如果开启了自动同步,启动定时器
|
||||
if (syncSettings.auto_sync) {
|
||||
startSyncTimer()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载同步设置失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSyncSettings() {
|
||||
try {
|
||||
await qywxSyncSettingsSave(syncSettings)
|
||||
feedback.msgSuccess('保存成功')
|
||||
showSyncSettings.value = false
|
||||
|
||||
// 重新启动定时器
|
||||
stopSyncTimer()
|
||||
if (syncSettings.auto_sync) {
|
||||
startSyncTimer()
|
||||
}
|
||||
} catch (e: any) {
|
||||
feedback.msgError(e?.message || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
function startSyncTimer() {
|
||||
if (syncTimer) return
|
||||
|
||||
syncTimer = window.setInterval(() => {
|
||||
syncCustomers()
|
||||
}, syncSettings.interval * 1000)
|
||||
}
|
||||
|
||||
function stopSyncTimer() {
|
||||
if (syncTimer) {
|
||||
clearInterval(syncTimer)
|
||||
syncTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function viewDetail(row: any) {
|
||||
currentCustomer.value = row
|
||||
showDetail.value = true
|
||||
}
|
||||
|
||||
function formatTime(timestamp: number | string) {
|
||||
if (!timestamp) return '—'
|
||||
const date = new Date(typeof timestamp === 'number' ? timestamp * 1000 : timestamp)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getLists()
|
||||
loadStats()
|
||||
loadSyncSettings()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopSyncTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.qywx-customer-page {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -36,6 +36,7 @@
|
||||
<el-option label="已支付" :value="2" />
|
||||
<el-option label="已取消" :value="3" />
|
||||
<el-option label="已退款" :value="4" />
|
||||
<el-option label="待审核" :value="5" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -808,7 +809,8 @@ const getStatusText = (status: number) => {
|
||||
1: '待支付',
|
||||
2: '已支付',
|
||||
3: '已取消',
|
||||
4: '已退款'
|
||||
4: '已退款',
|
||||
5: '待审核'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
@@ -819,7 +821,8 @@ const getStatusTag = (status: number): 'warning' | 'success' | 'info' | 'danger'
|
||||
1: 'warning',
|
||||
2: 'success',
|
||||
3: 'info',
|
||||
4: 'danger'
|
||||
4: 'danger',
|
||||
5: 'warning'
|
||||
}
|
||||
return tagMap[status] || 'info'
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
{{ row.call_type === 1 ? '语音' : '视频' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="房间号" width="140">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.room_id" class="text-primary">{{ row.room_id }}</span>
|
||||
<span v-else class="text-gray-400">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时长" width="110" prop="duration_text" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ref, shallowRef, watch, computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getImChatMessages } from '@/api/tcm'
|
||||
@@ -80,7 +80,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref<any[]>([])
|
||||
const rows = shallowRef<any[]>([])
|
||||
const patientImId = ref('')
|
||||
const patientName = ref('')
|
||||
|
||||
|
||||
@@ -580,10 +580,10 @@
|
||||
lazy
|
||||
>
|
||||
<im-chat-record-panel
|
||||
v-if="formData.id"
|
||||
v-if="activeTab === 'chat' && formData.id"
|
||||
:diagnosis-id="Number(formData.id)"
|
||||
/>
|
||||
<el-empty v-else description="请先保存诊单后再查看聊天记录" />
|
||||
<el-empty v-else-if="!formData.id" description="请先保存诊单后再查看聊天记录" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-if="
|
||||
|
||||
Reference in New Issue
Block a user