This commit is contained in:
Your Name
2026-04-11 18:06:02 +08:00
parent 4909ec6daa
commit abcecf66e7
325 changed files with 4199 additions and 633 deletions
+26
View File
@@ -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 })
}
+9 -1
View File
@@ -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 })
+3 -2
View File
@@ -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"
+38 -15
View File
@@ -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
View File
@@ -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>
+347
View File
@@ -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>
+5 -2
View File
@@ -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('')
+2 -2
View File
@@ -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="