更新
This commit is contained in:
@@ -126,7 +126,7 @@ export function myPatientCancelAppointment(params: { id: number }) {
|
||||
}
|
||||
|
||||
export interface FirstVisitConversionParams {
|
||||
time_type: 'today' | 'week' | 'month' | 'quarter' | 'year'
|
||||
time_type: 'today' | 'yesterday' | 'week' | 'month' | 'quarter' | 'year'
|
||||
dept_id?: number
|
||||
assistant_id?: number
|
||||
}
|
||||
@@ -173,14 +173,6 @@ export function wecomPromotionOverview() {
|
||||
return request.get({ url: '/firstvisit.wecomPromotion/overview' })
|
||||
}
|
||||
|
||||
export function wecomPromotionAuthorizationUrl() {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/authorizationUrl' })
|
||||
}
|
||||
|
||||
export function wecomPromotionVerifyAccount(params: { id: number }) {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/verifyAccount', params })
|
||||
}
|
||||
|
||||
export function wecomPromotionSavePool(params: Record<string, unknown>) {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/savePool', params })
|
||||
}
|
||||
@@ -193,6 +185,22 @@ export function wecomPromotionSaveLink(params: Record<string, unknown>) {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/saveLink', params })
|
||||
}
|
||||
|
||||
export function wecomPromotionCheckApiPermission() {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/checkApiPermission' })
|
||||
}
|
||||
|
||||
export function wecomPromotionSyncRemoteLinks(params: { pool_id: number }) {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/syncRemoteLinks', params, timeout: 120000 })
|
||||
}
|
||||
|
||||
export function wecomPromotionRemoteLinkDetail(params: { id: number }) {
|
||||
return request.get({ url: '/firstvisit.wecomPromotion/remoteLinkDetail', params })
|
||||
}
|
||||
|
||||
export function wecomPromotionDeleteRemoteLink(params: { id: number }) {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/deleteRemoteLink', params })
|
||||
}
|
||||
|
||||
export function wecomPromotionToggleLink(params: { id: number; status: number }) {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/toggleLink', params })
|
||||
}
|
||||
@@ -200,3 +208,61 @@ export function wecomPromotionToggleLink(params: { id: number; status: number })
|
||||
export function wecomPromotionDeleteLink(params: { id: number }) {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/deleteLink', params })
|
||||
}
|
||||
|
||||
export type WecomPromotionCustomerChatStatus = '' | 'messaged' | 'silent' | 'unknown'
|
||||
|
||||
export interface WecomPromotionCustomerStatsParams {
|
||||
page_no: number
|
||||
page_size: number
|
||||
promotion_link_id?: number
|
||||
userid?: string
|
||||
chat_status?: 0 | 1 | 2
|
||||
}
|
||||
|
||||
export interface WecomPromotionCustomerStatsSummary {
|
||||
customer_count: number
|
||||
messaged_customer_count: number
|
||||
message_customer_rate: number
|
||||
received_message_count: number
|
||||
message_count_known_count: number
|
||||
}
|
||||
|
||||
export interface WecomPromotionCustomerStatRow {
|
||||
id?: number
|
||||
external_userid_masked?: string
|
||||
customer_id_masked?: string
|
||||
customer_name_masked?: string
|
||||
link_id?: number | string
|
||||
link_name?: string
|
||||
member_id?: number
|
||||
member_name?: string
|
||||
department_name?: string
|
||||
dept_name?: string
|
||||
has_messaged?: boolean | number
|
||||
chat_status?: 0 | 1 | 2
|
||||
message_count_known?: boolean | number
|
||||
received_message_count?: number
|
||||
last_synced_at?: string
|
||||
last_message_at?: string
|
||||
}
|
||||
|
||||
export interface WecomPromotionCustomerStatsResult {
|
||||
summary?: Partial<WecomPromotionCustomerStatsSummary>
|
||||
lists?: WecomPromotionCustomerStatRow[]
|
||||
total?: number
|
||||
link_options?: Array<{ id: number | string; name: string }>
|
||||
member_options?: Array<{ id: number; name: string; department_name?: string; dept_name?: string }>
|
||||
meta?: { last_synced_at?: string }
|
||||
}
|
||||
|
||||
/** 获客客户消息统计:服务端继续按当前角色和部门数据范围收窄。 */
|
||||
export function wecomPromotionCustomerStats(params: WecomPromotionCustomerStatsParams) {
|
||||
return request.get(
|
||||
{ url: '/firstvisit.wecomPromotion/customerStatistics', params, timeout: 120000 },
|
||||
{ ignoreCancelToken: true }
|
||||
)
|
||||
}
|
||||
|
||||
export function wecomPromotionSyncCustomers(params: { promotion_link_id?: number } = {}) {
|
||||
return request.post({ url: '/firstvisit.wecomPromotion/syncCustomers', params, timeout: 120000 })
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>部门订单量占比</h2>
|
||||
<p>当前范围内审核通过的接诊诊单</p>
|
||||
<p>双审通过,并排除取消、拒收及退款</p>
|
||||
</div>
|
||||
<span>单位:单</span>
|
||||
</div>
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>部门金额占比</h2>
|
||||
<p>与诊单金额指标保持同一审核口径</p>
|
||||
<p>仅统计未取消、未拒收且未退款的有效金额</p>
|
||||
</div>
|
||||
<span>单位:元</span>
|
||||
</div>
|
||||
@@ -231,6 +231,7 @@ const query = reactive<FirstVisitConversionParams>({ time_type: 'today', dept_id
|
||||
const deptTreeProps = { value: 'id', label: 'name', children: 'children' }
|
||||
const timeOptions = [
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '昨天', value: 'yesterday' },
|
||||
{ label: '本周', value: 'week' },
|
||||
{ label: '本月', value: 'month' },
|
||||
{ label: '本季度', value: 'quarter' },
|
||||
@@ -240,8 +241,8 @@ const metricCards: Array<{ key: string; label: string; type: MetricType; hint: s
|
||||
{ key: 'add_fans_count', label: '加粉数', type: 'count', hint: '企微新增客户' },
|
||||
{ key: 'total_open_count', label: '开口数', type: 'count', hint: '来源于个人业绩录入' },
|
||||
{ key: 'interview_count', label: '面诊', type: 'count', hint: '已完成挂号' },
|
||||
{ key: 'completed_order_count', label: '接诊诊单', type: 'count', hint: '处方与支付审核通过' },
|
||||
{ key: 'completed_order_amount', label: '诊单金额', type: 'money', hint: '与接诊诊单同口径' },
|
||||
{ key: 'completed_order_count', label: '接诊诊单', type: 'count', hint: '双审通过,排除取消、拒收及退款' },
|
||||
{ key: 'completed_order_amount', label: '诊单金额', type: 'money', hint: '仅统计有效诊单金额' },
|
||||
{ key: 'avg_unit_price', label: '平均客单价', type: 'money', hint: '诊单金额 / 接诊诊单' },
|
||||
{ key: 'account_cost', label: '现金成本', type: 'money', hint: '当前范围内实际投放成本' },
|
||||
{ key: 'roi', label: 'ROI', type: 'ratio', hint: '诊单金额 / 投放成本' }
|
||||
|
||||
@@ -52,9 +52,9 @@
|
||||
<small>笔</small>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>订单金额</span>
|
||||
<span>有效订单金额</span>
|
||||
<strong>¥{{ money(summary.amount) }}</strong>
|
||||
<small>当前筛选</small>
|
||||
<small>排除取消、拒收及退款</small>
|
||||
</div>
|
||||
<div class="metric-card metric-warning">
|
||||
<span>待审核</span>
|
||||
@@ -98,8 +98,11 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="108" align="right">
|
||||
<template #default="{ row }"><strong class="amount">¥{{ money(row.amount) }}</strong></template>
|
||||
<el-table-column label="有效金额" width="118" align="right">
|
||||
<template #default="{ row }">
|
||||
<strong v-if="row.amount_included" class="amount">¥{{ money(row.effective_amount) }}</strong>
|
||||
<span v-else class="amount-excluded">{{ row.amount_exclusion_text || '不计入' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处方审核" width="104" align="center">
|
||||
<template #default="{ row }">
|
||||
@@ -437,6 +440,11 @@ onMounted(getLists)
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.amount-excluded {
|
||||
color: #9aa4b2;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.order-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -4,21 +4,21 @@
|
||||
<div class="heading-copy">
|
||||
<span class="heading-icon"><el-icon><Promotion /></el-icon></span>
|
||||
<div>
|
||||
<h1>企业微信推广助手</h1>
|
||||
<p>授权企业微信后,将多个推广链接按权重、时段和每日上限安全分流</p>
|
||||
<h1>企业微信获客助手</h1>
|
||||
<p>对接企业微信获客助手官方 API,按角色与部门管理获客成员,并将链接安全分流到推广页面</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="heading-actions">
|
||||
<span class="scope-chip"><el-icon><Lock /></el-icon>{{ overview.meta.scope_label || '当前数据范围' }}</span>
|
||||
<span v-if="overview.meta.generated_at" class="update-time">更新于 {{ overview.meta.generated_at }}</span>
|
||||
<el-button :icon="Refresh" :loading="loading" @click="loadOverview">刷新</el-button>
|
||||
<el-button :icon="Refresh" :loading="loading || customerStatsLoading" @click="refreshCurrentTab">刷新</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="metric-grid">
|
||||
<article class="metric-card">
|
||||
<span class="metric-icon is-teal"><el-icon><OfficeBuilding /></el-icon></span>
|
||||
<div><small>已授权企业</small><strong>{{ overview.summary.authorized_accounts }}</strong><p>凭证仅在服务端加密保存</p></div>
|
||||
<div><small>内部应用</small><strong>{{ overview.summary.configured_apps }}</strong><p>直接读取服务器环境配置</p></div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span class="metric-icon is-blue"><el-icon><SetUp /></el-icon></span>
|
||||
@@ -35,12 +35,15 @@
|
||||
</section>
|
||||
|
||||
<section class="workspace-card">
|
||||
<nav class="tab-nav" aria-label="企业微信推广助手功能">
|
||||
<nav class="tab-nav" aria-label="企业微信获客助手功能">
|
||||
<button :class="{ active: activeTab === 'links' }" @click="activeTab = 'links'">
|
||||
<el-icon><Connection /></el-icon>推广链接
|
||||
<el-icon><Connection /></el-icon>获客助手链接
|
||||
</button>
|
||||
<button :class="{ active: activeTab === 'authorization' }" @click="activeTab = 'authorization'">
|
||||
<el-icon><Key /></el-icon>企业授权
|
||||
<button :class="{ active: activeTab === 'customer-stats' }" @click="activeTab = 'customer-stats'">
|
||||
<el-icon><DataAnalysis /></el-icon>获客客户统计
|
||||
</button>
|
||||
<button :class="{ active: activeTab === 'configuration' }" @click="activeTab = 'configuration'">
|
||||
<el-icon><Key /></el-icon>应用配置
|
||||
</button>
|
||||
<button :class="{ active: activeTab === 'install' }" @click="activeTab = 'install'">
|
||||
<el-icon><DocumentCopy /></el-icon>JS 安装
|
||||
@@ -50,8 +53,8 @@
|
||||
<div v-if="activeTab === 'links'" class="tab-content links-tab">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>推广链接分流</h2>
|
||||
<p>访客点击时由服务端从当前可用链接中按权重随机选择,页面端不会暴露完整链接池。</p>
|
||||
<h2>获客助手链接分流</h2>
|
||||
<p>访客点击时由服务端从当前可用的获客助手链接中按权重随机选择,页面端不会暴露完整链接池。</p>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">新建分流方案</el-button>
|
||||
</div>
|
||||
@@ -84,23 +87,40 @@
|
||||
<p>公开键:{{ selectedPool.public_key }}</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button :icon="CircleCheck" :loading="checkingApi" @click="checkApiPermission">验证 API</el-button>
|
||||
<el-button :icon="Refresh" :loading="syncingRemote" @click="syncRemoteLinks">同步企业微信</el-button>
|
||||
<el-button :icon="DocumentCopy" @click="copyText(selectedPool.install_code, 'JS 安装代码')">复制 JS</el-button>
|
||||
<el-button :icon="Edit" @click="openPoolDialog(selectedPool)">编辑方案</el-button>
|
||||
<el-button type="danger" plain :icon="Delete" @click="removePool(selectedPool)">删除</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openLinkDialog()">添加推广链接</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openLinkDialog()">创建官方获客链接</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="selectedLegacyCount > 0 && selectedOfficialCount === 0"
|
||||
class="legacy-sync-alert"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
:title="`当前方案的 ${selectedLegacyCount} 条链接都是历史手工链接,企业微信同步不会返回它们`"
|
||||
description="官方接口只返回当前应用通过 API 创建的获客链接。现有链接仍可参与本地分流;如需同步 link_id、客户和官方统计,请点击“创建官方获客链接”。"
|
||||
/>
|
||||
|
||||
<el-table :data="selectedLinks" class="link-table" stripe>
|
||||
<el-table-column label="推广成员 / 分组" min-width="180" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<div class="member-cell"><strong>{{ row.name }}</strong><small>{{ row.group_name || '默认分组' }}</small></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="授权企业" min-width="150">
|
||||
<el-table-column label="企业微信链接 ID" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.account_id">{{ row.corp_name || '授权已失效' }}</span>
|
||||
<span v-else class="muted">未关联</span>
|
||||
<span v-if="row.remote_link_id" class="remote-id" :title="row.remote_link_id">{{ row.remote_link_id }}</span>
|
||||
<span v-else class="legacy-tag">历史手工链接</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可用成员" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span>{{ remoteMemberLabel(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="权重" prop="weight" width="84" align="center" />
|
||||
@@ -115,77 +135,189 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="上线" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch :model-value="Number(row.status) === 1" @change="(value) => handleToggle(row, value)" />
|
||||
<el-switch :model-value="Number(row.status) === 1" :disabled="Number(row.remote_status) === 2" @change="(value) => handleToggle(row, value)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="132" fixed="right">
|
||||
<el-table-column label="操作" min-width="245" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="openLinkDialog(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="removeLink(row)">删除</el-button>
|
||||
<el-button v-if="row.is_official && Number(row.remote_status) !== 2" type="primary" link :loading="refreshingLinkId === Number(row.id)" @click="refreshRemoteDetail(row)">刷新详情</el-button>
|
||||
<el-button v-if="!row.is_official || Number(row.remote_status) !== 2" type="primary" link @click="openLinkDialog(row)">编辑</el-button>
|
||||
<el-dropdown v-if="row.is_official" trigger="click" @command="(command) => handleLinkCommand(command, row)">
|
||||
<el-button type="primary" link>更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="local">从本地移除</el-dropdown-item>
|
||||
<el-dropdown-item v-if="Number(row.remote_status) !== 2" command="remote" divided class="danger-command">永久删除企业微信链接</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button v-else type="danger" link @click="removeLink(row)">从本地移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty :image-size="72" description="当前方案还没有推广链接" /></template>
|
||||
<template #empty><el-empty :image-size="72" description="当前方案还没有获客助手链接" /></template>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="先创建一个分流方案,再添加企业微信推广链接">
|
||||
<el-empty v-else description="先创建一个分流方案,再添加企业微信获客助手链接">
|
||||
<el-button type="primary" :icon="Plus" @click="openPoolDialog()">创建第一个方案</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'authorization'" class="tab-content authorization-tab">
|
||||
<div v-else-if="activeTab === 'customer-stats'" class="tab-content customer-stats-tab">
|
||||
<div class="section-heading customer-stats-heading">
|
||||
<div>
|
||||
<h2>获客客户消息统计</h2>
|
||||
<p>统计当前权限范围内由获客助手链接添加的客户,以及客户向承接成员发送消息的情况。</p>
|
||||
</div>
|
||||
<div class="customer-heading-actions">
|
||||
<span v-if="customerStats.meta.last_synced_at" class="update-time">最近同步 {{ customerStats.meta.last_synced_at }}</span>
|
||||
<el-button type="primary" :icon="Refresh" :loading="syncingCustomers" @click="syncCustomers">同步客户</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="customer-metric-grid" aria-label="获客客户消息汇总">
|
||||
<article class="customer-metric-card">
|
||||
<span class="metric-icon is-teal"><el-icon><User /></el-icon></span>
|
||||
<div><small>获客客户数</small><strong>{{ formatNumber(customerStats.summary.customer_count) }}</strong><p>当前筛选范围内的去重客户</p></div>
|
||||
</article>
|
||||
<article class="customer-metric-card">
|
||||
<span class="metric-icon is-blue"><el-icon><ChatDotRound /></el-icon></span>
|
||||
<div><small>已发消息客户数</small><strong>{{ formatNumber(customerStats.summary.messaged_customer_count) }}</strong><p>至少发送过一条消息</p></div>
|
||||
</article>
|
||||
<article class="customer-metric-card">
|
||||
<span class="metric-icon is-green"><el-icon><DataAnalysis /></el-icon></span>
|
||||
<div><small>消息客户率</small><strong>{{ formatCustomerRate(customerStats.summary.message_customer_rate) }}</strong><p>已发消息客户占获客客户比例</p></div>
|
||||
</article>
|
||||
<article class="customer-metric-card">
|
||||
<span class="metric-icon is-orange"><el-icon><Connection /></el-icon></span>
|
||||
<div><small>累计接收消息数</small><strong>{{ formatNumber(customerStats.summary.received_message_count) }}</strong><p>已精确统计 {{ formatNumber(customerStats.summary.message_count_known_count) }} 位客户</p></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<el-alert
|
||||
v-if="!overview.config.callback_ready"
|
||||
class="chatkey-alert"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
title="消息次数回调尚未配置完成"
|
||||
description="请在企业微信获客助手的 API 接入场景中配置本页“应用配置”展示的消息回调地址,并在服务器配置 contact_callback_token 与 contact_callback_aes_key。未完成前只能同步客户及聊天状态,无法得到精确接收消息次数。"
|
||||
/>
|
||||
|
||||
<el-alert
|
||||
class="chatkey-alert"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
title="消息统计依赖企业微信 ChatKey"
|
||||
description="ChatKey 仅在事件产生后的 30 分钟内有效。服务端收到企业微信回调后会立即拉取会话详情;手动同步用于刷新客户清单,无法补取已过期的会话消息。"
|
||||
/>
|
||||
|
||||
<div class="customer-filter-bar">
|
||||
<el-form :model="customerFilters" inline label-position="top">
|
||||
<el-form-item label="来源链接">
|
||||
<el-select v-model="customerFilters.link_id" clearable filterable placeholder="全部链接">
|
||||
<el-option v-for="option in customerLinkOptions" :key="String(option.id)" :label="option.name" :value="option.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="承接成员">
|
||||
<el-select v-model="customerFilters.userid" clearable filterable placeholder="全部成员">
|
||||
<el-option v-for="option in customerMemberOptions" :key="option.userid || String(option.id)" :label="memberFilterLabel(option)" :value="option.userid || String(option.id)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="聊天状态">
|
||||
<el-select v-model="customerFilters.chat_status" clearable placeholder="全部状态">
|
||||
<el-option label="已发过消息" value="messaged" />
|
||||
<el-option label="未发过消息" value="silent" />
|
||||
<el-option label="状态未知" value="unknown" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="filter-actions">
|
||||
<el-button type="primary" :icon="Search" :loading="customerStatsLoading" @click="applyCustomerFilters">查询</el-button>
|
||||
<el-button @click="resetCustomerFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="customerStatsLoading" :data="customerStats.rows" class="customer-table" stripe>
|
||||
<el-table-column label="客户标识(已脱敏)" min-width="190" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<span class="customer-identifier" :title="customerIdentifier(row)">{{ customerIdentifier(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源链接" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div class="member-cell"><strong>{{ customerLinkName(row) }}</strong><small>{{ customerLinkId(row) }}</small></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="承接成员 / 部门" min-width="175">
|
||||
<template #default="{ row }">
|
||||
<div class="member-cell"><strong>{{ row.member_name || row.owner_name || '未匹配成员' }}</strong><small>{{ row.department_name || row.dept_name || '未分部门' }}</small></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否发过消息" width="125" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="message-state" :class="`is-${customerChatState(row)}`">{{ customerChatStateLabel(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接收消息数" width="120" align="right">
|
||||
<template #default="{ row }"><strong class="message-count" :class="{ muted: !customerMessageCountKnown(row) }">{{ customerMessageCountLabel(row) }}</strong></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近同步时间" min-width="165">
|
||||
<template #default="{ row }">{{ formatCustomerTime(row.last_synced_at || row.last_sync_time || row.synced_at || row.sync_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近消息时间" min-width="165">
|
||||
<template #default="{ row }"><span :class="{ muted: !customerLastMessageTime(row) }">{{ formatCustomerTime(customerLastMessageTime(row)) }}</span></template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty :image-size="72" description="当前筛选范围暂无获客客户数据" /></template>
|
||||
</el-table>
|
||||
|
||||
<div class="customer-pagination">
|
||||
<span>共 {{ formatNumber(customerPager.total) }} 位客户</span>
|
||||
<el-pagination
|
||||
v-model:current-page="customerPager.page_no"
|
||||
v-model:page-size="customerPager.page_size"
|
||||
background
|
||||
layout="sizes, prev, pager, next"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="customerPager.total"
|
||||
@current-change="loadCustomerStats"
|
||||
@size-change="handleCustomerPageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'configuration'" class="tab-content authorization-tab">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>企业微信第三方应用授权</h2>
|
||||
<p>永久授权码、suite_ticket 等敏感凭证全部加密存放,前端接口只返回企业名称和脱敏 CorpID。</p>
|
||||
<h2>企业微信内部应用配置</h2>
|
||||
<p>直接读取服务器现有的 work_wechat 配置,密钥不会返回浏览器,也不需要第三方应用授权。</p>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="overview.meta.can_authorize"
|
||||
type="primary"
|
||||
:icon="CirclePlus"
|
||||
:loading="authorizing"
|
||||
:disabled="!overview.config.ready"
|
||||
@click="startAuthorization"
|
||||
>授权新企业</el-button>
|
||||
</div>
|
||||
|
||||
<div class="configuration-panel" :class="overview.config.ready ? 'is-ready' : 'is-pending'">
|
||||
<div class="configuration-state">
|
||||
<span><el-icon><component :is="overview.config.ready ? CircleCheck : Warning" /></el-icon></span>
|
||||
<div>
|
||||
<strong>{{ overview.config.ready ? '授权通道已就绪' : '授权通道等待配置' }}</strong>
|
||||
<p v-if="overview.config.ready">已收到企业微信 suite_ticket,可以发起第三方应用安装授权。</p>
|
||||
<p v-else-if="overview.config.configured">配置已保存,尚未收到 suite_ticket,请核对应用指令回调地址。</p>
|
||||
<p v-else>请由运维按部署文档补齐服务商参数;页面不会展示任何密钥值。</p>
|
||||
<strong>{{ overview.config.ready ? '内部应用已就绪' : '内部应用配置不完整' }}</strong>
|
||||
<p v-if="overview.config.ready">已识别 CorpID 与应用 Secret;点击“验证 API”可同时检查可信 IP、获客助手开通状态和应用权限。</p>
|
||||
<p v-else>请补齐服务器 .env 的 [work_wechat] 配置;页面不会展示 Secret 内容。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="overview.config.missing?.length" class="missing-fields">
|
||||
缺少:<code v-for="item in overview.config.missing" :key="item">{{ item }}</code>
|
||||
</div>
|
||||
<dl class="callback-list">
|
||||
<div><dt>应用指令回调</dt><dd>{{ overview.config.provider_callback_url || '—' }}</dd><button @click="copyText(overview.config.provider_callback_url, '应用指令回调')">复制</button></div>
|
||||
<div><dt>授权完成回调</dt><dd>{{ overview.config.auth_callback_url || '—' }}</dd><button @click="copyText(overview.config.auth_callback_url, '授权完成回调')">复制</button></div>
|
||||
<div><dt>企业 CorpID</dt><dd>{{ overview.config.corp_id_masked || '未配置' }}</dd><span>{{ overview.config.corp_id_masked ? '已识别' : '缺失' }}</span></div>
|
||||
<div><dt>应用 AgentID</dt><dd>{{ overview.config.agent_id || '未配置' }}</dd><span>{{ overview.config.agent_id ? '已识别' : '缺失' }}</span></div>
|
||||
<div><dt>应用 Secret</dt><dd>仅保存在服务器环境变量中</dd><span>{{ overview.config.secret_configured ? '已配置' : '缺失' }}</span></div>
|
||||
<div><dt>消息回调地址</dt><dd>{{ overview.config.callback_url || '未生成' }}</dd><span>{{ overview.config.callback_ready ? '已配置密钥' : '待配置 Token/AESKey' }}</span></div>
|
||||
</dl>
|
||||
<div class="configuration-actions">
|
||||
<el-button type="primary" :loading="checkingApi" @click="checkApiPermission">验证获客助手 API</el-button>
|
||||
<el-link v-if="overview.config.official_doc" :href="overview.config.official_doc" target="_blank" type="primary">查看企业微信官方文档</el-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="overview.accounts" class="account-table" stripe>
|
||||
<el-table-column prop="corp_name" label="企业微信名称" min-width="180" fixed="left">
|
||||
<template #default="{ row }"><strong>{{ row.corp_name }}</strong></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="corp_id_masked" label="CorpID(脱敏)" min-width="190" />
|
||||
<el-table-column prop="agent_id" label="应用 AgentID" min-width="130">
|
||||
<template #default="{ row }">{{ row.agent_id || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="授权状态" min-width="110">
|
||||
<template #default="{ row }"><span class="availability" :class="Number(row.auth_status) === 1 ? 'is-ok' : 'is-error'">{{ Number(row.auth_status) === 1 ? '有效' : '已取消' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="授权时间" min-width="170"><template #default="{ row }">{{ formatTime(row.authorized_at) }}</template></el-table-column>
|
||||
<el-table-column label="最近校验" min-width="170"><template #default="{ row }">{{ formatTime(row.last_refresh_at) }}</template></el-table-column>
|
||||
<el-table-column v-if="overview.meta.can_authorize" label="操作" width="110" fixed="right">
|
||||
<template #default="{ row }"><el-button type="primary" link :loading="verifyingId === Number(row.id)" @click="verifyAccount(row)">验证凭证</el-button></template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty :image-size="72" description="尚未授权企业微信" /></template>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div v-else class="tab-content install-tab">
|
||||
@@ -214,7 +346,7 @@
|
||||
</div>
|
||||
|
||||
<div class="rule-panel">
|
||||
<div><el-icon><Select /></el-icon><span><strong>可用性筛选</strong>自动排除停用、授权失效、超出时间段和达到每日上限的链接。</span></div>
|
||||
<div><el-icon><Select /></el-icon><span><strong>可用性筛选</strong>自动排除停用、超出时间段和达到每日上限的链接。</span></div>
|
||||
<div><el-icon><Opportunity /></el-icon><span><strong>权重随机</strong>权重越高,被选中的概率越大;没有可用链接时才使用兜底链接。</span></div>
|
||||
<div><el-icon><View /></el-icon><span><strong>隐私与安全</strong>前端看不到完整链接池;访问 IP 只保存带服务端密钥的不可逆哈希。</span></div>
|
||||
</div>
|
||||
@@ -231,37 +363,39 @@
|
||||
<el-dialog v-model="poolDialogVisible" :title="poolForm.id ? '编辑分流方案' : '新建分流方案'" width="560px" destroy-on-close>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="方案名称" required><el-input v-model="poolForm.name" maxlength="60" show-word-limit placeholder="例如:官网咨询分流" /></el-form-item>
|
||||
<el-form-item label="兜底企业微信链接">
|
||||
<el-form-item label="兜底获客助手链接">
|
||||
<el-input v-model="poolForm.fallback_url" placeholder="可选;没有可用成员时跳转" />
|
||||
<span class="form-tip">仅允许配置页列出的企业微信 HTTPS 域名。</span>
|
||||
<span class="form-tip">仅接受企业微信获客助手生成的 https://work.weixin.qq.com/ca/... 链接。</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="运行状态"><el-switch v-model="poolForm.status" :active-value="1" :inactive-value="0" active-text="运行" inactive-text="停用" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="poolDialogVisible = false">取消</el-button><el-button type="primary" :loading="savingPool" @click="savePool">保存方案</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="linkDialogVisible" :title="linkForm.id ? '编辑推广链接' : '添加推广链接'" width="680px" destroy-on-close>
|
||||
<el-dialog v-model="linkDialogVisible" :title="linkForm.id ? '编辑获客助手链接' : '创建企业微信官方获客链接'" width="720px" destroy-on-close>
|
||||
<el-form label-position="top" class="link-form">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="推广成员名称" required><el-input v-model="linkForm.name" maxlength="80" placeholder="用于内部识别" /></el-form-item>
|
||||
<el-form-item label="获客链接名称" required><el-input v-model="linkForm.name" maxlength="80" placeholder="用于企业微信和后台识别" /></el-form-item>
|
||||
<el-form-item label="分组"><el-input v-model="linkForm.group_name" maxlength="60" placeholder="默认分组" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item label="企业微信推广链接" required>
|
||||
<el-input v-model="linkForm.wecom_url" type="textarea" :rows="2" placeholder="粘贴企业微信成员联系我、客户群或其他允许的推广链接" />
|
||||
<span class="form-tip">允许域名:{{ overview.allowed_link_hosts.join('、') || '请先配置允许域名' }}</span>
|
||||
<el-form-item v-if="!linkForm.legacy" label="获客成员" required>
|
||||
<el-select v-model="linkForm.member_admin_ids" multiple filterable collapse-tags collapse-tags-tooltip placeholder="仅显示当前角色和部门可管理、且已绑定企业微信的成员" style="width: 100%">
|
||||
<el-option v-for="member in overview.member_options" :key="member.id" :label="memberOptionLabel(member)" :value="Number(member.id)" />
|
||||
</el-select>
|
||||
<span class="form-tip">官方链接按这里的企业微信 userid 建立可见范围;越权成员不会出现在列表中。</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="企业微信获客助手链接" required>
|
||||
<el-input v-model="linkForm.wecom_url" type="textarea" :rows="2" :placeholder="overview.customer_acquisition_link_example" />
|
||||
<span class="form-tip">这是历史手工链接,只更新本地分流规则,不会调用企业微信创建或修改接口。</span>
|
||||
</el-form-item>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="关联授权企业">
|
||||
<el-select v-model="linkForm.account_id" clearable placeholder="可选;用于授权失效联动下线">
|
||||
<el-option v-for="account in activeAccounts" :key="account.id" :label="account.corp_name" :value="Number(account.id)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分流权重" required><el-input-number v-model="linkForm.weight" :min="1" :max="100" controls-position="right" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="每日点击上限"><el-input-number v-model="linkForm.daily_limit" :min="0" :max="1000000" controls-position="right" /><span class="form-tip">0 表示不限</span></el-form-item>
|
||||
<el-form-item label="上线状态"><el-switch v-model="linkForm.status" :active-value="1" :inactive-value="0" active-text="上线" inactive-text="下线" /></el-form-item>
|
||||
</div>
|
||||
<el-form-item v-if="!linkForm.legacy" label="添加客户时跳过验证">
|
||||
<el-switch v-model="linkForm.skip_verify" :active-value="1" :inactive-value="0" active-text="跳过验证" inactive-text="需要验证" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上线状态"><el-switch v-model="linkForm.status" :active-value="1" :inactive-value="0" active-text="上线" inactive-text="下线" /></el-form-item>
|
||||
<el-form-item label="生效时间段">
|
||||
<el-date-picker v-model="linkForm.active_range" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" start-placeholder="开始时间" end-placeholder="结束时间" range-separator="至" />
|
||||
<span class="form-tip">不选择表示长期有效</span>
|
||||
@@ -275,52 +409,85 @@
|
||||
|
||||
<script setup lang="ts" name="firstVisitWecomPromotionPage">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ArrowRight, CircleCheck, CirclePlus, Connection, Delete, DocumentCopy, Edit, Key, Link,
|
||||
Lock, Mouse, OfficeBuilding, Opportunity, Plus, Promotion, Refresh, Select, SetUp,
|
||||
TopRight, View, Warning
|
||||
ArrowDown, ArrowRight, ChatDotRound, CircleCheck, Connection, DataAnalysis, Delete,
|
||||
DocumentCopy, Edit, Key, Link, Lock, Mouse, OfficeBuilding, Opportunity, Plus,
|
||||
Promotion, Refresh, Search, Select, SetUp, TopRight, User, View, Warning
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
wecomPromotionAuthorizationUrl,
|
||||
wecomPromotionCheckApiPermission,
|
||||
wecomPromotionCustomerStats,
|
||||
wecomPromotionDeleteLink,
|
||||
wecomPromotionDeleteRemoteLink,
|
||||
wecomPromotionDeletePool,
|
||||
wecomPromotionOverview,
|
||||
wecomPromotionRemoteLinkDetail,
|
||||
wecomPromotionSaveLink,
|
||||
wecomPromotionSavePool,
|
||||
wecomPromotionToggleLink,
|
||||
wecomPromotionVerifyAccount
|
||||
wecomPromotionSyncCustomers,
|
||||
wecomPromotionSyncRemoteLinks,
|
||||
wecomPromotionToggleLink
|
||||
} from '@/api/first_visit'
|
||||
import type { WecomPromotionCustomerChatStatus } from '@/api/first_visit'
|
||||
|
||||
type TabName = 'links' | 'authorization' | 'install'
|
||||
type TabName = 'links' | 'customer-stats' | 'configuration' | 'install'
|
||||
const emptyOverview = () => ({
|
||||
meta: { scope_label: '', can_authorize: false, generated_at: '' },
|
||||
config: { enabled: false, configured: false, ready: false, missing: [] as string[], suite_id_masked: '', ticket_received_at: 0, provider_callback_url: '', auth_callback_url: '' },
|
||||
summary: { authorized_accounts: 0, pool_count: 0, online_links: 0, today_clicks: 0 },
|
||||
accounts: [] as any[], pools: [] as any[], links: [] as any[], allowed_link_hosts: [] as string[]
|
||||
meta: { scope_label: '', generated_at: '' },
|
||||
config: { mode: 'internal', configured: false, ready: false, missing: [] as string[], corp_id_masked: '', agent_id: '', secret_configured: false, official_doc: '' },
|
||||
summary: { configured_apps: 0, pool_count: 0, online_links: 0, today_clicks: 0 },
|
||||
pools: [] as any[], links: [] as any[], member_options: [] as any[], customer_acquisition_link_example: 'https://work.weixin.qq.com/ca/xxxxxxxx'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const overview = reactive(emptyOverview())
|
||||
const loading = ref(false)
|
||||
const activeTab = ref<TabName>('links')
|
||||
const selectedPoolId = ref<number>()
|
||||
const selectedInstallPoolId = ref<number>()
|
||||
const authorizing = ref(false)
|
||||
const verifyingId = ref(0)
|
||||
const poolDialogVisible = ref(false)
|
||||
const linkDialogVisible = ref(false)
|
||||
const savingPool = ref(false)
|
||||
const savingLink = ref(false)
|
||||
const checkingApi = ref(false)
|
||||
const syncingRemote = ref(false)
|
||||
const refreshingLinkId = ref(0)
|
||||
const poolForm = reactive({ id: 0, name: '', fallback_url: '', status: 1 })
|
||||
const linkForm = reactive({ id: 0, pool_id: 0, account_id: undefined as number | undefined, name: '', group_name: '默认分组', wecom_url: '', weight: 1, status: 1, daily_limit: 0, active_range: [] as string[], remark: '' })
|
||||
const linkForm = reactive({ id: 0, pool_id: 0, name: '', group_name: '默认分组', wecom_url: '', member_admin_ids: [] as number[], skip_verify: 0, legacy: false, weight: 1, status: 1, daily_limit: 0, active_range: [] as string[], remark: '' })
|
||||
const customerStatsLoading = ref(false)
|
||||
const customerStatsLoaded = ref(false)
|
||||
const syncingCustomers = ref(false)
|
||||
const customerFilters = reactive<{
|
||||
link_id: number | string
|
||||
userid: string
|
||||
chat_status: WecomPromotionCustomerChatStatus
|
||||
}>({ link_id: '', userid: '', chat_status: '' })
|
||||
const customerPager = reactive({ page_no: 1, page_size: 20, total: 0 })
|
||||
const customerStats = reactive({
|
||||
summary: { customer_count: 0, messaged_customer_count: 0, message_customer_rate: 0, received_message_count: 0, message_count_known_count: 0 },
|
||||
rows: [] as any[],
|
||||
link_options: [] as Array<{ id: number | string; name: string }>,
|
||||
member_options: [] as Array<{ id: number; userid?: string; name: string; department_name?: string; dept_name?: string }>,
|
||||
meta: { last_synced_at: '' }
|
||||
})
|
||||
|
||||
const selectedPool = computed(() => overview.pools.find((item: any) => Number(item.id) === selectedPoolId.value))
|
||||
const selectedLinks = computed(() => overview.links.filter((item: any) => Number(item.pool_id) === selectedPoolId.value))
|
||||
const activeAccounts = computed(() => overview.accounts.filter((item: any) => Number(item.auth_status) === 1))
|
||||
const selectedOfficialCount = computed(() => selectedLinks.value.filter((item: any) => Boolean(item.remote_link_id)).length)
|
||||
const selectedLegacyCount = computed(() => selectedLinks.value.length - selectedOfficialCount.value)
|
||||
const selectedInstallPool = computed(() => overview.pools.find((item: any) => Number(item.id) === selectedInstallPoolId.value))
|
||||
const customerLinkOptions = computed(() => customerStats.link_options.length
|
||||
? customerStats.link_options
|
||||
: overview.links
|
||||
.filter((item: any) => item.id)
|
||||
.map((item: any) => ({ id: Number(item.id), name: item.name || item.remote_link_id || `链接 ${item.id}` })))
|
||||
const customerMemberOptions = computed(() => customerStats.member_options.length
|
||||
? customerStats.member_options
|
||||
: overview.member_options.map((item: any) => ({
|
||||
id: Number(item.id),
|
||||
userid: String(item.userid || ''),
|
||||
name: item.name,
|
||||
department_name: Array.isArray(item.dept_names) ? item.dept_names.join(' / ') : (item.department_name || item.dept_name || '')
|
||||
})))
|
||||
|
||||
watch(() => overview.pools.map((item: any) => Number(item.id)).join(','), () => {
|
||||
const ids = overview.pools.map((item: any) => Number(item.id))
|
||||
@@ -328,6 +495,10 @@ watch(() => overview.pools.map((item: any) => Number(item.id)).join(','), () =>
|
||||
if (!selectedInstallPoolId.value || !ids.includes(selectedInstallPoolId.value)) selectedInstallPoolId.value = ids[0]
|
||||
}, { immediate: true })
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'customer-stats' && !customerStatsLoaded.value) loadCustomerStats()
|
||||
})
|
||||
|
||||
async function loadOverview() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -366,7 +537,7 @@ async function savePool() {
|
||||
}
|
||||
|
||||
async function removePool(pool: any) {
|
||||
await ElMessageBox.confirm(`删除“${pool.name}”后,该方案下的推广链接也会停用。确认继续?`, '删除分流方案', { type: 'warning' })
|
||||
await ElMessageBox.confirm(`删除“${pool.name}”后,该方案下的获客助手链接也会停用。确认继续?`, '删除分流方案', { type: 'warning' })
|
||||
try {
|
||||
await wecomPromotionDeletePool({ id: Number(pool.id) })
|
||||
ElMessage.success('分流方案已删除')
|
||||
@@ -380,39 +551,216 @@ function openLinkDialog(row?: any) {
|
||||
if (!selectedPool.value) return
|
||||
const range = row && Number(row.active_start) > 0 && Number(row.active_end) > 0
|
||||
? [formatPickerTime(row.active_start), formatPickerTime(row.active_end)] : []
|
||||
const selectedUserIds = new Set<string>((row?.range_userids || []).map((item: unknown) => String(item)))
|
||||
const selectedMemberIds = overview.member_options
|
||||
.filter((member: any) => selectedUserIds.has(String(member.userid)))
|
||||
.map((member: any) => Number(member.id))
|
||||
Object.assign(linkForm, row ? {
|
||||
id: Number(row.id), pool_id: Number(row.pool_id), account_id: Number(row.account_id) || undefined,
|
||||
id: Number(row.id), pool_id: Number(row.pool_id),
|
||||
name: row.name, group_name: row.group_name || '默认分组', wecom_url: row.wecom_url,
|
||||
member_admin_ids: selectedMemberIds, skip_verify: Number(row.skip_verify) === 1 ? 1 : 0, legacy: !row.is_official,
|
||||
weight: Number(row.weight) || 1, status: Number(row.status), daily_limit: Number(row.daily_limit) || 0,
|
||||
active_range: range, remark: row.remark || ''
|
||||
} : {
|
||||
id: 0, pool_id: Number(selectedPool.value.id), account_id: undefined, name: '', group_name: '默认分组',
|
||||
wecom_url: '', weight: 1, status: 1, daily_limit: 0, active_range: [], remark: ''
|
||||
id: 0, pool_id: Number(selectedPool.value.id), name: '', group_name: '默认分组',
|
||||
wecom_url: '', member_admin_ids: [], skip_verify: 0, legacy: false,
|
||||
weight: 1, status: 1, daily_limit: 0, active_range: [], remark: ''
|
||||
})
|
||||
linkDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveLink() {
|
||||
if (!linkForm.name.trim()) return ElMessage.warning('请输入推广成员名称')
|
||||
if (!linkForm.wecom_url.trim()) return ElMessage.warning('请粘贴企业微信推广链接')
|
||||
if (!linkForm.name.trim()) return ElMessage.warning('请输入获客链接名称')
|
||||
if (linkForm.legacy && !linkForm.wecom_url.trim()) return ElMessage.warning('请填写企业微信获客助手链接')
|
||||
if (!linkForm.legacy && !linkForm.member_admin_ids.length) return ElMessage.warning('请至少选择一名获客成员')
|
||||
savingLink.value = true
|
||||
try {
|
||||
await wecomPromotionSaveLink({
|
||||
...linkForm,
|
||||
account_id: linkForm.account_id || 0,
|
||||
active_start: linkForm.active_range?.[0] || '',
|
||||
active_end: linkForm.active_range?.[1] || ''
|
||||
})
|
||||
linkDialogVisible.value = false
|
||||
await loadOverview()
|
||||
ElMessage.success('推广链接已保存')
|
||||
ElMessage.success(linkForm.legacy ? '本地分流规则已保存' : '企业微信官方获客链接已保存')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '推广链接保存失败')
|
||||
ElMessage.error(error?.message || '获客助手链接保存失败')
|
||||
} finally {
|
||||
savingLink.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkApiPermission() {
|
||||
checkingApi.value = true
|
||||
try {
|
||||
const result: any = await wecomPromotionCheckApiPermission()
|
||||
if (result?.has_link) {
|
||||
ElMessage.success(result?.message || 'API 验证通过:应用凭证、可信 IP 与获客助手权限均可用')
|
||||
} else {
|
||||
ElMessage.warning(result?.message || 'API 权限可用,但当前应用还没有创建官方获客链接')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '获客助手 API 验证失败')
|
||||
} finally {
|
||||
checkingApi.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCurrentTab() {
|
||||
await loadOverview()
|
||||
if (activeTab.value === 'customer-stats') await loadCustomerStats()
|
||||
}
|
||||
|
||||
function numberFrom(source: Record<string, any>, keys: string[]) {
|
||||
for (const key of keys) {
|
||||
if (source?.[key] !== undefined && source?.[key] !== null && source?.[key] !== '') {
|
||||
const value = Number(source[key])
|
||||
return Number.isFinite(value) ? value : 0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
async function loadCustomerStats() {
|
||||
customerStatsLoading.value = true
|
||||
try {
|
||||
const result: any = await wecomPromotionCustomerStats({
|
||||
page_no: customerPager.page_no,
|
||||
page_size: customerPager.page_size,
|
||||
promotion_link_id: customerFilters.link_id ? Number(customerFilters.link_id) : undefined,
|
||||
userid: customerFilters.userid || undefined,
|
||||
chat_status: customerFilters.chat_status === ''
|
||||
? undefined
|
||||
: (customerFilters.chat_status === 'messaged' ? 1 : (customerFilters.chat_status === 'unknown' ? 2 : 0))
|
||||
})
|
||||
const summary = result?.summary || result?.stats || {}
|
||||
const customerCount = numberFrom(summary, ['customer_count', 'total_customers', 'customer_total', 'total'])
|
||||
const messagedCount = numberFrom(summary, ['messaged_customer_count', 'started_chat_count', 'chat_customer_count', 'message_customer_count', 'chatting_customers'])
|
||||
const rateKeys = ['message_customer_rate', 'chat_customer_rate', 'messaged_rate', 'chat_rate']
|
||||
const hasRate = rateKeys.some((key) => summary?.[key] !== undefined && summary?.[key] !== null && summary?.[key] !== '')
|
||||
Object.assign(customerStats.summary, {
|
||||
customer_count: customerCount,
|
||||
messaged_customer_count: messagedCount,
|
||||
message_customer_rate: hasRate ? numberFrom(summary, rateKeys) : (customerCount ? messagedCount / customerCount * 100 : 0),
|
||||
received_message_count: numberFrom(summary, ['received_message_count', 'recv_msg_cnt', 'recv_msg_count', 'total_recv_msg_cnt', 'message_count']),
|
||||
message_count_known_count: numberFrom(summary, ['message_count_known_count', 'exact_message_customer_count'])
|
||||
})
|
||||
customerStats.rows = Array.isArray(result?.lists)
|
||||
? result.lists
|
||||
: (Array.isArray(result?.list) ? result.list : (Array.isArray(result?.rows) ? result.rows : []))
|
||||
customerPager.total = Number(result?.total ?? result?.count ?? result?.page?.total ?? customerStats.rows.length) || 0
|
||||
const filters = result?.filters || {}
|
||||
const linkOptions = result?.link_options || filters?.link_options || filters?.links
|
||||
const memberOptions = result?.member_options || filters?.member_options || filters?.members
|
||||
if (Array.isArray(linkOptions)) customerStats.link_options = linkOptions
|
||||
if (Array.isArray(memberOptions)) customerStats.member_options = memberOptions
|
||||
const latestSyncTime = customerStats.rows.reduce((latest: unknown, row: any) => {
|
||||
const current = row.last_synced_at || row.last_sync_time || row.synced_at || row.sync_time || ''
|
||||
return Number(current || 0) > Number(latest || 0) ? current : latest
|
||||
}, '')
|
||||
const syncedAt = result?.meta?.last_synced_at || result?.last_synced_at || latestSyncTime || customerStats.meta.last_synced_at || ''
|
||||
Object.assign(customerStats.meta, result?.meta || {}, {
|
||||
last_synced_at: syncedAt ? formatCustomerTime(syncedAt) : ''
|
||||
})
|
||||
customerStatsLoaded.value = true
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '获客客户统计加载失败')
|
||||
} finally {
|
||||
customerStatsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function applyCustomerFilters() {
|
||||
customerPager.page_no = 1
|
||||
loadCustomerStats()
|
||||
}
|
||||
|
||||
function resetCustomerFilters() {
|
||||
Object.assign(customerFilters, { link_id: '', userid: '', chat_status: '' })
|
||||
customerPager.page_no = 1
|
||||
loadCustomerStats()
|
||||
}
|
||||
|
||||
function handleCustomerPageSizeChange() {
|
||||
customerPager.page_no = 1
|
||||
loadCustomerStats()
|
||||
}
|
||||
|
||||
async function syncCustomers() {
|
||||
syncingCustomers.value = true
|
||||
try {
|
||||
const result: any = await wecomPromotionSyncCustomers({
|
||||
promotion_link_id: customerFilters.link_id ? Number(customerFilters.link_id) : undefined
|
||||
})
|
||||
const scanned = Number(result?.scanned ?? result?.customer_scanned ?? result?.customers?.scanned ?? 0)
|
||||
const created = Number(result?.created ?? result?.customer_created ?? result?.customers?.created ?? 0)
|
||||
const updated = Number(result?.updated ?? result?.customer_updated ?? result?.customers?.updated ?? 0)
|
||||
ElMessage.success(result?.message || `客户同步完成:扫描 ${formatNumber(scanned)} 位,新增 ${formatNumber(created)} 位,更新 ${formatNumber(updated)} 位`)
|
||||
customerPager.page_no = 1
|
||||
await Promise.all([loadOverview(), loadCustomerStats()])
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '获客客户同步失败')
|
||||
} finally {
|
||||
syncingCustomers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function syncRemoteLinks() {
|
||||
if (!selectedPool.value) return ElMessage.warning('请先选择分流方案')
|
||||
syncingRemote.value = true
|
||||
try {
|
||||
const result: any = await wecomPromotionSyncRemoteLinks({ pool_id: Number(selectedPool.value.id) })
|
||||
const message = `扫描 ${Number(result?.scanned || 0)} 条,新建 ${Number(result?.created || 0)} 条,更新 ${Number(result?.updated || 0)} 条,按权限跳过 ${Number(result?.skipped || 0)} 条,失败 ${Number(result?.failed || 0)} 条`
|
||||
if (Number(result?.scanned || 0) === 0) {
|
||||
await ElMessageBox.alert(
|
||||
`${result?.empty_reason || '当前应用没有可同步的官方获客链接'}\n\n${result?.suggestion || ''}`,
|
||||
'没有可同步的官方链接',
|
||||
{ type: 'warning', confirmButtonText: '知道了' }
|
||||
)
|
||||
} else {
|
||||
result?.failed ? ElMessage.warning(message) : ElMessage.success(message)
|
||||
}
|
||||
await loadOverview()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '同步企业微信获客链接失败')
|
||||
} finally {
|
||||
syncingRemote.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRemoteDetail(row: any) {
|
||||
refreshingLinkId.value = Number(row.id)
|
||||
try {
|
||||
await wecomPromotionRemoteLinkDetail({ id: Number(row.id) })
|
||||
ElMessage.success('企业微信官方详情已刷新')
|
||||
await loadOverview()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '刷新官方详情失败')
|
||||
} finally {
|
||||
refreshingLinkId.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLinkCommand(command: string, row: any) {
|
||||
if (command === 'local') {
|
||||
await removeLink(row)
|
||||
return
|
||||
}
|
||||
if (command !== 'remote') return
|
||||
await ElMessageBox.confirm(
|
||||
`将永久删除企业微信后台中的“${row.name}”,现有推广页面会立即失效,且无法恢复。确认继续?`,
|
||||
'永久删除企业微信获客链接',
|
||||
{ type: 'error', confirmButtonText: '永久删除', cancelButtonText: '取消' }
|
||||
)
|
||||
try {
|
||||
await wecomPromotionDeleteRemoteLink({ id: Number(row.id) })
|
||||
ElMessage.success('企业微信获客链接已永久删除,本地记录已保留并停止分流')
|
||||
await loadOverview()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '永久删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggle(row: any, value: unknown) {
|
||||
try {
|
||||
await wecomPromotionToggleLink({ id: Number(row.id), status: value ? 1 : 0 })
|
||||
@@ -426,45 +774,21 @@ async function handleToggle(row: any, value: unknown) {
|
||||
}
|
||||
|
||||
async function removeLink(row: any) {
|
||||
await ElMessageBox.confirm(`确认删除推广链接“${row.name}”?`, '删除推广链接', { type: 'warning' })
|
||||
await ElMessageBox.confirm(`仅从本地分流池移除“${row.name}”,不会删除企业微信后台的官方链接。确认继续?`, '从本地移除', { type: 'warning' })
|
||||
try {
|
||||
await wecomPromotionDeleteLink({ id: Number(row.id) })
|
||||
ElMessage.success('推广链接已删除')
|
||||
ElMessage.success('已从本地分流池移除')
|
||||
await loadOverview()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function startAuthorization() {
|
||||
authorizing.value = true
|
||||
try {
|
||||
const result: any = await wecomPromotionAuthorizationUrl()
|
||||
if (!result?.url) throw new Error('未获取到企业微信授权地址')
|
||||
window.location.assign(result.url)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '授权地址生成失败')
|
||||
authorizing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyAccount(row: any) {
|
||||
verifyingId.value = Number(row.id)
|
||||
try {
|
||||
await wecomPromotionVerifyAccount({ id: Number(row.id) })
|
||||
ElMessage.success('授权凭证有效')
|
||||
await loadOverview()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '凭证验证失败')
|
||||
} finally {
|
||||
verifyingId.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
function eligibility(row: any) {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
if (Number(row.remote_status) === 2) return { label: '企业微信已删除', className: 'is-error' }
|
||||
if (row.valid_customer_acquisition_link === false) return { label: '非获客助手链接', className: 'is-error' }
|
||||
if (Number(row.status) !== 1) return { label: '已下线', className: 'is-muted' }
|
||||
if (Number(row.account_id) > 0 && Number(row.auth_status) !== 1) return { label: '授权失效', className: 'is-error' }
|
||||
if (Number(row.active_start) > 0 && Number(row.active_start) > now) return { label: '尚未生效', className: 'is-waiting' }
|
||||
if (Number(row.active_end) > 0 && Number(row.active_end) < now) return { label: '已过期', className: 'is-error' }
|
||||
if (Number(row.daily_limit) > 0 && todayCount(row) >= Number(row.daily_limit)) return { label: '今日已达上限', className: 'is-waiting' }
|
||||
@@ -476,14 +800,111 @@ function todayCount(row: any) {
|
||||
return row.today_date === today ? Number(row.today_count || 0) : 0
|
||||
}
|
||||
|
||||
function formatNumber(value: unknown) {
|
||||
return Number(value || 0).toLocaleString('zh-CN')
|
||||
function memberOptionLabel(member: any) {
|
||||
const departments = Array.isArray(member.dept_names) && member.dept_names.length
|
||||
? member.dept_names.join(' / ')
|
||||
: '未分部门'
|
||||
return `${member.name} · ${departments} · ${member.userid}`
|
||||
}
|
||||
|
||||
function formatTime(value: unknown) {
|
||||
const timestamp = Number(value || 0)
|
||||
if (!timestamp) return '—'
|
||||
return new Date(timestamp * 1000).toLocaleString('zh-CN', { hour12: false })
|
||||
function remoteMemberLabel(row: any) {
|
||||
const userIds = Array.isArray(row.range_userids) ? row.range_userids : []
|
||||
const names = userIds.map((userid: unknown) => {
|
||||
const member = overview.member_options.find((item: any) => String(item.userid) === String(userid))
|
||||
return member?.name || String(userid)
|
||||
})
|
||||
if (names.length) return names.length > 2 ? `${names.slice(0, 2).join('、')} 等 ${names.length} 人` : names.join('、')
|
||||
const departments = Array.isArray(row.range_department_ids) ? row.range_department_ids : []
|
||||
if (departments.length) return `企微部门 ${departments.join('、')}`
|
||||
return row.is_official ? '未返回成员范围' : '手工配置'
|
||||
}
|
||||
|
||||
function memberFilterLabel(member: any) {
|
||||
const department = member.department_name || member.dept_name || ''
|
||||
return department ? `${member.name} · ${department}` : member.name
|
||||
}
|
||||
|
||||
function customerIdentifier(row: any) {
|
||||
const masked = row.external_userid_masked || row.customer_id_masked || row.customer_masked || row.customer_name_masked
|
||||
if (masked) return String(masked)
|
||||
return maskIdentifier(row.external_userid || row.customer_id || row.id)
|
||||
}
|
||||
|
||||
function maskIdentifier(value: unknown) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) return '-'
|
||||
if (text.includes('*')) return text
|
||||
if (text.length <= 4) return `${text.slice(0, 1)}***`
|
||||
if (text.length <= 8) return `${text.slice(0, 2)}***${text.slice(-1)}`
|
||||
return `${text.slice(0, 4)}****${text.slice(-4)}`
|
||||
}
|
||||
|
||||
function customerLinkName(row: any) {
|
||||
return row.link_name || row.source_link_name || row.promotion_link_name || '未匹配来源链接'
|
||||
}
|
||||
|
||||
function customerLinkId(row: any) {
|
||||
const linkId = row.link_id || row.remote_link_id || row.source_link_id
|
||||
return linkId ? `链接 ID:${linkId}` : '暂无链接 ID'
|
||||
}
|
||||
|
||||
function customerMessageCount(row: any) {
|
||||
return numberFrom(row, ['received_message_count', 'recv_msg_cnt', 'message_count', 'received_count'])
|
||||
}
|
||||
|
||||
function customerMessageCountKnown(row: any) {
|
||||
const explicit = row.message_count_known ?? row.message_count_exact ?? row.count_known
|
||||
if (explicit !== undefined && explicit !== null) {
|
||||
return explicit === true || explicit === 1 || explicit === '1' || explicit === 'true'
|
||||
}
|
||||
return customerMessageCount(row) > 0
|
||||
}
|
||||
|
||||
function customerChatState(row: any): 'messaged' | 'silent' | 'unknown' {
|
||||
const status = Number(row.chat_status)
|
||||
if (status === 2) return 'unknown'
|
||||
const explicit = row.has_messaged ?? row.has_message ?? row.messaged
|
||||
if (explicit !== undefined && explicit !== null) {
|
||||
return explicit === true || explicit === 1 || explicit === '1' || explicit === 'true' || explicit === 'messaged'
|
||||
? 'messaged'
|
||||
: 'silent'
|
||||
}
|
||||
return status === 1 || customerMessageCount(row) > 0 ? 'messaged' : 'silent'
|
||||
}
|
||||
|
||||
function customerChatStateLabel(row: any) {
|
||||
const state = customerChatState(row)
|
||||
return state === 'messaged' ? '已发消息' : (state === 'unknown' ? '状态未知' : '未发消息')
|
||||
}
|
||||
|
||||
function customerMessageCountLabel(row: any) {
|
||||
if (customerMessageCountKnown(row)) return formatNumber(customerMessageCount(row))
|
||||
const state = customerChatState(row)
|
||||
if (state === 'silent') return '0'
|
||||
return state === 'unknown' ? '未知' : '待回调'
|
||||
}
|
||||
|
||||
function customerLastMessageTime(row: any) {
|
||||
return row.last_message_at || row.last_chat_time || row.last_message_time || row.last_msg_time || row.event_time || ''
|
||||
}
|
||||
|
||||
function formatCustomerTime(value: unknown) {
|
||||
if (value === undefined || value === null || value === '' || value === 0 || value === '0') return '-'
|
||||
if (typeof value === 'number' || /^\d+$/.test(String(value))) {
|
||||
const timestamp = Number(value)
|
||||
const date = new Date(timestamp > 100000000000 ? timestamp : timestamp * 1000)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
return date.toLocaleString('zh-CN', { hour12: false }).replaceAll('/', '-')
|
||||
}
|
||||
return String(value).replace('T', ' ').replace(/Z$/, '').slice(0, 19)
|
||||
}
|
||||
|
||||
function formatCustomerRate(value: unknown) {
|
||||
return `${Number(value || 0).toFixed(1)}%`
|
||||
}
|
||||
|
||||
function formatNumber(value: unknown) {
|
||||
return Number(value || 0).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
function formatPickerTime(value: unknown) {
|
||||
@@ -513,22 +934,8 @@ function openTestLink() {
|
||||
if (selectedInstallPool.value?.go_url) window.open(selectedInstallPool.value.go_url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
async function handleAuthorizationResult() {
|
||||
const status = String(route.query.wecom_auth || '')
|
||||
if (!status) return
|
||||
activeTab.value = 'authorization'
|
||||
if (status === 'success') ElMessage.success('企业微信授权成功,凭证已安全保存')
|
||||
else ElMessage.error(String(route.query.message || '企业微信授权失败,请重新发起'))
|
||||
const query = { ...route.query }
|
||||
delete query.wecom_auth
|
||||
delete query.account_id
|
||||
delete query.message
|
||||
await router.replace({ query })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadOverview()
|
||||
await handleAuthorizationResult()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -545,7 +952,7 @@ onMounted(async () => {
|
||||
background: var(--canvas);
|
||||
}
|
||||
h1, h2, h3, p { margin: 0; }
|
||||
.page-header, .metric-card, .workspace-card { border: 1px solid var(--line); background: #fff; }
|
||||
.page-header, .metric-card, .customer-metric-card, .workspace-card { border: 1px solid var(--line); background: #fff; }
|
||||
.page-header { display: flex; align-items: center; justify-content: space-between; gap: 20px; min-height: 78px; padding: 14px 18px; border-radius: 13px; }
|
||||
.heading-copy, .heading-actions, .pool-title-row, .toolbar-actions { display: flex; align-items: center; }
|
||||
.heading-copy { gap: 12px; }
|
||||
@@ -576,14 +983,25 @@ h1, h2, h3, p { margin: 0; }
|
||||
.pool-title-row { gap: 8px; }.pool-title-row h3 { font-size: 15px; }.pool-toolbar p { margin-top: 6px; color: #8b97a6; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 10px; }.toolbar-actions { flex-wrap: wrap; justify-content: flex-end; gap: 7px; }.toolbar-actions .el-button + .el-button { margin-left: 0; }
|
||||
.status-tag, .owner-tag, .availability { display: inline-flex; align-items: center; min-height: 22px; padding: 0 8px; border-radius: 11px; font-size: 10px; }.status-tag.is-online, .availability.is-ok { color: #16895f; background: #eaf8ef; }.status-tag.is-offline, .availability.is-muted { color: #788696; background: #eef2f5; }.owner-tag { color: #50728c; background: #edf4f8; }.availability.is-error { color: #d94b4b; background: #ffeded; }.availability.is-waiting { color: #b86c1f; background: #fff1dd; }
|
||||
.link-table, .account-table { --el-table-header-bg-color: #f7f9fb; }.link-table :deep(th.el-table__cell), .account-table :deep(th.el-table__cell) { color: #67768a; font-weight: 500; }.member-cell strong, .member-cell small { display: block; }.member-cell small { margin-top: 3px; color: #8d98a7; font-size: 10px; }.muted { color: #9aa4b0; }
|
||||
.remote-id { display: block; overflow: hidden; color: #68778b; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }.legacy-tag { display: inline-flex; padding: 3px 7px; border-radius: 5px; color: #9a6b22; background: #fff5df; font-size: 10px; }.danger-command { color: #d94b4b; }
|
||||
.customer-heading-actions { display: flex; align-items: center; gap: 12px; }
|
||||
.customer-metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
|
||||
.customer-metric-card { display: flex; align-items: center; gap: 12px; min-width: 0; min-height: 94px; padding: 14px; border-radius: 10px; }
|
||||
.customer-metric-card > div { min-width: 0; }.customer-metric-card small { color: #78869a; font-size: 11px; }.customer-metric-card strong { display: block; margin: 3px 0; font-size: 23px; font-variant-numeric: tabular-nums; line-height: 1.15; }.customer-metric-card p { overflow: hidden; color: #98a2af; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.chatkey-alert { margin-top: 14px; }.chatkey-alert :deep(.el-alert__title) { font-size: 12px; }.chatkey-alert :deep(.el-alert__description) { line-height: 1.6; }
|
||||
.customer-filter-bar { margin-top: 14px; padding: 12px 14px 0; border: 1px solid var(--line); border-radius: 9px; background: #f8fafb; }
|
||||
.customer-filter-bar :deep(.el-form-item) { margin-right: 12px; margin-bottom: 12px; }.customer-filter-bar :deep(.el-form-item__label) { height: 22px; padding: 0; color: #67768a; font-size: 11px; line-height: 22px; }.customer-filter-bar .el-select { width: 220px; }.customer-filter-bar .filter-actions { align-self: flex-end; margin-right: 0; }
|
||||
.customer-table { margin-top: 14px; --el-table-header-bg-color: #f7f9fb; }.customer-table :deep(th.el-table__cell) { color: #67768a; font-weight: 500; }.customer-identifier { color: #53657a; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; }.message-state { display: inline-flex; align-items: center; min-height: 24px; padding: 0 9px; border-radius: 12px; font-size: 10px; }.message-state.is-messaged { color: #16895f; background: #eaf8ef; }.message-state.is-silent { color: #788696; background: #eef2f5; }.message-state.is-unknown { color: #a66a1f; background: #fff3df; }.message-count { color: #253348; font-variant-numeric: tabular-nums; }
|
||||
.customer-pagination { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 14px; color: #8491a2; font-size: 11px; }
|
||||
.configuration-panel { margin-bottom: 16px; padding: 16px; border: 1px solid; border-radius: 10px; }.configuration-panel.is-ready { border-color: #cce8e3; background: #f5fbfa; }.configuration-panel.is-pending { border-color: #f0d8b6; background: #fffaf2; }
|
||||
.configuration-state { display: flex; align-items: center; gap: 12px; }.configuration-state > span { display: grid; width: 36px; height: 36px; place-items: center; border-radius: 9px; color: #fff; background: var(--teal); font-size: 18px; }.is-pending .configuration-state > span { background: #e69a43; }.configuration-state strong { font-size: 14px; }.configuration-state p { margin-top: 4px; color: #718094; font-size: 11px; }
|
||||
.missing-fields { margin-top: 12px; color: #9c651f; font-size: 11px; }.missing-fields code { margin-left: 6px; padding: 3px 6px; border-radius: 4px; background: rgba(224,153,63,.11); }
|
||||
.callback-list { margin: 14px 0 0; border-top: 1px solid rgba(124,150,157,.16); }.callback-list > div { display: grid; grid-template-columns: 120px 1fr 50px; align-items: center; min-height: 40px; border-bottom: 1px solid rgba(124,150,157,.12); font-size: 11px; }.callback-list dt { color: #657488; }.callback-list dd { overflow: hidden; margin: 0; color: #27374c; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; text-overflow: ellipsis; white-space: nowrap; }.callback-list button { border: 0; color: #148f83; background: transparent; cursor: pointer; }
|
||||
.configuration-actions { display: flex; align-items: center; gap: 14px; margin-top: 16px; }
|
||||
.install-pool-select { width: 220px; }.install-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 14px; }.code-card { min-width: 0; padding: 16px; border: 1px solid var(--line); border-radius: 10px; background: #fbfcfd; }.code-heading { display: flex; align-items: center; gap: 10px; }.code-heading > span { display: grid; width: 30px; height: 30px; flex: 0 0 30px; place-items: center; border-radius: 8px; color: #fff; background: var(--teal); font-weight: 700; }.code-heading strong { font-size: 13px; }.code-heading p { margin-top: 3px; color: #7a889a; font-size: 10px; }.code-card pre { min-height: 92px; margin: 14px 0; padding: 13px; overflow: auto; border-radius: 7px; color: #cbe9e6; background: #172a36; white-space: pre-wrap; word-break: break-all; }.code-card pre code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; line-height: 1.7; }
|
||||
.rule-panel { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 10px; margin-top: 14px; }.rule-panel > div { display: flex; align-items: flex-start; gap: 9px; min-height: 68px; padding: 12px; border-radius: 9px; color: #69788b; background: #f3f7f8; font-size: 11px; line-height: 1.65; }.rule-panel .el-icon { margin-top: 2px; color: var(--teal); font-size: 17px; }.rule-panel strong { display: block; color: #26364a; }
|
||||
.test-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 14px; padding: 14px 16px; border: 1px dashed #b8d9d4; border-radius: 9px; background: #f7fcfb; }.test-row strong { font-size: 13px; }.test-row p { margin-top: 4px; color: #7c8999; font-size: 10px; }
|
||||
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 14px; }.form-tip { display: block; margin-top: 5px; color: #8b97a6; font-size: 10px; }.link-form .el-select, .link-form .el-input-number { width: 100%; }
|
||||
@media (max-width: 1100px) { .heading-actions { flex-wrap: wrap; justify-content: flex-end; }.metric-grid { grid-template-columns: repeat(2, minmax(0,1fr)); }.pool-layout { grid-template-columns: 210px minmax(0,1fr); }.pool-toolbar { align-items: flex-start; flex-direction: column; }.rule-panel { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 760px) { .promotion-page { padding: 10px; }.page-header, .section-heading { align-items: flex-start; flex-direction: column; }.update-time { display: none; }.metric-grid, .install-grid, .form-grid { grid-template-columns: 1fr; }.tab-nav { overflow-x: auto; }.tab-nav button { min-width: 112px; }.tab-content { padding: 14px; }.pool-layout { grid-template-columns: 1fr; }.pool-sidebar { display: flex; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--line); }.pool-item { min-width: 190px; }.callback-list > div { grid-template-columns: 1fr 48px; padding: 8px 0; }.callback-list dt { grid-column: 1 / -1; }.install-pool-select { width: 100%; } }
|
||||
@media (max-width: 1100px) { .heading-actions { flex-wrap: wrap; justify-content: flex-end; }.metric-grid, .customer-metric-grid { grid-template-columns: repeat(2, minmax(0,1fr)); }.pool-layout { grid-template-columns: 210px minmax(0,1fr); }.pool-toolbar { align-items: flex-start; flex-direction: column; }.rule-panel { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 760px) { .promotion-page { padding: 10px; }.page-header, .section-heading { align-items: flex-start; flex-direction: column; }.update-time { display: none; }.metric-grid, .customer-metric-grid, .install-grid, .form-grid { grid-template-columns: 1fr; }.tab-nav { overflow-x: auto; }.tab-nav button { min-width: 112px; }.tab-content { padding: 14px; }.pool-layout { grid-template-columns: 1fr; }.pool-sidebar { display: flex; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--line); }.pool-item { min-width: 190px; }.callback-list > div { grid-template-columns: 1fr 48px; padding: 8px 0; }.callback-list dt { grid-column: 1 / -1; }.install-pool-select { width: 100%; }.customer-heading-actions { width: 100%; justify-content: flex-end; }.customer-filter-bar :deep(.el-form-item) { width: 100%; margin-right: 0; }.customer-filter-bar :deep(.el-form-item__content), .customer-filter-bar .el-select { width: 100%; }.customer-filter-bar .filter-actions :deep(.el-form-item__content) { justify-content: flex-end; }.customer-pagination { align-items: flex-start; flex-direction: column; }.customer-pagination :deep(.el-pagination) { max-width: 100%; flex-wrap: wrap; justify-content: flex-start; } }
|
||||
</style>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ namespace app\adminapi\controller\firstvisit;
|
||||
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\firstvisit\WecomAcquisitionCustomerLogic;
|
||||
use app\adminapi\logic\firstvisit\WecomPromotionLogic;
|
||||
|
||||
class WecomPromotionController extends BaseAdminController
|
||||
@@ -15,7 +16,7 @@ class WecomPromotionController extends BaseAdminController
|
||||
public function overview()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
return $this->fail('权限不足,无法访问企业微信推广助手');
|
||||
return $this->fail('权限不足,无法访问企业微信获客助手');
|
||||
}
|
||||
|
||||
return $this->run(fn () => $this->data(WecomPromotionLogic::overview(
|
||||
@@ -25,33 +26,6 @@ class WecomPromotionController extends BaseAdminController
|
||||
)));
|
||||
}
|
||||
|
||||
public function authorizationUrl()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
|
||||
return $this->run(fn () => $this->data(WecomPromotionLogic::authorizationUrl(
|
||||
$this->adminId,
|
||||
$this->adminInfo,
|
||||
$this->request->domain()
|
||||
)));
|
||||
}
|
||||
|
||||
public function verifyAccount()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
$id = (int) $this->request->post('id', 0);
|
||||
|
||||
return $this->run(fn () => $this->success('凭证验证成功', WecomPromotionLogic::verifyAccount(
|
||||
$id,
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
)));
|
||||
}
|
||||
|
||||
public function savePool()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
@@ -85,13 +59,90 @@ class WecomPromotionController extends BaseAdminController
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
|
||||
return $this->run(fn () => $this->success('推广链接已保存', WecomPromotionLogic::saveLink(
|
||||
return $this->run(fn () => $this->success('获客助手链接已保存', WecomPromotionLogic::saveLink(
|
||||
$this->request->post(),
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
)));
|
||||
}
|
||||
|
||||
public function checkApiPermission()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
|
||||
return $this->run(fn () => $this->success('获客助手 API 权限验证通过', WecomPromotionLogic::checkApiPermission()));
|
||||
}
|
||||
|
||||
public function syncRemoteLinks()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
$poolId = (int) $this->request->post('pool_id', 0);
|
||||
|
||||
return $this->run(fn () => $this->success('企业微信获客链接同步完成', WecomPromotionLogic::syncRemoteLinks(
|
||||
$poolId,
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
)));
|
||||
}
|
||||
|
||||
public function remoteLinkDetail()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
$id = (int) $this->request->get('id', 0);
|
||||
|
||||
return $this->run(fn () => $this->data(WecomPromotionLogic::remoteLinkDetail(
|
||||
$id,
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
)));
|
||||
}
|
||||
|
||||
public function deleteRemoteLink()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
$id = (int) $this->request->post('id', 0);
|
||||
|
||||
return $this->run(function () use ($id) {
|
||||
WecomPromotionLogic::deleteRemoteLink($id, $this->adminId, $this->adminInfo);
|
||||
|
||||
return $this->success('企业微信获客链接已永久删除,本地审计记录已保留');
|
||||
});
|
||||
}
|
||||
|
||||
public function syncCustomers()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
|
||||
return $this->run(fn () => $this->success('获客客户同步完成', WecomAcquisitionCustomerLogic::sync(
|
||||
$this->request->post(),
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
)));
|
||||
}
|
||||
|
||||
public function customerStatistics()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
|
||||
return $this->run(fn () => $this->data(WecomAcquisitionCustomerLogic::statistics(
|
||||
$this->request->get(),
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
)));
|
||||
}
|
||||
|
||||
public function toggleLink()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
@@ -117,7 +168,7 @@ class WecomPromotionController extends BaseAdminController
|
||||
return $this->run(function () use ($id) {
|
||||
WecomPromotionLogic::deleteLink($id, $this->adminId, $this->adminInfo);
|
||||
|
||||
return $this->success('推广链接已删除');
|
||||
return $this->success('获客助手链接已删除');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace app\adminapi\lists\firstvisit;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\adminapi\logic\firstvisit\MyPatientLogic;
|
||||
use app\adminapi\logic\stats\YejiStatsLogic;
|
||||
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
|
||||
use app\common\lists\ListsExtendInterface;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
@@ -61,11 +62,13 @@ class MyPatientOrderLists extends BaseAdminDataLists implements ListsSearchInter
|
||||
{
|
||||
$query = $this->buildQuery();
|
||||
$pendingQuery = clone $query;
|
||||
$effectiveAmountQuery = clone $query;
|
||||
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($effectiveAmountQuery, 'po');
|
||||
|
||||
return [
|
||||
'summary' => [
|
||||
'orders' => (int) (clone $query)->count('po.id'),
|
||||
'amount' => round((float) (clone $query)->sum('po.amount'), 2),
|
||||
'amount' => round((float) $effectiveAmountQuery->sum('po.amount'), 2),
|
||||
'pending' => (int) $pendingQuery
|
||||
->where(function ($q) {
|
||||
$q->where('po.prescription_audit_status', 0)
|
||||
@@ -294,6 +297,18 @@ class MyPatientOrderLists extends BaseAdminDataLists implements ListsSearchInter
|
||||
$row['prescription_audit_text'] = $this->auditStatusText((int) ($row['prescription_audit_status'] ?? 0));
|
||||
$row['payment_slip_audit_text'] = $this->auditStatusText((int) ($row['payment_slip_audit_status'] ?? 0));
|
||||
$row['fulfillment_text'] = $this->fulfillmentStatusText((int) ($row['fulfillment_status'] ?? 0));
|
||||
$fulfillmentStatus = (int) ($row['fulfillment_status'] ?? 0);
|
||||
$refundAmount = round((float) ($row['refund_amount'] ?? 0), 2);
|
||||
$amountIncluded = !in_array(
|
||||
$fulfillmentStatus,
|
||||
YejiStatsLogic::PRESCRIPTION_ORDER_FULFILLMENT_EXCLUDED_FROM_PERFORMANCE,
|
||||
true
|
||||
) && $refundAmount <= 0;
|
||||
$row['amount_included'] = $amountIncluded;
|
||||
$row['effective_amount'] = $amountIncluded ? round((float) ($row['amount'] ?? 0), 2) : 0.0;
|
||||
$row['amount_exclusion_text'] = $amountIncluded
|
||||
? ''
|
||||
: ($refundAmount > 0 || $fulfillmentStatus === 10 ? '退款不计入' : $row['fulfillment_text'] . '不计入');
|
||||
}
|
||||
unset($row);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace app\adminapi\logic\firstvisit;
|
||||
|
||||
use app\adminapi\logic\dept\DeptLogic;
|
||||
use app\adminapi\logic\stats\ConversionLogic;
|
||||
use app\adminapi\logic\stats\YejiStatsLogic;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminDept;
|
||||
use app\common\model\stats\PersonalYeji;
|
||||
@@ -156,10 +157,15 @@ class FirstVisitConversionLogic
|
||||
private static function resolveTimeRange(string $timeType): array
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
$timeType = in_array($timeType, ['today', 'week', 'month', 'quarter', 'year'], true)
|
||||
$timeType = in_array($timeType, ['today', 'yesterday', 'week', 'month', 'quarter', 'year'], true)
|
||||
? $timeType
|
||||
: 'today';
|
||||
|
||||
if ($timeType === 'yesterday') {
|
||||
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
||||
|
||||
return [$yesterday, $yesterday, $timeType, '昨天'];
|
||||
}
|
||||
if ($timeType === 'week') {
|
||||
return [date('Y-m-d', strtotime('monday this week')), $today, $timeType, '本周'];
|
||||
}
|
||||
@@ -451,6 +457,7 @@ class FirstVisitConversionLogic
|
||||
strtotime($year . '-01-01 00:00:00'),
|
||||
strtotime($year . '-12-31 23:59:59'),
|
||||
]);
|
||||
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($actualQuery, 'po');
|
||||
if ($effectiveAdminIds !== null) {
|
||||
$actualQuery->whereIn('rx.assistant_id', $effectiveAdminIds);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ class FirstVisitDoctorDashboardLogic
|
||||
'selected_doctor_name' => $selectedDoctorName,
|
||||
'doctor_count' => count($rows),
|
||||
'appointment_rule' => '总挂号包含已预约、已取消、已完成和已过号;面诊取状态为已完成的挂号',
|
||||
'performance_rule' => '诊单按订单创建时间统计,排除履约已取消、拒收和退款,金额归属处方开方医生',
|
||||
'performance_rule' => '诊单按订单创建时间统计,排除已取消、拒收、全额退款及部分退款,金额归属处方开方医生',
|
||||
],
|
||||
'filters' => [
|
||||
'departments' => $doctorSelf ? [] : DeptLogic::getAllDataScoped($adminId, $adminInfo),
|
||||
@@ -396,7 +396,7 @@ class FirstVisitDoctorDashboardLogic
|
||||
strtotime($startDate . ' 00:00:00'),
|
||||
strtotime($endDate . ' 23:59:59'),
|
||||
]);
|
||||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, 'o');
|
||||
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($query, 'o');
|
||||
if ($assistantIds !== null) {
|
||||
$query->whereIn('o.creator_id', $assistantIds);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\firstvisit;
|
||||
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use app\common\service\qywx\QywxCustomerAcquisitionCustomerService;
|
||||
use RuntimeException;
|
||||
use think\facade\Db;
|
||||
|
||||
/** 获客客户同步与数据权限统计。 */
|
||||
class WecomAcquisitionCustomerLogic
|
||||
{
|
||||
/** @return array<string,mixed> */
|
||||
public static function sync(array $params, int $adminId, array $adminInfo): array
|
||||
{
|
||||
$localLinkId = max(0, (int) ($params['promotion_link_id'] ?? $params['id'] ?? 0));
|
||||
$query = Db::name('qywx_promotion_link')->alias('l')
|
||||
->whereNull('l.delete_time')
|
||||
->where('l.remote_link_id', '<>', '')
|
||||
->where('l.remote_status', 1);
|
||||
self::applyScope($query, 'l', DataScopeService::getVisibleAdminIds($adminId, $adminInfo));
|
||||
if ($localLinkId > 0) {
|
||||
$query->where('l.id', $localLinkId);
|
||||
}
|
||||
$links = $query->field('l.id,l.remote_link_id')->order('l.id', 'asc')->limit(200)->select()->toArray();
|
||||
if ($localLinkId > 0 && $links === []) {
|
||||
throw new RuntimeException('获客链接不存在、已失效,或超出当前权限范围');
|
||||
}
|
||||
if ($links === []) {
|
||||
throw new RuntimeException('当前数据范围内没有可同步的有效官方获客链接;已删除和历史手工链接不会参与客户同步,请先创建官方获客链接');
|
||||
}
|
||||
$service = new QywxCustomerAcquisitionCustomerService();
|
||||
$result = ['links' => count($links), 'scanned' => 0, 'created' => 0, 'updated' => 0, 'failed' => 0, 'errors' => []];
|
||||
foreach ($links as $link) {
|
||||
try {
|
||||
$one = $service->syncLink((string) $link['remote_link_id']);
|
||||
$result['scanned'] += $one['scanned'];
|
||||
$result['created'] += $one['created'];
|
||||
$result['updated'] += $one['updated'];
|
||||
} catch (\Throwable $e) {
|
||||
$result['failed']++;
|
||||
if (count($result['errors']) < 10) {
|
||||
$result['errors'][] = (string) $link['remote_link_id'] . ':' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public static function statistics(array $params, int $adminId, array $adminInfo): array
|
||||
{
|
||||
$page = max(1, (int) ($params['page_no'] ?? $params['page'] ?? 1));
|
||||
$pageSize = min(100, max(1, (int) ($params['page_size'] ?? 20)));
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
$base = self::customerQuery($params, $visibleIds);
|
||||
$total = (int) (clone $base)->count();
|
||||
$rows = $base
|
||||
->field('c.id,c.promotion_link_id,c.link_id,c.external_userid,c.userid,c.owner_admin_id,c.dept_id,c.state,c.chat_status,c.recv_msg_cnt,c.message_count_known,c.first_acquired_time,c.last_chat_time,c.last_sync_time,c.create_time,c.update_time,a.name as owner_name,d.name as dept_name,l.name as link_name,p.name as pool_name')
|
||||
->order('c.last_chat_time', 'desc')->order('c.id', 'desc')
|
||||
->page($page, $pageSize)->select()->toArray();
|
||||
foreach ($rows as &$row) {
|
||||
$row['external_userid_masked'] = self::maskIdentifier((string) ($row['external_userid'] ?? ''));
|
||||
unset($row['external_userid']);
|
||||
$row['has_messaged'] = (int) ($row['chat_status'] ?? 0) === 1;
|
||||
$row['message_count_known'] = (int) ($row['message_count_known'] ?? 0);
|
||||
$row['received_message_count'] = (int) ($row['recv_msg_cnt'] ?? 0);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$summaryQuery = self::customerQuery($params, $visibleIds);
|
||||
$summaryRow = $summaryQuery->fieldRaw(
|
||||
'COUNT(*) AS customer_count, '
|
||||
. 'COALESCE(SUM(CASE WHEN c.message_count_known = 1 THEN c.recv_msg_cnt ELSE 0 END),0) AS recv_msg_cnt, '
|
||||
. 'SUM(CASE WHEN c.chat_status = 1 THEN 1 ELSE 0 END) AS started_chat_count, '
|
||||
. 'SUM(CASE WHEN c.message_count_known = 1 THEN 1 ELSE 0 END) AS message_count_known_count'
|
||||
)->find() ?: [];
|
||||
|
||||
return [
|
||||
'meta' => [
|
||||
'scope_label' => DataScopeService::scopeLabel(DataScopeService::getEffectiveScope($adminInfo)),
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
'summary' => [
|
||||
'customer_count' => (int) ($summaryRow['customer_count'] ?? 0),
|
||||
'started_chat_count' => (int) ($summaryRow['started_chat_count'] ?? 0),
|
||||
'recv_msg_cnt' => (int) ($summaryRow['recv_msg_cnt'] ?? 0),
|
||||
'received_message_count' => (int) ($summaryRow['recv_msg_cnt'] ?? 0),
|
||||
'message_count_known_count' => (int) ($summaryRow['message_count_known_count'] ?? 0),
|
||||
],
|
||||
'lists' => $rows,
|
||||
'count' => $total,
|
||||
'page_no' => $page,
|
||||
'page_size' => $pageSize,
|
||||
];
|
||||
}
|
||||
|
||||
private static function customerQuery(array $params, ?array $visibleIds)
|
||||
{
|
||||
$query = Db::name('qywx_customer_acquisition_customer')->alias('c')
|
||||
->leftJoin('admin a', 'a.id = c.owner_admin_id AND a.delete_time IS NULL')
|
||||
->leftJoin('dept d', 'd.id = c.dept_id')
|
||||
->leftJoin('qywx_promotion_link l', 'l.id = c.promotion_link_id')
|
||||
->leftJoin('qywx_promotion_pool p', 'p.id = l.pool_id');
|
||||
self::applyScope($query, 'c', $visibleIds);
|
||||
$localLinkId = max(0, (int) ($params['promotion_link_id'] ?? 0));
|
||||
if ($localLinkId > 0) {
|
||||
$query->where('c.promotion_link_id', $localLinkId);
|
||||
}
|
||||
$userId = trim((string) ($params['userid'] ?? ''));
|
||||
if ($userId !== '') {
|
||||
$query->where('c.userid', $userId);
|
||||
}
|
||||
if (isset($params['chat_status']) && $params['chat_status'] !== '') {
|
||||
$query->where('c.chat_status', max(0, (int) $params['chat_status']));
|
||||
}
|
||||
$keyword = trim((string) ($params['keyword'] ?? ''));
|
||||
if ($keyword !== '') {
|
||||
$query->whereLike('c.external_userid|c.userid|a.name|l.name', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private static function applyScope($query, string $alias, ?array $visibleIds): void
|
||||
{
|
||||
if ($visibleIds === null) {
|
||||
return;
|
||||
}
|
||||
if ($visibleIds === []) {
|
||||
$query->whereRaw('1 = 0');
|
||||
return;
|
||||
}
|
||||
$query->whereIn($alias . '.owner_admin_id', array_values(array_unique(array_map('intval', $visibleIds))));
|
||||
}
|
||||
|
||||
private static function maskIdentifier(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
$length = mb_strlen($value);
|
||||
if ($length <= 0) {
|
||||
return '-';
|
||||
}
|
||||
if ($length <= 4) {
|
||||
return mb_substr($value, 0, 1) . '***';
|
||||
}
|
||||
if ($length <= 8) {
|
||||
return mb_substr($value, 0, 2) . '***' . mb_substr($value, -1);
|
||||
}
|
||||
|
||||
return mb_substr($value, 0, 4) . '****' . mb_substr($value, -4);
|
||||
}
|
||||
}
|
||||
@@ -5,31 +5,17 @@ declare(strict_types=1);
|
||||
namespace app\adminapi\logic\firstvisit;
|
||||
|
||||
use app\common\service\DataScope\DataScopeService;
|
||||
use app\common\service\qywx\QywxPromotionOpenWorkService;
|
||||
use app\common\service\qywx\QywxCustomerAcquisitionApiService;
|
||||
use app\common\service\qywx\QywxCustomerAcquisitionLinkService;
|
||||
use RuntimeException;
|
||||
use think\facade\Db;
|
||||
|
||||
/** 一诊 / 企业微信推广助手管理逻辑。 */
|
||||
/** 一诊 / 企业微信获客助手管理逻辑。 */
|
||||
class WecomPromotionLogic
|
||||
{
|
||||
public static function overview(int $adminId, array $adminInfo, string $domain): array
|
||||
{
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
$accountsQuery = Db::name('qywx_promotion_account')->alias('a')
|
||||
->leftJoin('admin u', 'u.id = a.owner_admin_id')
|
||||
->leftJoin('dept d', 'd.id = a.dept_id')
|
||||
->whereNull('a.delete_time');
|
||||
$accounts = $accountsQuery
|
||||
->field('a.id,a.corp_id,a.corp_name,a.agent_id,a.auth_status,a.owner_admin_id,a.dept_id,a.authorized_at,a.last_refresh_at,a.create_time,u.name as owner_name,d.name as dept_name')
|
||||
->order('a.auth_status', 'desc')
|
||||
->order('a.id', 'desc')
|
||||
->select()->toArray();
|
||||
foreach ($accounts as &$account) {
|
||||
$account['corp_id_masked'] = self::mask((string) ($account['corp_id'] ?? ''));
|
||||
unset($account['corp_id']);
|
||||
}
|
||||
unset($account);
|
||||
|
||||
$poolsQuery = Db::name('qywx_promotion_pool')->alias('p')
|
||||
->leftJoin('admin u', 'u.id = p.owner_admin_id')
|
||||
->leftJoin('dept d', 'd.id = p.dept_id')
|
||||
@@ -44,10 +30,9 @@ class WecomPromotionLogic
|
||||
$links = [];
|
||||
if ($poolIds !== []) {
|
||||
$links = Db::name('qywx_promotion_link')->alias('l')
|
||||
->leftJoin('qywx_promotion_account a', 'a.id = l.account_id AND a.delete_time IS NULL')
|
||||
->whereNull('l.delete_time')
|
||||
->whereIn('l.pool_id', $poolIds)
|
||||
->field('l.id,l.pool_id,l.account_id,l.name,l.group_name,l.wecom_url,l.weight,l.status,l.daily_limit,l.today_count,l.today_date,l.active_start,l.active_end,l.click_count,l.last_click_time,l.remark,l.create_time,l.update_time,a.corp_name,a.auth_status')
|
||||
->field('l.id,l.pool_id,l.name,l.group_name,l.wecom_url,l.remote_link_id,l.remote_status,l.remote_create_time,l.range_user_json,l.range_department_json,l.skip_verify,l.priority_option_json,l.last_sync_time,l.sync_error,l.weight,l.status,l.daily_limit,l.today_count,l.today_date,l.active_start,l.active_end,l.click_count,l.last_click_time,l.remark,l.create_time,l.update_time')
|
||||
->order('l.status', 'desc')
|
||||
->order('l.weight', 'desc')
|
||||
->order('l.id', 'desc')
|
||||
@@ -69,61 +54,42 @@ class WecomPromotionLogic
|
||||
$today = date('Y-m-d');
|
||||
$todayClicks = 0;
|
||||
$onlineLinks = 0;
|
||||
foreach ($links as $link) {
|
||||
if ((int) ($link['status'] ?? 0) === 1) {
|
||||
foreach ($links as &$link) {
|
||||
$link['range_userids'] = self::decodeStringList($link['range_user_json'] ?? null);
|
||||
$link['range_department_ids'] = self::decodeStringList($link['range_department_json'] ?? null);
|
||||
$link['priority_option'] = self::decodeObject($link['priority_option_json'] ?? null);
|
||||
$link['is_official'] = trim((string) ($link['remote_link_id'] ?? '')) !== '';
|
||||
$link['valid_customer_acquisition_link'] = QywxCustomerAcquisitionLinkService::isAllowed((string) ($link['wecom_url'] ?? ''));
|
||||
if ((int) ($link['status'] ?? 0) === 1 && $link['valid_customer_acquisition_link']) {
|
||||
$onlineLinks++;
|
||||
}
|
||||
if ((string) ($link['today_date'] ?? '') === $today) {
|
||||
$todayClicks += (int) ($link['today_count'] ?? 0);
|
||||
}
|
||||
}
|
||||
unset($link);
|
||||
|
||||
$config = QywxPromotionOpenWorkService::configurationStatus();
|
||||
$config['provider_callback_url'] = $domain . '/api/qywx-promotion/provider/callback';
|
||||
$config['auth_callback_url'] = QywxPromotionOpenWorkService::configuredRedirectUri(
|
||||
$domain . '/api/qywx-promotion/auth/callback'
|
||||
);
|
||||
$config = self::internalApplicationStatus($domain);
|
||||
|
||||
return [
|
||||
'meta' => [
|
||||
'scope_label' => DataScopeService::scopeLabel(DataScopeService::getEffectiveScope($adminInfo)),
|
||||
'can_authorize' => self::canAuthorize($adminId, $adminInfo),
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
'config' => $config,
|
||||
'summary' => [
|
||||
'authorized_accounts' => count(array_filter($accounts, static fn (array $row): bool => (int) ($row['auth_status'] ?? 0) === 1)),
|
||||
'configured_apps' => $config['ready'] ? 1 : 0,
|
||||
'pool_count' => count($pools),
|
||||
'online_links' => $onlineLinks,
|
||||
'today_clicks' => $todayClicks,
|
||||
],
|
||||
'accounts' => $accounts,
|
||||
'pools' => $pools,
|
||||
'links' => $links,
|
||||
'allowed_link_hosts' => array_values((array) config('qywx_promotion.allowed_link_hosts', [])),
|
||||
'member_options' => self::memberOptions($adminId, $adminInfo),
|
||||
'customer_acquisition_link_example' => QywxCustomerAcquisitionLinkService::example(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function authorizationUrl(int $adminId, array $adminInfo, string $domain): array
|
||||
{
|
||||
if (!self::canAuthorize($adminId, $adminInfo)) {
|
||||
throw new RuntimeException('只有系统管理员可以发起企业微信应用授权');
|
||||
}
|
||||
$redirectUri = rtrim($domain, '/') . '/api/qywx-promotion/auth/callback';
|
||||
|
||||
return ['url' => QywxPromotionOpenWorkService::authorizationUrl($adminId, $redirectUri)];
|
||||
}
|
||||
|
||||
public static function verifyAccount(int $id, int $adminId, array $adminInfo): array
|
||||
{
|
||||
if (!self::canAuthorize($adminId, $adminInfo)) {
|
||||
throw new RuntimeException('只有系统管理员可以验证企业微信授权凭证');
|
||||
}
|
||||
self::assertAuthorizedAccount($id, false);
|
||||
|
||||
return QywxPromotionOpenWorkService::verifyAccount($id);
|
||||
}
|
||||
|
||||
public static function savePool(array $params, int $adminId, array $adminInfo): array
|
||||
{
|
||||
$id = max(0, (int) ($params['id'] ?? 0));
|
||||
@@ -132,8 +98,8 @@ class WecomPromotionLogic
|
||||
throw new RuntimeException('请输入 1-60 个字符的分流方案名称');
|
||||
}
|
||||
$fallback = trim((string) ($params['fallback_url'] ?? ''));
|
||||
if (!QywxPromotionOpenWorkService::isAllowedPromotionUrl($fallback, true)) {
|
||||
throw new RuntimeException('兜底链接必须是已允许的 HTTPS 企业微信链接');
|
||||
if (!QywxCustomerAcquisitionLinkService::isAllowed($fallback, true)) {
|
||||
throw new RuntimeException('兜底链接必须是企业微信获客助手生成的 HTTPS 链接');
|
||||
}
|
||||
$now = time();
|
||||
$data = [
|
||||
@@ -174,17 +140,10 @@ class WecomPromotionLogic
|
||||
$id = max(0, (int) ($params['id'] ?? 0));
|
||||
$poolId = max(0, (int) ($params['pool_id'] ?? 0));
|
||||
$pool = self::assertScopedRow('qywx_promotion_pool', $poolId, $adminId, $adminInfo);
|
||||
$existing = $id > 0 ? self::assertScopedRow('qywx_promotion_link', $id, $adminId, $adminInfo) : null;
|
||||
$name = trim((string) ($params['name'] ?? ''));
|
||||
if ($name === '' || mb_strlen($name) > 80) {
|
||||
throw new RuntimeException('请输入 1-80 个字符的推广链接名称');
|
||||
}
|
||||
$url = trim((string) ($params['wecom_url'] ?? ''));
|
||||
if (!QywxPromotionOpenWorkService::isAllowedPromotionUrl($url)) {
|
||||
throw new RuntimeException('推广链接必须是已允许的 HTTPS 企业微信链接');
|
||||
}
|
||||
$accountId = max(0, (int) ($params['account_id'] ?? 0));
|
||||
if ($accountId > 0) {
|
||||
self::assertAuthorizedAccount($accountId, true);
|
||||
throw new RuntimeException('请输入 1-80 个字符的获客链接名称');
|
||||
}
|
||||
$startAt = self::parseTime($params['active_start'] ?? null);
|
||||
$endAt = self::parseTime($params['active_end'] ?? null);
|
||||
@@ -194,10 +153,9 @@ class WecomPromotionLogic
|
||||
$now = time();
|
||||
$data = [
|
||||
'pool_id' => $poolId,
|
||||
'account_id' => $accountId,
|
||||
'account_id' => 0,
|
||||
'name' => $name,
|
||||
'group_name' => mb_substr(trim((string) ($params['group_name'] ?? '默认分组')), 0, 60),
|
||||
'wecom_url' => $url,
|
||||
'weight' => min(100, max(1, (int) ($params['weight'] ?? 1))),
|
||||
'status' => (int) ($params['status'] ?? 1) === 1 ? 1 : 0,
|
||||
'daily_limit' => min(1000000, max(0, (int) ($params['daily_limit'] ?? 0))),
|
||||
@@ -206,8 +164,43 @@ class WecomPromotionLogic
|
||||
'remark' => mb_substr(trim((string) ($params['remark'] ?? '')), 0, 255),
|
||||
'update_time' => $now,
|
||||
];
|
||||
if ($id > 0) {
|
||||
self::assertScopedRow('qywx_promotion_link', $id, $adminId, $adminInfo);
|
||||
|
||||
// 历史手工链接只维护本地分流规则,不会在企业微信端创建重复链接。
|
||||
if ($existing !== null && trim((string) ($existing['remote_link_id'] ?? '')) === '') {
|
||||
$url = trim((string) ($params['wecom_url'] ?? $existing['wecom_url'] ?? ''));
|
||||
if (!QywxCustomerAcquisitionLinkService::isAllowed($url)) {
|
||||
throw new RuntimeException('历史链接必须是 https://work.weixin.qq.com/ca/... 格式');
|
||||
}
|
||||
$data['wecom_url'] = $url;
|
||||
Db::name('qywx_promotion_link')->where('id', $id)->update($data);
|
||||
|
||||
return ['id' => $id, 'mode' => 'legacy'];
|
||||
}
|
||||
|
||||
$userIds = self::resolveMemberUserIds((array) ($params['member_admin_ids'] ?? []), $adminId, $adminInfo);
|
||||
$skipVerify = (int) ($params['skip_verify'] ?? 0) === 1 ? 1 : 0;
|
||||
$payload = [
|
||||
'link_name' => $name,
|
||||
'range' => ['user_list' => $userIds],
|
||||
'skip_verify' => $skipVerify === 1,
|
||||
];
|
||||
|
||||
$api = new QywxCustomerAcquisitionApiService();
|
||||
if ($existing !== null) {
|
||||
$remoteLinkId = trim((string) ($existing['remote_link_id'] ?? ''));
|
||||
$payload['link_id'] = $remoteLinkId;
|
||||
$api->updateLink($payload);
|
||||
} else {
|
||||
$created = $api->createLink($payload);
|
||||
$remoteLinkId = self::extractRemoteLinkId($created);
|
||||
if ($remoteLinkId === '') {
|
||||
throw new RuntimeException('企业微信已创建链接,但接口未返回 link_id,请先执行“同步企业微信”确认结果');
|
||||
}
|
||||
}
|
||||
|
||||
$remote = self::normaliseRemoteLink($api->getLink($remoteLinkId), $remoteLinkId);
|
||||
$data += self::remoteColumns($remote, $now);
|
||||
if ($existing !== null) {
|
||||
Db::name('qywx_promotion_link')->where('id', $id)->update($data);
|
||||
} else {
|
||||
$data += [
|
||||
@@ -219,15 +212,142 @@ class WecomPromotionLogic
|
||||
'last_click_time' => 0,
|
||||
'create_time' => $now,
|
||||
];
|
||||
$id = (int) Db::name('qywx_promotion_link')->insertGetId($data);
|
||||
try {
|
||||
$id = (int) Db::name('qywx_promotion_link')->insertGetId($data);
|
||||
} catch (\Throwable $e) {
|
||||
try {
|
||||
$api->deleteLink($remoteLinkId);
|
||||
} catch (\Throwable) {
|
||||
// 远端补偿失败时保留原始异常,管理员可通过“同步企业微信”找回链接。
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return ['id' => $id];
|
||||
return ['id' => $id, 'remote_link_id' => $remoteLinkId, 'mode' => 'official'];
|
||||
}
|
||||
|
||||
/** 验证 CorpID、应用 Secret、可信 IP 与获客助手接口权限。 */
|
||||
public static function checkApiPermission(): array
|
||||
{
|
||||
return (new QywxCustomerAcquisitionApiService())->checkPermission();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将企业微信端获客链接同步进指定分流方案。
|
||||
* 非全量权限账号仅导入 range.user_list 与其可见成员有交集的链接,未知部门映射时严格隐藏。
|
||||
*/
|
||||
public static function syncRemoteLinks(int $poolId, int $adminId, array $adminInfo): array
|
||||
{
|
||||
$pool = self::assertScopedRow('qywx_promotion_pool', $poolId, $adminId, $adminInfo);
|
||||
$legacyCount = (int) Db::name('qywx_promotion_link')
|
||||
->where('pool_id', $poolId)
|
||||
->whereNull('delete_time')
|
||||
->whereRaw("(remote_link_id IS NULL OR remote_link_id = '')")
|
||||
->count();
|
||||
$visibleAdminIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
$visibleUserIds = null;
|
||||
if ($visibleAdminIds !== null) {
|
||||
$visibleUserIds = array_fill_keys(array_column(self::memberOptions($adminId, $adminInfo), 'userid'), true);
|
||||
}
|
||||
|
||||
$api = new QywxCustomerAcquisitionApiService();
|
||||
$cursor = '';
|
||||
$seen = 0;
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
$errors = [];
|
||||
|
||||
do {
|
||||
$page = $api->listLinks($cursor, 100);
|
||||
foreach ($page['link_id_list'] as $remoteLinkId) {
|
||||
if ($seen >= 500) {
|
||||
break 2;
|
||||
}
|
||||
$seen++;
|
||||
try {
|
||||
$remote = self::normaliseRemoteLink($api->getLink($remoteLinkId), $remoteLinkId);
|
||||
if (!self::canSeeRemoteLink($remote, $visibleUserIds)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
$result = self::upsertRemoteLink($remote, $pool, $adminId, $adminInfo);
|
||||
$result === 'created' ? $created++ : $updated++;
|
||||
} catch (\Throwable $e) {
|
||||
$failed++;
|
||||
if (count($errors) < 5) {
|
||||
$errors[] = $remoteLinkId . ':' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
$cursor = (string) ($page['next_cursor'] ?? '');
|
||||
} while ($cursor !== '');
|
||||
|
||||
return [
|
||||
'scanned' => $seen,
|
||||
'created' => $created,
|
||||
'updated' => $updated,
|
||||
'skipped' => $skipped,
|
||||
'failed' => $failed,
|
||||
'legacy_count' => $legacyCount,
|
||||
'empty_reason' => $seen === 0
|
||||
? '当前获客助手可调用应用没有通过 API 创建的官方获客链接;历史手工链接及其他应用创建的链接不会出现在该应用的同步列表中。'
|
||||
: '',
|
||||
'suggestion' => $seen === 0
|
||||
? '请点击“创建官方获客链接”通过当前应用创建。历史手工链接仍可参与本地分流,但无法同步官方 link_id 和官方获客数据。'
|
||||
: '',
|
||||
'truncated' => $cursor !== '',
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
/** 获取并刷新单条企业微信官方详情。 */
|
||||
public static function remoteLinkDetail(int $id, int $adminId, array $adminInfo): array
|
||||
{
|
||||
$row = self::assertScopedRow('qywx_promotion_link', $id, $adminId, $adminInfo);
|
||||
$remoteLinkId = trim((string) ($row['remote_link_id'] ?? ''));
|
||||
if ($remoteLinkId === '') {
|
||||
throw new RuntimeException('这是历史手工链接,没有企业微信 link_id');
|
||||
}
|
||||
$api = new QywxCustomerAcquisitionApiService();
|
||||
$remote = self::normaliseRemoteLink($api->getLink($remoteLinkId), $remoteLinkId);
|
||||
$visibleUserIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo) === null
|
||||
? null
|
||||
: array_fill_keys(array_column(self::memberOptions($adminId, $adminInfo), 'userid'), true);
|
||||
if (!self::canSeeRemoteLink($remote, $visibleUserIds)) {
|
||||
throw new RuntimeException('该获客链接已不在当前角色或部门的数据范围内');
|
||||
}
|
||||
Db::name('qywx_promotion_link')->where('id', $id)->update(self::remoteColumns($remote, time()));
|
||||
|
||||
return self::remotePublicPayload($remote);
|
||||
}
|
||||
|
||||
/** 永久删除企业微信端获客链接,本地保留审计记录并停止分流。 */
|
||||
public static function deleteRemoteLink(int $id, int $adminId, array $adminInfo): void
|
||||
{
|
||||
$row = self::assertScopedRow('qywx_promotion_link', $id, $adminId, $adminInfo);
|
||||
$remoteLinkId = trim((string) ($row['remote_link_id'] ?? ''));
|
||||
if ($remoteLinkId === '') {
|
||||
throw new RuntimeException('历史手工链接只能从本地移除');
|
||||
}
|
||||
(new QywxCustomerAcquisitionApiService())->deleteLink($remoteLinkId);
|
||||
Db::name('qywx_promotion_link')->where('id', $id)->update([
|
||||
'status' => 0,
|
||||
'remote_status' => 2,
|
||||
'last_sync_time' => time(),
|
||||
'sync_error' => '',
|
||||
'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function toggleLink(int $id, int $status, int $adminId, array $adminInfo): void
|
||||
{
|
||||
self::assertScopedRow('qywx_promotion_link', $id, $adminId, $adminInfo);
|
||||
$row = self::assertScopedRow('qywx_promotion_link', $id, $adminId, $adminInfo);
|
||||
if ($status === 1 && (int) ($row['remote_status'] ?? 0) === 2) {
|
||||
throw new RuntimeException('企业微信端已永久删除该链接,不能重新上线');
|
||||
}
|
||||
Db::name('qywx_promotion_link')->where('id', $id)->update([
|
||||
'status' => $status === 1 ? 1 : 0,
|
||||
'update_time' => time(),
|
||||
@@ -243,6 +363,251 @@ class WecomPromotionLogic
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return list<array{id:int,name:string,userid:string,dept_ids:list<int>,dept_names:list<string>}> */
|
||||
private static function memberOptions(int $adminId, array $adminInfo): array
|
||||
{
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
if ($visibleIds === []) {
|
||||
return [];
|
||||
}
|
||||
$query = Db::name('admin')->alias('a')
|
||||
->whereNull('a.delete_time')
|
||||
->where('a.work_wechat_userid', '<>', '');
|
||||
if ($visibleIds !== null) {
|
||||
$query->whereIn('a.id', $visibleIds);
|
||||
}
|
||||
$admins = $query->field('a.id,a.name,a.work_wechat_userid')->order('a.id', 'asc')->select()->toArray();
|
||||
if ($admins === []) {
|
||||
return [];
|
||||
}
|
||||
$adminIds = array_map('intval', array_column($admins, 'id'));
|
||||
$deptRows = Db::name('admin_dept')->alias('ad')
|
||||
->leftJoin('dept d', 'd.id = ad.dept_id')
|
||||
->whereIn('ad.admin_id', $adminIds)
|
||||
->field('ad.admin_id,ad.dept_id,d.name as dept_name')
|
||||
->order('ad.dept_id', 'asc')->select()->toArray();
|
||||
$departments = [];
|
||||
foreach ($deptRows as $row) {
|
||||
$aid = (int) ($row['admin_id'] ?? 0);
|
||||
$departments[$aid]['ids'][] = (int) ($row['dept_id'] ?? 0);
|
||||
if (trim((string) ($row['dept_name'] ?? '')) !== '') {
|
||||
$departments[$aid]['names'][] = (string) $row['dept_name'];
|
||||
}
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$seenUserIds = [];
|
||||
foreach ($admins as $admin) {
|
||||
$userId = trim((string) ($admin['work_wechat_userid'] ?? ''));
|
||||
if ($userId === '' || isset($seenUserIds[$userId])) {
|
||||
continue;
|
||||
}
|
||||
$seenUserIds[$userId] = true;
|
||||
$aid = (int) $admin['id'];
|
||||
$result[] = [
|
||||
'id' => $aid,
|
||||
'name' => (string) ($admin['name'] ?? $userId),
|
||||
'userid' => $userId,
|
||||
'dept_ids' => array_values(array_unique(array_filter($departments[$aid]['ids'] ?? []))),
|
||||
'dept_names' => array_values(array_unique($departments[$aid]['names'] ?? [])),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function resolveMemberUserIds(array $adminIds, int $adminId, array $adminInfo): array
|
||||
{
|
||||
$requested = array_values(array_unique(array_filter(array_map('intval', $adminIds))));
|
||||
if ($requested === []) {
|
||||
throw new RuntimeException('请至少选择一名当前角色或部门范围内的获客成员');
|
||||
}
|
||||
$available = [];
|
||||
foreach (self::memberOptions($adminId, $adminInfo) as $member) {
|
||||
$available[$member['id']] = $member['userid'];
|
||||
}
|
||||
$userIds = [];
|
||||
foreach ($requested as $requestedId) {
|
||||
if (!isset($available[$requestedId])) {
|
||||
throw new RuntimeException('选择的获客成员超出当前角色或部门的数据范围,或尚未绑定企业微信 userid');
|
||||
}
|
||||
$userIds[] = $available[$requestedId];
|
||||
}
|
||||
if (count($userIds) > 500) {
|
||||
throw new RuntimeException('单个获客链接最多配置 500 名成员');
|
||||
}
|
||||
|
||||
return array_values(array_unique($userIds));
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function normaliseRemoteLink(array $response, string $fallbackId = ''): array
|
||||
{
|
||||
$link = isset($response['link']) && is_array($response['link']) ? $response['link'] : $response;
|
||||
$linkId = trim((string) ($link['link_id'] ?? $response['link_id'] ?? $fallbackId));
|
||||
$url = trim((string) ($link['url'] ?? $link['link_url'] ?? $response['url'] ?? ''));
|
||||
if ($linkId === '') {
|
||||
throw new RuntimeException('企业微信获客链接详情缺少 link_id');
|
||||
}
|
||||
if (!QywxCustomerAcquisitionLinkService::isAllowed($url)) {
|
||||
throw new RuntimeException('企业微信获客链接详情未返回有效的 https://work.weixin.qq.com/ca/... 地址');
|
||||
}
|
||||
$range = isset($link['range']) && is_array($link['range']) ? $link['range'] : [];
|
||||
|
||||
return [
|
||||
'link_id' => $linkId,
|
||||
'link_name' => trim((string) ($link['link_name'] ?? $link['name'] ?? $linkId)),
|
||||
'url' => $url,
|
||||
'create_time' => max(0, (int) ($link['create_time'] ?? 0)),
|
||||
'range_userids' => self::normaliseScalarList($range['user_list'] ?? []),
|
||||
'range_department_ids' => self::normaliseScalarList($range['department_list'] ?? []),
|
||||
'skip_verify' => !empty($link['skip_verify']),
|
||||
'priority_option' => isset($link['priority_option']) && is_array($link['priority_option']) ? $link['priority_option'] : [],
|
||||
'snapshot' => $link,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function remoteColumns(array $remote, int $now): array
|
||||
{
|
||||
return [
|
||||
'name' => mb_substr((string) ($remote['link_name'] ?? ''), 0, 80),
|
||||
'wecom_url' => (string) ($remote['url'] ?? ''),
|
||||
'remote_link_id' => (string) ($remote['link_id'] ?? ''),
|
||||
'remote_status' => 1,
|
||||
'remote_create_time' => (int) ($remote['create_time'] ?? 0),
|
||||
'range_user_json' => self::encodeJson($remote['range_userids'] ?? []),
|
||||
'range_department_json' => self::encodeJson($remote['range_department_ids'] ?? []),
|
||||
'skip_verify' => !empty($remote['skip_verify']) ? 1 : 0,
|
||||
'priority_option_json' => self::encodeJson($remote['priority_option'] ?? []),
|
||||
'remote_snapshot' => self::encodeJson($remote['snapshot'] ?? []),
|
||||
'last_sync_time' => $now,
|
||||
'sync_error' => '',
|
||||
'update_time' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
private static function upsertRemoteLink(array $remote, array $pool, int $adminId, array $adminInfo): string
|
||||
{
|
||||
$remoteLinkId = (string) $remote['link_id'];
|
||||
$now = time();
|
||||
$existing = Db::name('qywx_promotion_link')->where('remote_link_id', $remoteLinkId)->find();
|
||||
$remoteData = self::remoteColumns($remote, $now);
|
||||
if ($existing) {
|
||||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||||
if ($visibleIds !== null && !in_array((int) ($existing['owner_admin_id'] ?? 0), $visibleIds, true)) {
|
||||
throw new RuntimeException('该链接已归属其他数据范围');
|
||||
}
|
||||
$remoteData['delete_time'] = null;
|
||||
Db::name('qywx_promotion_link')->where('id', (int) $existing['id'])->update($remoteData);
|
||||
|
||||
return 'updated';
|
||||
}
|
||||
|
||||
Db::name('qywx_promotion_link')->insert($remoteData + [
|
||||
'pool_id' => (int) $pool['id'],
|
||||
'account_id' => 0,
|
||||
'group_name' => '企业微信同步',
|
||||
'weight' => 1,
|
||||
'status' => 1,
|
||||
'daily_limit' => 0,
|
||||
'today_count' => 0,
|
||||
'today_date' => null,
|
||||
'active_start' => 0,
|
||||
'active_end' => 0,
|
||||
'click_count' => 0,
|
||||
'last_click_time' => 0,
|
||||
'owner_admin_id' => (int) ($pool['owner_admin_id'] ?? 0) ?: $adminId,
|
||||
'dept_id' => (int) ($pool['dept_id'] ?? 0) ?: self::primaryDeptId($adminId),
|
||||
'remark' => '',
|
||||
'create_time' => $now,
|
||||
'delete_time' => null,
|
||||
]);
|
||||
|
||||
return 'created';
|
||||
}
|
||||
|
||||
private static function canSeeRemoteLink(array $remote, ?array $visibleUserIds): bool
|
||||
{
|
||||
if ($visibleUserIds === null) {
|
||||
return true;
|
||||
}
|
||||
foreach ((array) ($remote['range_userids'] ?? []) as $userId) {
|
||||
if (isset($visibleUserIds[(string) $userId])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function remotePublicPayload(array $remote): array
|
||||
{
|
||||
return [
|
||||
'link_id' => (string) ($remote['link_id'] ?? ''),
|
||||
'link_name' => (string) ($remote['link_name'] ?? ''),
|
||||
'url' => (string) ($remote['url'] ?? ''),
|
||||
'create_time' => (int) ($remote['create_time'] ?? 0),
|
||||
'range_userids' => (array) ($remote['range_userids'] ?? []),
|
||||
'range_department_ids' => (array) ($remote['range_department_ids'] ?? []),
|
||||
'skip_verify' => !empty($remote['skip_verify']),
|
||||
'priority_option' => (array) ($remote['priority_option'] ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
private static function extractRemoteLinkId(array $response): string
|
||||
{
|
||||
if (isset($response['link']) && is_array($response['link'])) {
|
||||
return trim((string) ($response['link']['link_id'] ?? ''));
|
||||
}
|
||||
|
||||
return trim((string) ($response['link_id'] ?? ''));
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function normaliseScalarList(mixed $value): array
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter(array_map(
|
||||
static fn (mixed $item): string => trim((string) $item),
|
||||
$value
|
||||
), static fn (string $item): bool => $item !== '')));
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function decodeStringList(mixed $value): array
|
||||
{
|
||||
if (!is_string($value) || $value === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
return self::normaliseScalarList(is_array($decoded) ? $decoded : []);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function decodeObject(mixed $value): array
|
||||
{
|
||||
if (!is_string($value) || $value === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
private static function encodeJson(mixed $value): string
|
||||
{
|
||||
$encoded = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return $encoded === false ? '[]' : $encoded;
|
||||
}
|
||||
|
||||
private static function assertScopedRow(string $table, int $id, int $adminId, array $adminInfo): array
|
||||
{
|
||||
if ($id <= 0) {
|
||||
@@ -264,23 +629,6 @@ class WecomPromotionLogic
|
||||
return $row;
|
||||
}
|
||||
|
||||
private static function assertAuthorizedAccount(int $id, bool $requireActive): array
|
||||
{
|
||||
if ($id <= 0) {
|
||||
throw new RuntimeException('授权企业不存在');
|
||||
}
|
||||
$query = Db::name('qywx_promotion_account')->where('id', $id)->whereNull('delete_time');
|
||||
if ($requireActive) {
|
||||
$query->where('auth_status', 1);
|
||||
}
|
||||
$row = $query->find();
|
||||
if (!$row) {
|
||||
throw new RuntimeException($requireActive ? '授权企业无效或已取消授权' : '授权企业不存在');
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private static function applyOwnerScope($query, string $alias, ?array $visibleIds): void
|
||||
{
|
||||
if ($visibleIds === null) {
|
||||
@@ -293,19 +641,6 @@ class WecomPromotionLogic
|
||||
$query->whereIn($alias . '.owner_admin_id', $visibleIds);
|
||||
}
|
||||
|
||||
private static function canAuthorize(int $adminId, array $adminInfo): bool
|
||||
{
|
||||
if ((int) ($adminInfo['root'] ?? 0) === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Db::name('admin_role')->alias('ar')
|
||||
->join('system_role r', 'r.id = ar.role_id AND r.delete_time IS NULL')
|
||||
->where('ar.admin_id', $adminId)
|
||||
->where('r.name', '管理员')
|
||||
->count() > 0;
|
||||
}
|
||||
|
||||
private static function primaryDeptId(int $adminId): int
|
||||
{
|
||||
return (int) (Db::name('admin_dept')->where('admin_id', $adminId)->order('dept_id', 'asc')->value('dept_id') ?? 0);
|
||||
@@ -333,4 +668,34 @@ class WecomPromotionLogic
|
||||
|
||||
return substr($value, 0, 4) . str_repeat('*', max(4, $length - 8)) . substr($value, -4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部应用直接复用项目现有 work_wechat 配置,不经过第三方服务商授权。
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function internalApplicationStatus(string $domain): array
|
||||
{
|
||||
$corpId = trim((string) config('qywx_customer_acquisition.corp_id', ''));
|
||||
$agentId = trim((string) env('WECHAT_WORK_AGENT_ID', ''));
|
||||
if ($agentId === '') {
|
||||
$agentId = trim((string) env('work_wechat.agent_id', ''));
|
||||
}
|
||||
$apiStatus = QywxCustomerAcquisitionApiService::configurationStatus();
|
||||
$callbackTokenConfigured = trim((string) config('pay.wechat_work.contact_callback_token', '')) !== '';
|
||||
$callbackAesConfigured = trim((string) config('pay.wechat_work.contact_callback_aes_key', '')) !== '';
|
||||
|
||||
return [
|
||||
'mode' => 'internal',
|
||||
'configured' => $apiStatus['configured'],
|
||||
'ready' => $apiStatus['configured'],
|
||||
'missing' => $apiStatus['missing'],
|
||||
'corp_id_masked' => self::mask($corpId),
|
||||
'agent_id' => $agentId,
|
||||
'secret_configured' => trim((string) config('qywx_customer_acquisition.secret', '')) !== '',
|
||||
'callback_ready' => $callbackTokenConfigured && $callbackAesConfigured,
|
||||
'callback_url' => rtrim($domain, '/') . '/api/qywx/external-contact/notify',
|
||||
'official_doc' => 'https://developer.work.weixin.qq.com/document/path/97297',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1068,7 +1068,9 @@ class ConversionLogic
|
||||
->leftJoin('tcm_diagnosis dg', 'dg.id = po.diagnosis_id')
|
||||
->whereNull('po.delete_time')
|
||||
->where('po.payment_slip_audit_status', 1)
|
||||
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp])
|
||||
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp]);
|
||||
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($completedQuery, 'po');
|
||||
$completedQuery
|
||||
->fieldRaw("{$completedSourceExpr} AS source_admin_id, SUM(CASE WHEN po.prescription_audit_status = 1 THEN 1 ELSE 0 END) AS order_count, SUM(CASE WHEN po.prescription_audit_status = 1 THEN po.amount ELSE 0 END) AS total_amount")
|
||||
->group($completedSourceExpr);
|
||||
|
||||
@@ -1104,7 +1106,7 @@ class ConversionLogic
|
||||
->leftJoin('tcm_diagnosis dg', 'dg.id = po.diagnosis_id')
|
||||
->whereNull('po.delete_time')
|
||||
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp]);
|
||||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($businessQuery, 'po');
|
||||
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($businessQuery, 'po');
|
||||
$businessQuery
|
||||
->fieldRaw("{$businessSourceExpr} AS source_admin_id, SUM(po.amount) AS total_amount")
|
||||
->group($businessSourceExpr);
|
||||
|
||||
@@ -433,7 +433,7 @@ class DoctorDailyStatsLogic
|
||||
->join('tcm_prescription rx', 'rx.id = o.prescription_id AND rx.delete_time IS NULL', 'INNER')
|
||||
->whereNull('o.delete_time')
|
||||
->whereBetween('o.create_time', [$startTs, $endTs]);
|
||||
YejiStatsLogic::applyPrescriptionOrderNotCancelledForPerformanceQuery($q, 'o');
|
||||
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($q, 'o');
|
||||
$q->whereIn('rx.creator_id', $doctorIds)
|
||||
->where('o.diagnosis_id', '>', 0);
|
||||
|
||||
|
||||
@@ -3893,6 +3893,18 @@ class YejiStatsLogic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 有效金额统计:在业绩履约状态口径上,再排除任何已发生退款(含部分退款)的订单。
|
||||
*
|
||||
* @param \think\db\BaseQuery|\think\Model $query
|
||||
*/
|
||||
public static function applyPrescriptionOrderEffectiveAmountQuery($query, string $tableAlias = ''): void
|
||||
{
|
||||
self::applyPrescriptionOrderNotCancelledForPerformanceQuery($query, $tableAlias);
|
||||
$refundField = $tableAlias !== '' ? "{$tableAlias}.refund_amount" : 'refund_amount';
|
||||
$query->whereRaw("({$refundField} IS NULL OR {$refundField} <= 0)");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \think\db\BaseQuery|\think\Model $query
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use app\common\service\qywx\QywxCustomerAcquisitionCustomerService;
|
||||
use EasyWeChat\Kernel\Exceptions\BadRequestException;
|
||||
use EasyWeChat\Work\Application;
|
||||
use EasyWeChat\Work\Message;
|
||||
@@ -17,7 +18,7 @@ use think\facade\Log;
|
||||
*
|
||||
* ⚠️ 关于"员工↔客户消息内容"接收:
|
||||
* 企业微信 **不会** 通过本回调推送客户与员工之间真实的聊天消息内容,
|
||||
* 这里只处理"添加 / 编辑 / 删除客户"等业务事件(change_external_contact 变体)。
|
||||
* 这里只处理客户关系事件,以及 customer_acquisition 回调中的累计收消息次数;不保存消息正文。
|
||||
* 实时消息接收走「会话内容存档」独立通道:
|
||||
* - 命令: php think qywx:sync-msg-archive
|
||||
* - 服务: app\common\service\wechat\QywxMsgArchiveService
|
||||
@@ -33,15 +34,18 @@ class QywxExternalContactCallbackController extends BaseApiController
|
||||
{
|
||||
$corpId = (string) config('pay.wechat_work.corp_id', '');
|
||||
$customerSecret = (string) config('pay.wechat_work.customer_contact_secret', '');
|
||||
$acquisitionSecret = (string) config('qywx_customer_acquisition.secret', '');
|
||||
$payContactSecret = (string) config('pay.wechat_work.external_pay_secret', '');
|
||||
// 客户联系回调验签需用「接收事件服务器」所属应用的 Secret,优先使用 customer_contact_secret,
|
||||
// 缺省回退到 external_pay_secret 保持向后兼容(同一应用同时具备两类权限的旧部署可继续工作)。
|
||||
$secret = $customerSecret !== '' ? $customerSecret : $payContactSecret;
|
||||
$secret = $customerSecret !== ''
|
||||
? $customerSecret
|
||||
: ($acquisitionSecret !== '' ? $acquisitionSecret : $payContactSecret);
|
||||
$token = (string) config('pay.wechat_work.contact_callback_token', '');
|
||||
$aesKey = (string) config('pay.wechat_work.contact_callback_aes_key', '');
|
||||
|
||||
if ($corpId === '' || $secret === '' || $token === '' || $aesKey === '') {
|
||||
Log::error('qywx external contact callback: 缺少配置 corp_id / customer_contact_secret(或 external_pay_secret) / contact_callback_token / contact_callback_aes_key');
|
||||
Log::error('qywx external contact callback: 缺少配置 corp_id / customer_contact_secret(或获客助手应用 secret) / contact_callback_token / contact_callback_aes_key');
|
||||
|
||||
return response('config error', 503, ['Content-Type' => 'text/plain; charset=utf-8']);
|
||||
}
|
||||
@@ -67,6 +71,17 @@ class QywxExternalContactCallbackController extends BaseApiController
|
||||
return $next($message);
|
||||
});
|
||||
|
||||
$server->addEventListener('customer_acquisition', function (Message $message, \Closure $next) {
|
||||
try {
|
||||
(new QywxCustomerAcquisitionCustomerService())->handleCallback($message->toArray());
|
||||
} catch (\Throwable $e) {
|
||||
// 服务已将失败事件与 next_retry 落库,定时命令会在 ChatKey 30 分钟内继续重试。
|
||||
Log::error('qywx customer acquisition callback: ' . $e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
|
||||
return $next($message);
|
||||
});
|
||||
|
||||
$psr = $server->serve();
|
||||
$body = $psr->getBody();
|
||||
$body->rewind();
|
||||
|
||||
@@ -4,15 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\service\qywx\QywxPromotionOpenWorkService;
|
||||
use app\common\service\qywx\QywxPromotionRedirectService;
|
||||
use think\facade\Log;
|
||||
|
||||
/** 企业微信推广公开端点:服务商回调、授权回跳、JS 与随机跳转。 */
|
||||
/** 企业微信获客助手公开端点:JS 与随机跳转。 */
|
||||
class QywxPromotionPublicController extends BaseApiController
|
||||
{
|
||||
/** 公开安装代码、随机跳转与企业微信服务商回调均不依赖前台用户登录。 */
|
||||
public array $notNeedLogin = ['script', 'redirect', 'providerCallback', 'authCallback'];
|
||||
/** 公开安装代码与随机跳转不依赖前台用户登录。 */
|
||||
public array $notNeedLogin = ['script', 'redirect'];
|
||||
|
||||
public function script(string $key)
|
||||
{
|
||||
@@ -58,7 +56,7 @@ JS;
|
||||
'ip' => (string) $this->request->ip(),
|
||||
]);
|
||||
if (!$picked) {
|
||||
return response('当前暂无可用的企业微信推广链接,请稍后再试。', 503, [
|
||||
return response('当前暂无可用的企业微信获客助手链接,请稍后再试。', 503, [
|
||||
'Content-Type' => 'text/plain; charset=utf-8',
|
||||
'Cache-Control' => 'no-store',
|
||||
]);
|
||||
@@ -70,40 +68,4 @@ JS;
|
||||
]);
|
||||
}
|
||||
|
||||
public function providerCallback()
|
||||
{
|
||||
try {
|
||||
$psr = QywxPromotionOpenWorkService::serveProviderCallback();
|
||||
$body = $psr->getBody();
|
||||
$body->rewind();
|
||||
$headers = [];
|
||||
if ($psr->getHeaderLine('Content-Type') !== '') {
|
||||
$headers['Content-Type'] = $psr->getHeaderLine('Content-Type');
|
||||
}
|
||||
|
||||
return response($body->getContents(), $psr->getStatusCode(), $headers);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('企微推广服务商回调失败:' . $e->getMessage(), ['exception' => $e]);
|
||||
|
||||
return response('error', 500, ['Content-Type' => 'text/plain; charset=utf-8']);
|
||||
}
|
||||
}
|
||||
|
||||
public function authCallback()
|
||||
{
|
||||
$fallback = rtrim($this->request->domain(), '/') . '/admin/first_visit/wecom_promotion';
|
||||
$returnUrl = QywxPromotionOpenWorkService::configuredAdminReturnUrl($fallback);
|
||||
try {
|
||||
$result = QywxPromotionOpenWorkService::consumeAuthorizationCallback(
|
||||
trim((string) $this->request->get('auth_code', '')),
|
||||
trim((string) $this->request->get('state', ''))
|
||||
);
|
||||
$query = ['wecom_auth' => 'success', 'account_id' => (int) $result['id']];
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('企微推广授权回跳失败:' . $e->getMessage(), ['exception' => $e]);
|
||||
$query = ['wecom_auth' => 'failed', 'message' => mb_substr($e->getMessage(), 0, 160)];
|
||||
}
|
||||
|
||||
return redirect($returnUrl . (str_contains($returnUrl, '?') ? '&' : '?') . http_build_query($query), 302);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ use think\facade\Route;
|
||||
Route::rule('qywx/external-contact/notify', 'QywxExternalContactCallback/notify', 'GET|POST');
|
||||
Route::post('ej-pharmacy/webhook', 'EjPharmacyCallback/webhook');
|
||||
|
||||
// 企业微信推广助手:服务商应用指令、授权回跳、公开 JS 与随机分流。
|
||||
Route::rule('qywx-promotion/provider/callback', 'QywxPromotionPublic/providerCallback', 'GET|POST');
|
||||
Route::get('qywx-promotion/auth/callback', 'QywxPromotionPublic/authCallback');
|
||||
// 企业微信内部应用推广助手:公开 JS 与服务端随机分流。
|
||||
Route::get('qywx-promotion/js/:key', 'QywxPromotionPublic/script');
|
||||
Route::get('qywx-promotion/go/:key', 'QywxPromotionPublic/redirect');
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\qywx\QywxCustomerAcquisitionCustomerService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
|
||||
/** 每分钟重试获客助手 message_from_customer/customer_start_chat 回调。 */
|
||||
class QywxRetryCustomerAcquisitionEvents extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('qywx:retry-customer-acquisition-events')
|
||||
->setDescription('重试 30 分钟有效期内失败的企业微信获客会话回调');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
$result = (new QywxCustomerAcquisitionCustomerService())->retryPending(100);
|
||||
$output->writeln(sprintf(
|
||||
'QYWX_CUSTOMER_ACQUISITION_RETRY selected=%d success=%d failed=%d expired=%d',
|
||||
$result['selected'],
|
||||
$result['success'],
|
||||
$result['failed'],
|
||||
$result['expired']
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\qywx;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use RuntimeException;
|
||||
use think\facade\Cache;
|
||||
|
||||
/**
|
||||
* 企业微信内部应用获客链接 API。
|
||||
*
|
||||
* @see https://developer.work.weixin.qq.com/document/path/97297
|
||||
*/
|
||||
class QywxCustomerAcquisitionApiService
|
||||
{
|
||||
private const TOKEN_INVALID_CODES = [40001, 40014, 42001];
|
||||
|
||||
private string $corpId;
|
||||
private string $secret;
|
||||
private Client $client;
|
||||
/** @var null|callable():string */
|
||||
private $accessTokenResolver;
|
||||
|
||||
/** @param null|callable():string $accessTokenResolver 仅用于测试或托管 token 场景。 */
|
||||
public function __construct(?Client $client = null, ?callable $accessTokenResolver = null)
|
||||
{
|
||||
$this->corpId = trim((string) config('qywx_customer_acquisition.corp_id', ''));
|
||||
$this->secret = trim((string) config('qywx_customer_acquisition.secret', ''));
|
||||
$this->client = $client ?? new Client([
|
||||
'base_uri' => rtrim((string) config('qywx_customer_acquisition.base_uri', 'https://qyapi.weixin.qq.com'), '/') . '/',
|
||||
'timeout' => max(5, (int) config('qywx_customer_acquisition.timeout', 20)),
|
||||
'connect_timeout' => 8,
|
||||
'http_errors' => false,
|
||||
'verify' => config('qywx_customer_acquisition.verify', true),
|
||||
'headers' => ['Accept' => 'application/json'],
|
||||
]);
|
||||
$this->accessTokenResolver = $accessTokenResolver;
|
||||
}
|
||||
|
||||
/** @return array{configured:bool,missing:list<string>} */
|
||||
public static function configurationStatus(): array
|
||||
{
|
||||
$missing = [];
|
||||
if (trim((string) config('qywx_customer_acquisition.corp_id', '')) === '') {
|
||||
$missing[] = 'work_wechat.corp_id';
|
||||
}
|
||||
if (trim((string) config('qywx_customer_acquisition.secret', '')) === '') {
|
||||
$missing[] = 'work_wechat.customer_acquisition_secret / secret';
|
||||
}
|
||||
|
||||
return ['configured' => $missing === [], 'missing' => $missing];
|
||||
}
|
||||
|
||||
/** @return array{link_id_list:list<string>,next_cursor:string} */
|
||||
public function listLinks(string $cursor = '', int $limit = 100): array
|
||||
{
|
||||
$body = ['limit' => min(100, max(1, $limit))];
|
||||
if ($cursor !== '') {
|
||||
$body['cursor'] = $cursor;
|
||||
}
|
||||
$response = $this->request('POST', 'cgi-bin/externalcontact/customer_acquisition/list_link', $body);
|
||||
|
||||
return [
|
||||
'link_id_list' => array_values(array_filter(array_map('strval', (array) ($response['link_id_list'] ?? [])))),
|
||||
'next_cursor' => trim((string) ($response['next_cursor'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function getLink(string $linkId): array
|
||||
{
|
||||
$this->assertLinkId($linkId);
|
||||
|
||||
return $this->request('POST', 'cgi-bin/externalcontact/customer_acquisition/get', ['link_id' => $linkId]);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function createLink(array $payload): array
|
||||
{
|
||||
return $this->request('POST', 'cgi-bin/externalcontact/customer_acquisition/create_link', $payload);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function updateLink(array $payload): array
|
||||
{
|
||||
$this->assertLinkId((string) ($payload['link_id'] ?? ''));
|
||||
|
||||
return $this->request('POST', 'cgi-bin/externalcontact/customer_acquisition/update_link', $payload);
|
||||
}
|
||||
|
||||
public function deleteLink(string $linkId): void
|
||||
{
|
||||
$this->assertLinkId($linkId);
|
||||
$this->request('POST', 'cgi-bin/externalcontact/customer_acquisition/delete_link', ['link_id' => $linkId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定获客链接添加的客户。单页最多 1000 条。
|
||||
*
|
||||
* @return array{customer_list:list<array<string,mixed>>,next_cursor:string}
|
||||
*/
|
||||
public function listCustomers(string $linkId, string $cursor = '', int $limit = 1000): array
|
||||
{
|
||||
$this->assertLinkId($linkId);
|
||||
$body = [
|
||||
'link_id' => $linkId,
|
||||
'limit' => min(1000, max(1, $limit)),
|
||||
];
|
||||
if ($cursor !== '') {
|
||||
$body['cursor'] = $cursor;
|
||||
}
|
||||
$response = $this->request('POST', 'cgi-bin/externalcontact/customer_acquisition/customer', $body);
|
||||
$customers = array_values(array_filter(
|
||||
(array) ($response['customer_list'] ?? []),
|
||||
static fn (mixed $row): bool => is_array($row)
|
||||
));
|
||||
|
||||
return [
|
||||
'customer_list' => $customers,
|
||||
'next_cursor' => trim((string) ($response['next_cursor'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function getChatInfo(string $chatKey): array
|
||||
{
|
||||
$chatKey = trim($chatKey);
|
||||
if ($chatKey === '' || strlen($chatKey) > 512) {
|
||||
throw new RuntimeException('获客会话 ChatKey 不正确');
|
||||
}
|
||||
|
||||
return $this->request(
|
||||
'POST',
|
||||
'cgi-bin/externalcontact/customer_acquisition/get_chat_info',
|
||||
['chat_key' => $chatKey]
|
||||
);
|
||||
}
|
||||
|
||||
/** 通过只读列表接口验证 token、可信 IP、获客助手开通状态与应用权限。 */
|
||||
public function checkPermission(): array
|
||||
{
|
||||
$result = $this->listLinks('', 1);
|
||||
$hasLink = $result['link_id_list'] !== [];
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => $hasLink
|
||||
? '获客助手 API 权限验证通过,当前应用已有官方获客链接'
|
||||
: '获客助手 API 权限验证通过,但当前应用尚未通过 API 创建官方获客链接',
|
||||
'has_link' => $hasLink,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function request(string $method, string $path, array $body = [], bool $retried = false): array
|
||||
{
|
||||
$this->assertConfigured();
|
||||
$cacheKey = $this->tokenCacheKey();
|
||||
$token = $this->accessToken();
|
||||
try {
|
||||
$options = ['query' => ['access_token' => $token]];
|
||||
if (strtoupper($method) === 'POST') {
|
||||
$options['json'] = $body;
|
||||
}
|
||||
$response = $this->client->request($method, ltrim($path, '/'), $options);
|
||||
} catch (GuzzleException $e) {
|
||||
throw new RuntimeException('企业微信获客助手接口连接失败,请检查服务器网络与可信 IP 配置', 0, $e);
|
||||
}
|
||||
|
||||
$decoded = json_decode((string) $response->getBody(), true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new RuntimeException('企业微信获客助手接口返回了无法解析的数据');
|
||||
}
|
||||
$errcode = (int) ($decoded['errcode'] ?? 0);
|
||||
if ($errcode === 0) {
|
||||
return $decoded;
|
||||
}
|
||||
if (!$retried && in_array($errcode, self::TOKEN_INVALID_CODES, true)) {
|
||||
Cache::delete($cacheKey);
|
||||
|
||||
return $this->request($method, $path, $body, true);
|
||||
}
|
||||
|
||||
throw new RuntimeException(sprintf(
|
||||
'企业微信获客助手接口失败[%d]:%s',
|
||||
$errcode,
|
||||
trim((string) ($decoded['errmsg'] ?? '未知错误')) ?: '未知错误'
|
||||
));
|
||||
}
|
||||
|
||||
private function accessToken(): string
|
||||
{
|
||||
if ($this->accessTokenResolver !== null) {
|
||||
$token = trim((string) call_user_func($this->accessTokenResolver));
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('托管 access_token 为空');
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
$cacheKey = $this->tokenCacheKey();
|
||||
$cached = trim((string) Cache::get($cacheKey, ''));
|
||||
if ($cached !== '') {
|
||||
return $cached;
|
||||
}
|
||||
try {
|
||||
$response = $this->client->request('GET', 'cgi-bin/gettoken', [
|
||||
'query' => ['corpid' => $this->corpId, 'corpsecret' => $this->secret],
|
||||
]);
|
||||
} catch (GuzzleException $e) {
|
||||
throw new RuntimeException('获取企业微信 access_token 失败,请检查服务器网络', 0, $e);
|
||||
}
|
||||
$decoded = json_decode((string) $response->getBody(), true);
|
||||
if (!is_array($decoded) || (int) ($decoded['errcode'] ?? 0) !== 0 || empty($decoded['access_token'])) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'获取企业微信 access_token 失败[%d]:%s',
|
||||
(int) ($decoded['errcode'] ?? -1),
|
||||
trim((string) ($decoded['errmsg'] ?? '未知错误')) ?: '未知错误'
|
||||
));
|
||||
}
|
||||
$token = (string) $decoded['access_token'];
|
||||
Cache::set($cacheKey, $token, max(60, (int) ($decoded['expires_in'] ?? 7200) - 300));
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
private function tokenCacheKey(): string
|
||||
{
|
||||
return 'qywx_customer_acquisition_token:' . hash('sha256', $this->corpId . '|' . $this->secret);
|
||||
}
|
||||
|
||||
private function assertConfigured(): void
|
||||
{
|
||||
$status = self::configurationStatus();
|
||||
if (!$status['configured']) {
|
||||
throw new RuntimeException('获客助手应用配置不完整:缺少 ' . implode('、', $status['missing']));
|
||||
}
|
||||
}
|
||||
|
||||
private function assertLinkId(string $linkId): void
|
||||
{
|
||||
if ($linkId === '' || strlen($linkId) > 128) {
|
||||
throw new RuntimeException('获客链接 ID 不正确');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\qywx;
|
||||
|
||||
use RuntimeException;
|
||||
use think\facade\Db;
|
||||
|
||||
/** 获客客户归因、会话统计与回调幂等落库。 */
|
||||
class QywxCustomerAcquisitionCustomerService
|
||||
{
|
||||
private QywxCustomerAcquisitionApiService $api;
|
||||
|
||||
public function __construct(?QywxCustomerAcquisitionApiService $api = null)
|
||||
{
|
||||
$this->api = $api ?? new QywxCustomerAcquisitionApiService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步一个远端获客链接的全部客户,远端列表字段采用覆盖语义。
|
||||
* recv_msg_cnt 不在列表接口中返回,因此同步时保留本地值。
|
||||
*
|
||||
* @return array{scanned:int,created:int,updated:int,pages:int,truncated:bool}
|
||||
*/
|
||||
public function syncLink(string $remoteLinkId, int $maxCustomers = 20000): array
|
||||
{
|
||||
$remoteLinkId = trim($remoteLinkId);
|
||||
if ($remoteLinkId === '') {
|
||||
throw new RuntimeException('获客链接 ID 不能为空');
|
||||
}
|
||||
$cursor = '';
|
||||
$scanned = 0;
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$pages = 0;
|
||||
do {
|
||||
$page = $this->api->listCustomers($remoteLinkId, $cursor, 1000);
|
||||
$pages++;
|
||||
foreach ($page['customer_list'] as $customer) {
|
||||
if ($scanned >= $maxCustomers) {
|
||||
break 2;
|
||||
}
|
||||
$scanned++;
|
||||
$result = self::upsertCustomer($remoteLinkId, $customer, false);
|
||||
$result === 'created' ? $created++ : $updated++;
|
||||
}
|
||||
$cursor = (string) ($page['next_cursor'] ?? '');
|
||||
} while ($cursor !== '');
|
||||
|
||||
return compact('scanned', 'created', 'updated', 'pages') + ['truncated' => $cursor !== ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 customer_acquisition 回调。相同事件只成功处理一次;失败事件保留审计并允许企微重试。
|
||||
*
|
||||
* @return array{duplicate:bool,status:string}
|
||||
*/
|
||||
public function handleCallback(array $message): array
|
||||
{
|
||||
$changeType = trim((string) ($message['ChangeType'] ?? $message['change_type'] ?? ''));
|
||||
if (!in_array($changeType, ['customer_start_chat', 'message_from_customer'], true)) {
|
||||
return ['duplicate' => false, 'status' => 'ignored'];
|
||||
}
|
||||
$chatKey = trim((string) ($message['ChatKey'] ?? $message['Chatkey'] ?? $message['chat_key'] ?? ''));
|
||||
$eventTime = (int) ($message['CreateTime'] ?? $message['create_time'] ?? 0);
|
||||
$eventKey = self::eventKey($message, $changeType, $chatKey, $eventTime);
|
||||
$event = self::beginEvent($eventKey, $changeType, $chatKey, $eventTime, $message);
|
||||
if (($event['duplicate'] ?? false) === true) {
|
||||
return ['duplicate' => true, 'status' => 'success'];
|
||||
}
|
||||
|
||||
$eventId = (int) ($event['id'] ?? 0);
|
||||
try {
|
||||
// customer_start_chat 仅能确认“客户已发起会话”,企业微信不保证该事件携带 ChatKey。
|
||||
// 此时先落归因与聊天状态,精确消息数等待 message_from_customer 回调补齐。
|
||||
if ($changeType === 'customer_start_chat' && $chatKey === '') {
|
||||
$remoteLinkId = trim((string) (
|
||||
$message['LinkID'] ?? $message['LinkId'] ?? $message['link_id'] ?? ''
|
||||
));
|
||||
$externalUserId = trim((string) (
|
||||
$message['ExternalUserID'] ?? $message['ExternalUserId'] ?? $message['external_userid'] ?? ''
|
||||
));
|
||||
$userId = trim((string) ($message['UserID'] ?? $message['UserId'] ?? $message['userid'] ?? ''));
|
||||
if ($remoteLinkId === '' || $externalUserId === '' || $userId === '') {
|
||||
throw new RuntimeException('customer_start_chat 回调缺少 link_id / external_userid / userid');
|
||||
}
|
||||
self::upsertCustomer($remoteLinkId, [
|
||||
'external_userid' => $externalUserId,
|
||||
'userid' => $userId,
|
||||
'chat_status' => 1,
|
||||
'state' => (string) ($message['State'] ?? $message['state'] ?? ''),
|
||||
'event_time' => $eventTime,
|
||||
'snapshot' => $message,
|
||||
], false);
|
||||
self::finishEvent($eventId, 1, '', $remoteLinkId, $externalUserId, $userId);
|
||||
|
||||
return ['duplicate' => false, 'status' => 'success'];
|
||||
}
|
||||
if ($chatKey === '') {
|
||||
self::finishEvent($eventId, 3, 'failed_invalid: message_from_customer 回调缺少 ChatKey');
|
||||
throw new RuntimeException('message_from_customer 回调缺少 ChatKey');
|
||||
}
|
||||
$now = time();
|
||||
if ($eventTime > 0 && ($now - $eventTime) >= 1800) {
|
||||
throw new RuntimeException('获客回调 ChatKey 已超过 30 分钟有效期');
|
||||
}
|
||||
$chat = $this->api->getChatInfo($chatKey);
|
||||
$chatInfo = is_array($chat['chat_info'] ?? null) ? $chat['chat_info'] : [];
|
||||
$remoteLinkId = trim((string) (
|
||||
$chatInfo['link_id'] ?? $message['LinkID'] ?? $message['LinkId'] ?? $message['link_id'] ?? ''
|
||||
));
|
||||
$externalUserId = trim((string) (
|
||||
$chat['external_userid'] ?? $message['ExternalUserID'] ?? $message['ExternalUserId'] ?? ''
|
||||
));
|
||||
$userId = trim((string) ($chat['userid'] ?? $message['UserID'] ?? $message['UserId'] ?? ''));
|
||||
if ($remoteLinkId === '' || $externalUserId === '' || $userId === '') {
|
||||
throw new RuntimeException('get_chat_info 未返回完整的 link_id / external_userid / userid');
|
||||
}
|
||||
self::upsertCustomer($remoteLinkId, [
|
||||
'external_userid' => $externalUserId,
|
||||
'userid' => $userId,
|
||||
'chat_status' => max(1, (int) ($message['ChatStatus'] ?? 1)),
|
||||
'recv_msg_cnt' => max(0, (int) ($chatInfo['recv_msg_cnt'] ?? 0)),
|
||||
'state' => (string) ($chatInfo['state'] ?? $message['State'] ?? ''),
|
||||
'event_time' => $eventTime,
|
||||
'snapshot' => $chat,
|
||||
], true);
|
||||
self::finishEvent($eventId, 1, '', $remoteLinkId, $externalUserId, $userId);
|
||||
|
||||
return ['duplicate' => false, 'status' => 'success'];
|
||||
} catch (\Throwable $e) {
|
||||
if ($eventId > 0 && str_contains($e->getMessage(), 'message_from_customer 回调缺少 ChatKey')) {
|
||||
throw $e;
|
||||
}
|
||||
self::scheduleRetryOrExpire($eventId, $eventTime, $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试仍在 ChatKey 30 分钟有效期内的失败回调,并把到期记录明确标记 failed_expired。
|
||||
*
|
||||
* @return array{selected:int,success:int,failed:int,expired:int}
|
||||
*/
|
||||
public function retryPending(int $limit = 100): array
|
||||
{
|
||||
$now = time();
|
||||
// 进程在 beginEvent 后异常退出时,处理中事件会卡在 status=0;一分钟后自动回收再试。
|
||||
Db::name('qywx_customer_acquisition_event')
|
||||
->where('status', 0)
|
||||
->where('update_time', '<=', $now - 60)
|
||||
->where('expire_time', '>', $now)
|
||||
->update([
|
||||
'status' => 2,
|
||||
'next_retry' => $now,
|
||||
'error_message' => 'watchdog_recovered: 上次处理未正常结束',
|
||||
'update_time' => $now,
|
||||
]);
|
||||
$expired = (int) Db::name('qywx_customer_acquisition_event')
|
||||
->whereIn('status', [0, 2])
|
||||
->where('expire_time', '>', 0)
|
||||
->where('expire_time', '<=', $now)
|
||||
->update([
|
||||
'status' => 3,
|
||||
'next_retry' => 0,
|
||||
'error_message' => 'failed_expired: ChatKey 已超过 30 分钟有效期',
|
||||
'chat_key' => '',
|
||||
'raw_json' => null,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
$rows = Db::name('qywx_customer_acquisition_event')
|
||||
->where('status', 2)
|
||||
->where('next_retry', '<=', $now)
|
||||
->where('expire_time', '>', $now)
|
||||
->order('next_retry', 'asc')
|
||||
->limit(min(500, max(1, $limit)))
|
||||
->select()->toArray();
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
foreach ($rows as $row) {
|
||||
$message = json_decode((string) ($row['raw_json'] ?? ''), true);
|
||||
if (!is_array($message)) {
|
||||
self::scheduleRetryOrExpire(
|
||||
(int) $row['id'],
|
||||
(int) ($row['event_time'] ?? 0),
|
||||
'回调原始数据无法解析'
|
||||
);
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$this->handleCallback($message);
|
||||
$success++;
|
||||
} catch (\Throwable) {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
return ['selected' => count($rows), 'success' => $success, 'failed' => $failed, 'expired' => $expired];
|
||||
}
|
||||
|
||||
public static function eventKey(array $message, string $changeType, string $chatKey, int $eventTime): string
|
||||
{
|
||||
$parts = [
|
||||
(string) ($message['MsgId'] ?? $message['MsgID'] ?? ''),
|
||||
$changeType,
|
||||
$chatKey,
|
||||
(string) $eventTime,
|
||||
(string) ($message['LinkID'] ?? $message['LinkId'] ?? ''),
|
||||
(string) ($message['ExternalUserID'] ?? $message['ExternalUserId'] ?? ''),
|
||||
(string) ($message['UserID'] ?? $message['UserId'] ?? ''),
|
||||
];
|
||||
|
||||
return hash('sha256', implode('|', $parts));
|
||||
}
|
||||
|
||||
/** @return array{expire_time:int,next_retry:int,expired:bool} */
|
||||
public static function retryDecision(int $eventTime, int $now, int $storedExpireTime = 0): array
|
||||
{
|
||||
$expireTime = $storedExpireTime > 0
|
||||
? $storedExpireTime
|
||||
: ($eventTime > 0 ? $eventTime + 1800 : $now + 1800);
|
||||
$expired = $expireTime <= $now;
|
||||
|
||||
return [
|
||||
'expire_time' => $expireTime,
|
||||
'next_retry' => $expired ? 0 : min($expireTime - 1, $now + 30),
|
||||
'expired' => $expired,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{id:int,duplicate:bool} */
|
||||
private static function beginEvent(
|
||||
string $eventKey,
|
||||
string $changeType,
|
||||
string $chatKey,
|
||||
int $eventTime,
|
||||
array $message
|
||||
): array {
|
||||
$now = time();
|
||||
$raw = self::encodeJson($message);
|
||||
$expireTime = self::retryDecision($eventTime, $now)['expire_time'];
|
||||
try {
|
||||
$id = (int) Db::name('qywx_customer_acquisition_event')->insertGetId([
|
||||
'event_key' => $eventKey,
|
||||
'change_type' => $changeType,
|
||||
'chat_key' => $chatKey,
|
||||
'status' => 0,
|
||||
'attempts' => 1,
|
||||
'event_time' => max(0, $eventTime),
|
||||
'expire_time' => $expireTime,
|
||||
'next_retry' => 0,
|
||||
'error_message' => '',
|
||||
'raw_json' => $raw,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
return ['id' => $id, 'duplicate' => false];
|
||||
} catch (\Throwable $e) {
|
||||
$existing = Db::name('qywx_customer_acquisition_event')->where('event_key', $eventKey)->find();
|
||||
if (!$existing) {
|
||||
throw $e;
|
||||
}
|
||||
if ((int) ($existing['status'] ?? 0) === 1) {
|
||||
return ['id' => (int) $existing['id'], 'duplicate' => true];
|
||||
}
|
||||
if ((int) ($existing['status'] ?? 0) !== 2) {
|
||||
return ['id' => (int) $existing['id'], 'duplicate' => true];
|
||||
}
|
||||
$claimed = Db::name('qywx_customer_acquisition_event')
|
||||
->where('id', (int) $existing['id'])
|
||||
->where('status', 2)
|
||||
->update([
|
||||
'status' => 0,
|
||||
'attempts' => (int) ($existing['attempts'] ?? 0) + 1,
|
||||
'error_message' => '',
|
||||
'raw_json' => $raw,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
if ($claimed <= 0) {
|
||||
return ['id' => (int) $existing['id'], 'duplicate' => true];
|
||||
}
|
||||
|
||||
return ['id' => (int) $existing['id'], 'duplicate' => false];
|
||||
}
|
||||
}
|
||||
|
||||
private static function finishEvent(
|
||||
int $id,
|
||||
int $status,
|
||||
string $error = '',
|
||||
string $remoteLinkId = '',
|
||||
string $externalUserId = '',
|
||||
string $userId = ''
|
||||
): void {
|
||||
if ($id <= 0) {
|
||||
return;
|
||||
}
|
||||
Db::name('qywx_customer_acquisition_event')->where('id', $id)->update([
|
||||
'status' => $status,
|
||||
'link_id' => $remoteLinkId,
|
||||
'external_userid' => $externalUserId,
|
||||
'userid' => $userId,
|
||||
'error_message' => mb_substr($error, 0, 1000),
|
||||
'next_retry' => 0,
|
||||
// ChatKey 是短时敏感凭证,终态后不再保留;原始回调也随之清理。
|
||||
'chat_key' => '',
|
||||
'raw_json' => null,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
private static function scheduleRetryOrExpire(int $id, int $eventTime, string $error): void
|
||||
{
|
||||
if ($id <= 0) {
|
||||
return;
|
||||
}
|
||||
$now = time();
|
||||
$expireTime = (int) (Db::name('qywx_customer_acquisition_event')
|
||||
->where('id', $id)->value('expire_time') ?? 0);
|
||||
$decision = self::retryDecision($eventTime, $now, $expireTime);
|
||||
$expireTime = $decision['expire_time'];
|
||||
$expired = $decision['expired'];
|
||||
Db::name('qywx_customer_acquisition_event')->where('id', $id)->update([
|
||||
'status' => $expired ? 3 : 2,
|
||||
'expire_time' => $expireTime,
|
||||
'next_retry' => $decision['next_retry'],
|
||||
'error_message' => mb_substr(
|
||||
$expired ? 'failed_expired: ' . $error : $error,
|
||||
0,
|
||||
1000
|
||||
),
|
||||
'chat_key' => $expired ? '' : Db::raw('chat_key'),
|
||||
'raw_json' => $expired ? null : Db::raw('raw_json'),
|
||||
'update_time' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return 'created'|'updated' */
|
||||
private static function upsertCustomer(string $remoteLinkId, array $customer, bool $messageCountKnown): string
|
||||
{
|
||||
$externalUserId = trim((string) ($customer['external_userid'] ?? ''));
|
||||
$userId = trim((string) ($customer['userid'] ?? ''));
|
||||
if ($remoteLinkId === '' || $externalUserId === '' || $userId === '') {
|
||||
throw new RuntimeException('获客客户数据缺少 link_id / external_userid / userid');
|
||||
}
|
||||
[$ownerAdminId, $deptId] = self::resolveOwner($userId);
|
||||
$now = time();
|
||||
$existing = Db::name('qywx_customer_acquisition_customer')
|
||||
->where('link_id', $remoteLinkId)
|
||||
->where('external_userid', $externalUserId)
|
||||
->where('userid', $userId)
|
||||
->find();
|
||||
$snapshot = $customer['snapshot'] ?? $customer;
|
||||
$incomingChatStatus = max(0, min(2, (int) ($customer['chat_status'] ?? 0)));
|
||||
$data = [
|
||||
'promotion_link_id' => (int) (Db::name('qywx_promotion_link')
|
||||
->where('remote_link_id', $remoteLinkId)->value('id') ?? 0),
|
||||
'owner_admin_id' => $ownerAdminId,
|
||||
'dept_id' => $deptId,
|
||||
'state' => mb_substr((string) ($customer['state'] ?? ''), 0, 255),
|
||||
// 已确认发过消息后,列表同步返回的“未发/未知”不得把状态回退。
|
||||
'chat_status' => $existing
|
||||
? Db::raw('CASE WHEN chat_status = 1 OR ' . $incomingChatStatus . ' = 1 THEN 1 ELSE ' . $incomingChatStatus . ' END')
|
||||
: $incomingChatStatus,
|
||||
'last_sync_time' => $now,
|
||||
'raw_snapshot' => self::encodeJson($snapshot),
|
||||
'update_time' => $now,
|
||||
];
|
||||
if ($messageCountKnown) {
|
||||
$remoteCount = max(0, (int) ($customer['recv_msg_cnt'] ?? 0));
|
||||
// get_chat_info 返回累计值,必须 max/覆盖,绝不按回调次数累加。
|
||||
$data['recv_msg_cnt'] = $existing
|
||||
? Db::raw('GREATEST(recv_msg_cnt,' . $remoteCount . ')')
|
||||
: $remoteCount;
|
||||
$data['message_count_known'] = 1;
|
||||
}
|
||||
if ($incomingChatStatus === 1 || $messageCountKnown) {
|
||||
$eventTime = max(0, (int) ($customer['event_time'] ?? $now));
|
||||
$data['last_chat_time'] = $existing
|
||||
? Db::raw('GREATEST(last_chat_time,' . $eventTime . ')')
|
||||
: $eventTime;
|
||||
}
|
||||
if ($existing) {
|
||||
Db::name('qywx_customer_acquisition_customer')->where('id', (int) $existing['id'])->update($data);
|
||||
|
||||
return 'updated';
|
||||
}
|
||||
$data += [
|
||||
'link_id' => $remoteLinkId,
|
||||
'external_userid' => $externalUserId,
|
||||
'userid' => $userId,
|
||||
'recv_msg_cnt' => $messageCountKnown ? max(0, (int) ($customer['recv_msg_cnt'] ?? 0)) : 0,
|
||||
'message_count_known' => $messageCountKnown ? 1 : 0,
|
||||
'first_acquired_time' => max(0, (int) ($customer['create_time'] ?? $customer['event_time'] ?? $now)),
|
||||
'last_chat_time' => ($incomingChatStatus === 1 || $messageCountKnown)
|
||||
? max(0, (int) ($customer['event_time'] ?? $now))
|
||||
: 0,
|
||||
'create_time' => $now,
|
||||
];
|
||||
Db::name('qywx_customer_acquisition_customer')->insert($data);
|
||||
|
||||
return 'created';
|
||||
}
|
||||
|
||||
/** @return array{0:int,1:int} */
|
||||
private static function resolveOwner(string $userId): array
|
||||
{
|
||||
$adminId = (int) (Db::name('admin')->where('work_wechat_userid', $userId)
|
||||
->whereNull('delete_time')->value('id') ?? 0);
|
||||
if ($adminId <= 0) {
|
||||
return [0, 0];
|
||||
}
|
||||
$deptId = (int) (Db::name('admin_dept')->where('admin_id', $adminId)
|
||||
->order('dept_id', 'asc')->value('dept_id') ?? 0);
|
||||
|
||||
return [$adminId, $deptId];
|
||||
}
|
||||
|
||||
private static function encodeJson(mixed $value): string
|
||||
{
|
||||
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return $json === false ? '{}' : $json;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\qywx;
|
||||
|
||||
/** 企业微信获客助手链接校验。 */
|
||||
class QywxCustomerAcquisitionLinkService
|
||||
{
|
||||
private const HOST = 'work.weixin.qq.com';
|
||||
|
||||
/**
|
||||
* 只接受企业微信获客助手生成的 https://work.weixin.qq.com/ca/... 链接。
|
||||
*/
|
||||
public static function isAllowed(string $url, bool $allowEmpty = false): bool
|
||||
{
|
||||
$url = trim($url);
|
||||
if ($url === '') {
|
||||
return $allowEmpty;
|
||||
}
|
||||
|
||||
$parts = parse_url($url);
|
||||
if (!is_array($parts)
|
||||
|| strtolower((string) ($parts['scheme'] ?? '')) !== 'https'
|
||||
|| strtolower((string) ($parts['host'] ?? '')) !== self::HOST
|
||||
|| isset($parts['user'])
|
||||
|| isset($parts['pass'])
|
||||
|| (isset($parts['port']) && (int) $parts['port'] !== 443)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = (string) ($parts['path'] ?? '');
|
||||
|
||||
return preg_match('#^/ca/[A-Za-z0-9_-]+/?$#', $path) === 1;
|
||||
}
|
||||
|
||||
public static function example(): string
|
||||
{
|
||||
return 'https://work.weixin.qq.com/ca/xxxxxxxx';
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace app\common\service\qywx;
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
/** 公开推广链接分流:按权重随机,并在事务内维护当日限额与点击计数。 */
|
||||
/** 公开获客助手链接分流:按权重随机,并在事务内维护当日限额与点击计数。 */
|
||||
class QywxPromotionRedirectService
|
||||
{
|
||||
/** @return array{url:string,link_id:int}|null */
|
||||
@@ -30,11 +30,9 @@ class QywxPromotionRedirectService
|
||||
$now = time();
|
||||
$today = date('Y-m-d', $now);
|
||||
$links = Db::name('qywx_promotion_link')->alias('l')
|
||||
->leftJoin('qywx_promotion_account a', 'a.id = l.account_id AND a.delete_time IS NULL')
|
||||
->where('l.pool_id', (int) $pool['id'])
|
||||
->where('l.status', 1)
|
||||
->whereNull('l.delete_time')
|
||||
->whereRaw('(l.account_id = 0 OR a.auth_status = 1)')
|
||||
->whereRaw('(l.active_start = 0 OR l.active_start <= ' . $now . ')')
|
||||
->whereRaw('(l.active_end = 0 OR l.active_end >= ' . $now . ')')
|
||||
->whereRaw("(l.daily_limit = 0 OR l.today_date IS NULL OR l.today_date <> '" . addslashes($today) . "' OR l.today_count < l.daily_limit)")
|
||||
@@ -43,10 +41,16 @@ class QywxPromotionRedirectService
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
// 兼容历史数据:旧的普通外链或客户群链接即使仍在库中,也不能参与分流。
|
||||
$links = array_values(array_filter(
|
||||
$links,
|
||||
static fn (array $link): bool => QywxCustomerAcquisitionLinkService::isAllowed((string) ($link['wecom_url'] ?? ''))
|
||||
));
|
||||
|
||||
$selected = self::weightedRandom($links);
|
||||
if (!$selected) {
|
||||
$fallback = trim((string) ($pool['fallback_url'] ?? ''));
|
||||
if (QywxPromotionOpenWorkService::isAllowedPromotionUrl($fallback, true) && $fallback !== '') {
|
||||
if (QywxCustomerAcquisitionLinkService::isAllowed($fallback, true) && $fallback !== '') {
|
||||
return ['url' => $fallback, 'link_id' => 0];
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ return [
|
||||
'qywx:scan-media-channel' => 'app\\command\\QywxScanMediaChannel',
|
||||
// 企业微信会话内容存档同步(需开通会话存档 License 并配置 msgaudit_* 相关项 + 动态库)
|
||||
'qywx:sync-msg-archive' => 'app\\command\\QywxSyncMsgArchive',
|
||||
// 获客助手 ChatKey 仅 30 分钟有效,部署时应每分钟执行一次
|
||||
'qywx:retry-customer-acquisition-events' => 'app\\command\\QywxRetryCustomerAcquisitionEvents',
|
||||
// 甘草订单物流路由同步(GET_TASK_ROUTE_LIST)
|
||||
'gancao:sync-logistics' => 'app\\command\\GancaoSyncLogisticsRoute',
|
||||
'ej-pharmacy:sync-catalog' => 'app\\command\\EjPharmacySyncCatalog',
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
// 企业微信获客助手“可调用应用”的 CorpID 与应用 Secret。
|
||||
// 优先使用语义明确的独立配置,兼容复用现有内部应用 Secret。
|
||||
'corp_id' => trim((string) (env('WECHAT_WORK_CORP_ID', '') ?: env('work_wechat.corp_id', ''))),
|
||||
'secret' => trim((string) (
|
||||
env('WECHAT_WORK_CUSTOMER_ACQUISITION_SECRET', '')
|
||||
?: env('work_wechat.customer_acquisition_secret', '')
|
||||
?: env('WECHAT_WORK_AGENT_SECRET', '')
|
||||
?: env('work_wechat.agent_secret', '')
|
||||
?: env('work_wechat.secret', '')
|
||||
)),
|
||||
'base_uri' => 'https://qyapi.weixin.qq.com',
|
||||
'timeout' => 20,
|
||||
// Windows PHP 常未配置系统 CA;使用项目内 CA 文件,仍保持严格证书校验。
|
||||
'verify' => is_file(dirname(__DIR__) . '/cacert.pem') ? dirname(__DIR__) . '/cacert.pem' : true,
|
||||
];
|
||||
@@ -1,37 +1,48 @@
|
||||
# 企业微信推广助手配置
|
||||
# 企业微信获客助手配置
|
||||
|
||||
管理端菜单:`一诊 / 企业微信推广助手`
|
||||
管理端菜单:`一诊 / 企业微信获客助手`
|
||||
|
||||
该功能使用企业微信“服务商第三方应用”授权模式。服务商参数只保存在服务器环境变量中,企业永久授权码会使用 AES-256-GCM 加密后存入数据库。
|
||||
该功能使用当前企业的内部自建应用配置,不使用服务商第三方应用,也不需要 SuiteID、suite_ticket、永久授权码或企业扫码安装。
|
||||
|
||||
在 `server/.env` 中增加:
|
||||
系统直接复用 `server/.env` 已有配置:
|
||||
|
||||
```ini
|
||||
[qywx_promotion]
|
||||
ENABLED = true
|
||||
PROVIDER_CORP_ID = "服务商企业 CorpID"
|
||||
SUITE_ID = "第三方应用 SuiteID"
|
||||
SUITE_SECRET = "第三方应用 SuiteSecret"
|
||||
TOKEN = "应用指令回调 Token"
|
||||
AES_KEY = "应用指令回调 EncodingAESKey"
|
||||
CREDENTIAL_KEY = "至少32字节的独立随机密钥"
|
||||
REDIRECT_URI = "https://你的域名/api/qywx-promotion/auth/callback"
|
||||
ADMIN_RETURN_URL = "https://你的域名/admin/first_visit/wecom_promotion"
|
||||
[work_wechat]
|
||||
CORP_ID = "当前企业 CorpID"
|
||||
AGENT_ID = "内部自建应用 AgentID"
|
||||
CUSTOMER_ACQUISITION_SECRET = "获客助手可调用应用的 Secret"
|
||||
```
|
||||
|
||||
企业微信服务商后台需要配置:
|
||||
兼容已有项目:没有 `CUSTOMER_ACQUISITION_SECRET` 时,会依次回退读取 `AGENT_SECRET`、`SECRET`。如果现有 `SECRET` 就是获客助手中配置的“可调用应用”Secret,无需重复配置。
|
||||
|
||||
- 应用指令回调 URL:`https://你的域名/api/qywx-promotion/provider/callback`
|
||||
- 授权完成回调域名:与你的 `REDIRECT_URI` 域名一致
|
||||
企业微信管理后台还需完成三项外部配置:开通获客助手、将该内部应用设置为获客助手可调用应用、将接口服务器公网 IP 加入可信 IP。页面“验证获客助手 API”会通过只读列表接口检查这些条件。
|
||||
|
||||
保存配置并让企业微信成功推送一次 `suite_ticket` 后,管理端的“发起企业授权”按钮才会生成安装链接。
|
||||
## 官方 API 对接范围
|
||||
|
||||
推广链接默认仅允许企业微信官方域名。如需跳转企业自有的可信中间页,可在环境变量中追加:
|
||||
按[企业微信获客链接管理文档](https://developer.work.weixin.qq.com/document/path/97297)完成以下五个接口:
|
||||
|
||||
```ini
|
||||
ALLOWED_LINK_HOSTS = "promo.example.com,crm.example.com"
|
||||
- 获取获客链接列表 `list_link`
|
||||
- 获取获客链接详情 `get`
|
||||
- 创建获客链接 `create_link`
|
||||
- 更新获客链接 `update_link`
|
||||
- 删除获客链接 `delete_link`
|
||||
|
||||
“永久删除企业微信链接”会调用官方删除接口且无法恢复;“从本地移除”只退出当前分流池,不会修改企业微信后台。
|
||||
|
||||
获客成员来自后台管理员的 `work_wechat_userid`。管理员可管理全量;组长、医助等账号只返回 `DataScopeService` 当前角色与部门范围内的成员。同步远端链接时,非全量账号只导入 `range.user_list` 与其可见成员有交集的数据;企业微信部门 ID 尚未建立本地映射时按安全原则隐藏,不会越权放行。
|
||||
|
||||
`list_link` 只返回当前获客助手可调用应用通过 API 创建的官方链接。后台历史手工粘贴的 `work.weixin.qq.com/ca/...` 链接,以及其他应用创建的链接,不会出现在当前应用的同步列表中,也无法仅凭 URL 反查为官方 `link_id`。需要官方客户、统计和消息归因时,应在本页面使用“创建官方获客链接”。
|
||||
|
||||
链接分流只接受企业微信获客助手生成的链接:
|
||||
|
||||
```text
|
||||
https://work.weixin.qq.com/ca/xxxxxxxx
|
||||
```
|
||||
|
||||
“联系我”、客户群、自有网页或其他外部链接均会被拒绝;已有的非获客助手历史链接也不会参与随机分流。Secret 与 access_token 不会返回到浏览器,也不会写入接口错误日志。
|
||||
|
||||
如果需要为点击 IP 生成不可逆服务端哈希,可在 `[qywx_promotion]` 下额外设置独立的 `CREDENTIAL_KEY`。
|
||||
|
||||
公开 JS 示例:
|
||||
|
||||
```html
|
||||
@@ -39,4 +50,4 @@ ALLOWED_LINK_HOSTS = "promo.example.com,crm.example.com"
|
||||
<a href="#" data-wecom-promotion="分流方案KEY">添加企业微信</a>
|
||||
```
|
||||
|
||||
随机分流在服务端完成。候选链接必须同时满足:方案启用、链接上线、授权企业有效、处于有效时间段、未超过当日上限。权重越大,被选中的概率越高。
|
||||
随机分流在服务端完成。候选链接必须同时满足:方案启用、链接上线、处于有效时间段、未超过当日上限。权重越大,被选中的概率越高。
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import t from"./error-C_WBAqz-.js";import{o,q as r,r as a,v as n,D as c,s}from"./.pnpm-CJoARskO.js";import"./index-vwMTrNXz.js";const p="/admin/assets/no_perms-jDxcYpYC.png",i={class:"error404"},x=o({__name:"403",setup(m){return(_,e)=>(r(),a("div",i,[n(t,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:c(()=>[...e[0]||(e[0]=[s("div",{class:"flex justify-center"},[s("img",{class:"w-[150px] h-[150px]",src:p,alt:""})],-1)])]),_:1})]))}});export{x as default};
|
||||
import t from"./error-DNemHh_Y.js";import{o,q as r,r as a,v as n,D as c,s}from"./.pnpm-BadRMC3e.js";import"./index-Do7fAtr3.js";const p="/admin/assets/no_perms-jDxcYpYC.png",i={class:"error404"},x=o({__name:"403",setup(m){return(_,e)=>(r(),a("div",i,[n(t,{code:"403",title:"您的账号权限不足,请联系管理员添加权限!","show-btn":!1},{content:c(()=>[...e[0]||(e[0]=[s("div",{class:"flex justify-center"},[s("img",{class:"w-[150px] h-[150px]",src:p,alt:""})],-1)])]),_:1})]))}});export{x as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import e from"./error-C_WBAqz-.js";import{o,q as r,r as t,v as s}from"./.pnpm-CJoARskO.js";import"./index-vwMTrNXz.js";const a={class:"error404"},d=o({__name:"404",setup(c){return(n,_)=>(r(),t("div",a,[s(e,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{d as default};
|
||||
import e from"./error-DNemHh_Y.js";import{o,q as r,r as t,v as s}from"./.pnpm-BadRMC3e.js";import"./index-Do7fAtr3.js";const a={class:"error404"},d=o({__name:"404",setup(c){return(n,_)=>(r(),t("div",a,[s(e,{code:"404",title:"哎呀,出错了!您访问的页面不存在…"})]))}});export{d as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{o as C,R as D,q as d,r as I,ac as N,O as u,bj as B,D as o,v as l,bk as E,br as L,L as i,T as r,s as p,bi as R,M as w}from"./.pnpm-CJoARskO.js";import{a as V}from"./doctor-htXSGAHr.js";import{m as A,_ as M}from"./index-vwMTrNXz.js";const P={class:"appointment-record-panel"},$={class:"cell-stack"},j={class:"font-medium"},q={class:"text-gray-500 text-sm"},O={class:"cell-stack"},F={class:"text-gray-500 text-sm"},G=C({__name:"AppointmentRecordPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const f=v,m=w(!1),g=w([]),h=w({});function x(t){return t?String(t).length>=8?String(t).slice(0,5):t:""}function k(t){return t==="morning"?"上午":t==="afternoon"?"下午":t==="all"?"全天":t||"—"}function S(t){const n=t.channel_source??t.channels??"";if(n===""||n===null||n===void 0)return"—";const a=String(n),s=h.value[a]||a,c=String(t.channel_source_detail??"").trim();return c!==""?`${s}(${c})`:s}function T(t){return[...t].sort((n,a)=>{const s=String(n.appointment_date||""),c=String(a.appointment_date||"");if(s!==c)return c.localeCompare(s);const _=String(n.appointment_time||""),e=String(a.appointment_time||"");return _!==e?e.localeCompare(_):Number(a.id||0)-Number(n.id||0)})}const z=async()=>{try{const t=await A({type:"channels"}),n=((t==null?void 0:t.channels)||[]).filter(s=>s.status!==0),a={};for(const s of n)s.value!=null&&(a[String(s.value)]=s.name||String(s.value));h.value=a}catch{h.value={}}},b=async()=>{if(f.diagnosisId){m.value=!0;try{await z();const t=await V({patient_id:f.diagnosisId,diag_scope_relax:1,page_no:1,page_size:500}),n=(t==null?void 0:t.lists)||[];g.value=T(n)}catch(t){console.error(t),g.value=[]}finally{m.value=!1}}};return D(()=>f.diagnosisId,()=>{b()},{immediate:!0}),y({refresh:b}),(t,n)=>{const a=E,s=L,c=B,_=R;return d(),I("div",P,[N((d(),u(c,{data:g.value,border:"",stripe:"","empty-text":"暂无挂号记录"},{default:o(()=>[l(a,{label:"ID",prop:"id",width:"72",align:"center"}),l(a,{label:"状态",width:"100",align:"center"},{default:o(({row:e})=>[l(s,{type:e.status===1?"success":e.status===2?"info":e.status===3?"primary":"danger",size:"small",effect:"light"},{default:o(()=>[i(r(e.status_desc||"—"),1)]),_:2},1032,["type"])]),_:1}),l(a,{label:"患者(挂号人)","min-width":"150"},{default:o(({row:e})=>[p("div",$,[p("span",j,r(e.patient_name||"—"),1),p("span",q,r(e.patient_phone||""),1)])]),_:1}),l(a,{label:"挂号医生",prop:"doctor_name",width:"110","show-overflow-tooltip":""}),l(a,{label:"挂号助理",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.assistant_name||"—"),1)]),_:1}),l(a,{label:"预约时间","min-width":"130"},{default:o(({row:e})=>[p("div",O,[p("span",null,r(e.appointment_date||"—"),1),p("span",F,r(x(e.appointment_time)),1)])]),_:1}),l(a,{label:"时段",width:"80",align:"center"},{default:o(({row:e})=>[i(r(k(e.period)),1)]),_:1}),l(a,{label:"类型",width:"100","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.appointment_type_desc||"—"),1)]),_:1}),l(a,{label:"渠道",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(S(e)),1)]),_:1}),l(a,{label:"确认诊单",width:"92",align:"center"},{default:o(({row:e})=>[e.diagnosis_confirmed?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[0]||(n[0]=[i("已确认",-1)])]),_:1})):(d(),u(s,{key:1,type:"warning",size:"small",effect:"plain"},{default:o(()=>[...n[1]||(n[1]=[i("未确认",-1)])]),_:1}))]),_:1}),l(a,{label:"开方",width:"80",align:"center"},{default:o(({row:e})=>[e.has_prescription?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[2]||(n[2]=[i("已开方",-1)])]),_:1})):(d(),u(s,{key:1,type:"info",size:"small",effect:"plain"},{default:o(()=>[...n[3]||(n[3]=[i("未开方",-1)])]),_:1}))]),_:1}),l(a,{label:"备注",prop:"remark","min-width":"100","show-overflow-tooltip":""}),l(a,{label:"创建时间",width:"165",prop:"create_time"})]),_:1},8,["data"])),[[_,m.value]])])}}}),Q=M(G,[["__scopeId","data-v-4de87dfa"]]);export{Q as default};
|
||||
import{o as C,R as D,q as d,r as I,ac as N,O as u,bj as B,D as o,v as l,bk as E,br as L,L as i,T as r,s as p,bi as R,M as w}from"./.pnpm-BadRMC3e.js";import{a as V}from"./doctor-DpsaODfP.js";import{m as A,_ as M}from"./index-Do7fAtr3.js";const P={class:"appointment-record-panel"},$={class:"cell-stack"},j={class:"font-medium"},q={class:"text-gray-500 text-sm"},O={class:"cell-stack"},F={class:"text-gray-500 text-sm"},G=C({__name:"AppointmentRecordPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const f=v,m=w(!1),g=w([]),h=w({});function x(t){return t?String(t).length>=8?String(t).slice(0,5):t:""}function k(t){return t==="morning"?"上午":t==="afternoon"?"下午":t==="all"?"全天":t||"—"}function S(t){const n=t.channel_source??t.channels??"";if(n===""||n===null||n===void 0)return"—";const a=String(n),s=h.value[a]||a,c=String(t.channel_source_detail??"").trim();return c!==""?`${s}(${c})`:s}function T(t){return[...t].sort((n,a)=>{const s=String(n.appointment_date||""),c=String(a.appointment_date||"");if(s!==c)return c.localeCompare(s);const _=String(n.appointment_time||""),e=String(a.appointment_time||"");return _!==e?e.localeCompare(_):Number(a.id||0)-Number(n.id||0)})}const z=async()=>{try{const t=await A({type:"channels"}),n=((t==null?void 0:t.channels)||[]).filter(s=>s.status!==0),a={};for(const s of n)s.value!=null&&(a[String(s.value)]=s.name||String(s.value));h.value=a}catch{h.value={}}},b=async()=>{if(f.diagnosisId){m.value=!0;try{await z();const t=await V({patient_id:f.diagnosisId,diag_scope_relax:1,page_no:1,page_size:500}),n=(t==null?void 0:t.lists)||[];g.value=T(n)}catch(t){console.error(t),g.value=[]}finally{m.value=!1}}};return D(()=>f.diagnosisId,()=>{b()},{immediate:!0}),y({refresh:b}),(t,n)=>{const a=E,s=L,c=B,_=R;return d(),I("div",P,[N((d(),u(c,{data:g.value,border:"",stripe:"","empty-text":"暂无挂号记录"},{default:o(()=>[l(a,{label:"ID",prop:"id",width:"72",align:"center"}),l(a,{label:"状态",width:"100",align:"center"},{default:o(({row:e})=>[l(s,{type:e.status===1?"success":e.status===2?"info":e.status===3?"primary":"danger",size:"small",effect:"light"},{default:o(()=>[i(r(e.status_desc||"—"),1)]),_:2},1032,["type"])]),_:1}),l(a,{label:"患者(挂号人)","min-width":"150"},{default:o(({row:e})=>[p("div",$,[p("span",j,r(e.patient_name||"—"),1),p("span",q,r(e.patient_phone||""),1)])]),_:1}),l(a,{label:"挂号医生",prop:"doctor_name",width:"110","show-overflow-tooltip":""}),l(a,{label:"挂号助理",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.assistant_name||"—"),1)]),_:1}),l(a,{label:"预约时间","min-width":"130"},{default:o(({row:e})=>[p("div",O,[p("span",null,r(e.appointment_date||"—"),1),p("span",F,r(x(e.appointment_time)),1)])]),_:1}),l(a,{label:"时段",width:"80",align:"center"},{default:o(({row:e})=>[i(r(k(e.period)),1)]),_:1}),l(a,{label:"类型",width:"100","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(e.appointment_type_desc||"—"),1)]),_:1}),l(a,{label:"渠道",width:"110","show-overflow-tooltip":""},{default:o(({row:e})=>[i(r(S(e)),1)]),_:1}),l(a,{label:"确认诊单",width:"92",align:"center"},{default:o(({row:e})=>[e.diagnosis_confirmed?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[0]||(n[0]=[i("已确认",-1)])]),_:1})):(d(),u(s,{key:1,type:"warning",size:"small",effect:"plain"},{default:o(()=>[...n[1]||(n[1]=[i("未确认",-1)])]),_:1}))]),_:1}),l(a,{label:"开方",width:"80",align:"center"},{default:o(({row:e})=>[e.has_prescription?(d(),u(s,{key:0,type:"success",size:"small",effect:"plain"},{default:o(()=>[...n[2]||(n[2]=[i("已开方",-1)])]),_:1})):(d(),u(s,{key:1,type:"info",size:"small",effect:"plain"},{default:o(()=>[...n[3]||(n[3]=[i("未开方",-1)])]),_:1}))]),_:1}),l(a,{label:"备注",prop:"remark","min-width":"100","show-overflow-tooltip":""}),l(a,{label:"创建时间",width:"165",prop:"create_time"})]),_:1},8,["data"])),[[_,m.value]])])}}}),Q=M(G,[["__scopeId","data-v-4de87dfa"]]);export{Q as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as T,R as C,q as _,r as b,ac as D,O as h,bj as $,D as r,v as n,bk as L,L as c,T as d,br as k,s as E,a1 as A,w as P,u as B,ch as F,bi as M,M as w}from"./.pnpm-CJoARskO.js";import{aa as V}from"./tcm-BBytqd3B.js";import{_ as q}from"./index-vwMTrNXz.js";const K={class:"assign-log-panel"},j={key:1,class:"text-gray-400"},z=T({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const u=v,m=w(!1),p=w([]);function N(a){const e=a.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(a){const e=a.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const s=new Date(t*1e3);if(Number.isNaN(s.getTime()))return"—";const o=l=>String(l).padStart(2,"0");return`${s.getFullYear()}-${o(s.getMonth()+1)}-${o(s.getDate())} ${o(s.getHours())}:${o(s.getMinutes())}:${o(s.getSeconds())}`}function f(a,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",s=e==="from"?"from_assistant_id":"to_assistant_id",o=a[t];if(o!=null&&String(o).trim()!==""&&String(o)!=="—")return String(o);const l=Number(a[s]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(u.diagnosisId){m.value=!0;try{const a=await V({id:u.diagnosisId}),e=Array.isArray(a)?a:[];p.value=e}catch(a){console.error(a),p.value=[]}finally{m.value=!1}}};return C(()=>u.diagnosisId,()=>{g()},{immediate:!0}),y({refresh:g}),(a,e)=>{const t=L,s=k,o=P,l=A,S=$,I=M;return _(),b("div",K,[D((_(),h(S,{data:p.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:r(()=>[n(t,{label:"操作时间",width:"175",prop:"create_time_text"}),n(t,{label:"原医助","min-width":"120"},{default:r(({row:i})=>[c(d(f(i,"from")),1)]),_:1}),n(t,{label:"新医助","min-width":"120"},{default:r(({row:i})=>[c(d(f(i,"to")),1)]),_:1}),n(t,{label:"继承",width:"72",align:"center"},{default:r(({row:i})=>[Number(i.is_inherit)===1?(_(),h(s,{key:0,type:"success",size:"small"},{default:r(()=>[...e[0]||(e[0]=[c("是",-1)])]),_:1})):(_(),b("span",j,"否"))]),_:1}),n(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:r(({row:i})=>[c(d(N(i)),1)]),_:1}),n(t,{label:"快照·业务单创建时间",width:"190"},{header:r(()=>[e[1]||(e[1]=E("span",null,"快照·业务单创建时间",-1)),n(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:r(()=>[n(o,{class:"assign-log-col-hint"},{default:r(()=>[n(B(F))]),_:1})]),_:1})]),default:r(({row:i})=>[c(d(x(i)),1)]),_:1}),n(t,{label:"操作人",width:"110",prop:"operator_name"}),n(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),n(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,m.value]])])}}}),Y=q(z,[["__scopeId","data-v-f670e3e6"]]);export{Y as default};
|
||||
import{o as T,R as C,q as _,r as b,ac as D,O as h,bj as $,D as r,v as n,bk as L,L as c,T as d,br as k,s as E,a1 as A,w as P,u as B,ch as F,bi as M,M as w}from"./.pnpm-BadRMC3e.js";import{aa as V}from"./tcm-BDu8-eLA.js";import{_ as q}from"./index-Do7fAtr3.js";const K={class:"assign-log-panel"},j={key:1,class:"text-gray-400"},z=T({__name:"AssignLogPanel",props:{diagnosisId:{}},setup(v,{expose:y}){const u=v,m=w(!1),p=w([]);function N(a){const e=a.related_po_creator_name;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_creator_id);return Number.isFinite(t)&&t>0?`ID:${t}`:"—"}function x(a){const e=a.related_po_create_time_text;if(e!=null&&String(e).trim()!=="")return String(e);const t=Number(a.related_po_create_time);if(!Number.isFinite(t)||t<=0)return"—";const s=new Date(t*1e3);if(Number.isNaN(s.getTime()))return"—";const o=l=>String(l).padStart(2,"0");return`${s.getFullYear()}-${o(s.getMonth()+1)}-${o(s.getDate())} ${o(s.getHours())}:${o(s.getMinutes())}:${o(s.getSeconds())}`}function f(a,e){const t=e==="from"?"from_assistant_name":"to_assistant_name",s=e==="from"?"from_assistant_id":"to_assistant_id",o=a[t];if(o!=null&&String(o).trim()!==""&&String(o)!=="—")return String(o);const l=Number(a[s]);return Number.isFinite(l)&&l>0?`ID:${l}`:"—"}const g=async()=>{if(u.diagnosisId){m.value=!0;try{const a=await V({id:u.diagnosisId}),e=Array.isArray(a)?a:[];p.value=e}catch(a){console.error(a),p.value=[]}finally{m.value=!1}}};return C(()=>u.diagnosisId,()=>{g()},{immediate:!0}),y({refresh:g}),(a,e)=>{const t=L,s=k,o=P,l=A,S=$,I=M;return _(),b("div",K,[D((_(),h(S,{data:p.value,border:"",stripe:"","empty-text":"暂无指派记录"},{default:r(()=>[n(t,{label:"操作时间",width:"175",prop:"create_time_text"}),n(t,{label:"原医助","min-width":"120"},{default:r(({row:i})=>[c(d(f(i,"from")),1)]),_:1}),n(t,{label:"新医助","min-width":"120"},{default:r(({row:i})=>[c(d(f(i,"to")),1)]),_:1}),n(t,{label:"继承",width:"72",align:"center"},{default:r(({row:i})=>[Number(i.is_inherit)===1?(_(),h(s,{key:0,type:"success",size:"small"},{default:r(()=>[...e[0]||(e[0]=[c("是",-1)])]),_:1})):(_(),b("span",j,"否"))]),_:1}),n(t,{label:"快照·业务单创建人","min-width":"130","show-overflow-tooltip":""},{default:r(({row:i})=>[c(d(N(i)),1)]),_:1}),n(t,{label:"快照·业务单创建时间",width:"190"},{header:r(()=>[e[1]||(e[1]=E("span",null,"快照·业务单创建时间",-1)),n(l,{placement:"top",content:"related_po_create_time:触发本次操作的处方业务订单 create_time;与原文助一致时表示医助创建订单时间"},{default:r(()=>[n(o,{class:"assign-log-col-hint"},{default:r(()=>[n(B(F))]),_:1})]),_:1})]),default:r(({row:i})=>[c(d(x(i)),1)]),_:1}),n(t,{label:"操作人",width:"110",prop:"operator_name"}),n(t,{label:"操作账号",width:"120",prop:"operator_account","show-overflow-tooltip":""}),n(t,{label:"IP",width:"130",prop:"ip","show-overflow-tooltip":""})]),_:1},8,["data"])),[[I,m.value]])])}}}),Y=q(z,[["__scopeId","data-v-f670e3e6"]]);export{Y as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as B,R as D,q as E,O,D as I,r as _,T as S,P as x,ac as W,s as h,ad as P,v as U,K as $,L as j,bt as H,p as K,M as v,de as c}from"./.pnpm-CJoARskO.js";import{ab as Y}from"./tcm-BBytqd3B.js";import{_ as q}from"./index-vwMTrNXz.js";const z={key:0,class:"watch-state"},F={key:1,class:"watch-state watch-error"},G=B({__name:"AssistantWatchCallDialog",props:{modelValue:{type:Boolean},diagnosisId:{}},emits:["update:modelValue","closed"],setup(R,{emit:A}){const u=R,g=A,y=K({get:()=>u.modelValue,set:t=>g("update:modelValue",t)}),r=v(null),l=v(!1),i=v(""),f=v("旁观视频通话"),n=new Map;let a=null,p=0;function w(t,e){return`${t}\0${String(e)}`}function N(t){return t.startsWith("patient_")?"患者":t.startsWith("doctor_")?"医护":t}async function T(t){if(!a||!r.value||t.streamType!==c.TYPE.STREAM_TYPE_MAIN)return;const e=w(t.userId,t.streamType);if(n.has(e))return;const o=document.createElement("div");o.className="watch-tile";const s=document.createElement("div");s.className="watch-tile-cap",s.textContent=N(t.userId);const d=document.createElement("div");d.className="watch-tile-view",o.appendChild(s),o.appendChild(d),r.value.appendChild(o),n.set(e,{wrap:o,userId:t.userId,streamType:t.streamType});try{await a.startRemoteVideo({userId:t.userId,streamType:t.streamType,view:d})}catch(M){console.warn("[AssistantWatchCall] startRemoteVideo",M)}}async function V(t){if(!a)return;const e=w(t.userId,t.streamType),o=n.get(e);if(o){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}o.wrap.remove(),n.delete(e)}}function C(){a&&(a.on(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.on(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}function k(){a&&(a.off(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.off(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}async function m(){if(k(),a){for(const[,t]of n){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}t.wrap.remove()}n.clear(),r.value&&(r.value.innerHTML="");try{await a.exitRoom()}catch{}try{a.destroy()}catch{}a=null}else n.clear(),r.value&&(r.value.innerHTML="")}async function L(){const t=++p;if(await m(),!u.diagnosisId){i.value="诊单无效";return}l.value=!0,i.value="",f.value="旁观视频通话";try{const e=await Y({diagnosis_id:u.diagnosisId});if(t!==p)return;e.patientName&&(f.value=`旁观视频通话 · ${e.patientName}`),a=c.create(),C();const o={sdkAppId:e.sdkAppId,userId:e.userId,userSig:e.userSig,autoReceiveAudio:!0,autoReceiveVideo:!0,...e.roomId!=null&&e.roomId>0?{roomId:e.roomId}:{strRoomId:e.strRoomId}};if(!(e.roomId!=null&&e.roomId>0)&&!e.strRoomId)throw new Error("缺少房间号");if(await a.enterRoom(o),t!==p){await m();return}l.value=!1}catch(e){l.value=!1;let o="进入房间失败";if(typeof e=="string")o=e;else if(e&&typeof e=="object"){const s=e;s.msg?o=String(s.msg):s.message&&(o=String(s.message))}i.value=o,await m()}}function b(){m(),l.value=!1,i.value="",f.value="旁观视频通话",g("closed")}return D(()=>[u.modelValue,u.diagnosisId],([t,e])=>{if(!t){p++,m();return}e>0&&L()}),(t,e)=>{const o=$,s=H;return E(),O(s,{modelValue:y.value,"onUpdate:modelValue":e[1]||(e[1]=d=>y.value=d),title:f.value,width:"760px","destroy-on-close":"","append-to-body":"","close-on-click-modal":!1,class:"assistant-watch-call-dialog",onClosed:b},{footer:I(()=>[e[3]||(e[3]=h("span",{class:"watch-hint"},"仅观看,不会开启摄像头与麦克风",-1)),U(o,{type:"primary",onClick:e[0]||(e[0]=d=>y.value=!1)},{default:I(()=>[...e[2]||(e[2]=[j("离开",-1)])]),_:1})]),default:I(()=>[l.value?(E(),_("div",z,"正在连接房间…")):i.value?(E(),_("div",F,S(i.value),1)):x("",!0),W(h("div",{ref_key:"gridRef",ref:r,class:"watch-grid"},null,512),[[P,!l.value&&!i.value]])]),_:1},8,["modelValue","title"])}}}),Z=q(G,[["__scopeId","data-v-7aff665d"]]);export{Z as default};
|
||||
import{o as B,R as D,q as E,O,D as I,r as _,T as S,P as x,ac as W,s as h,ad as P,v as U,K as $,L as j,bt as H,p as K,M as v,de as c}from"./.pnpm-BadRMC3e.js";import{ab as Y}from"./tcm-BDu8-eLA.js";import{_ as q}from"./index-Do7fAtr3.js";const z={key:0,class:"watch-state"},F={key:1,class:"watch-state watch-error"},G=B({__name:"AssistantWatchCallDialog",props:{modelValue:{type:Boolean},diagnosisId:{}},emits:["update:modelValue","closed"],setup(R,{emit:A}){const u=R,g=A,y=K({get:()=>u.modelValue,set:t=>g("update:modelValue",t)}),r=v(null),l=v(!1),i=v(""),f=v("旁观视频通话"),n=new Map;let a=null,p=0;function w(t,e){return`${t}\0${String(e)}`}function N(t){return t.startsWith("patient_")?"患者":t.startsWith("doctor_")?"医护":t}async function T(t){if(!a||!r.value||t.streamType!==c.TYPE.STREAM_TYPE_MAIN)return;const e=w(t.userId,t.streamType);if(n.has(e))return;const o=document.createElement("div");o.className="watch-tile";const s=document.createElement("div");s.className="watch-tile-cap",s.textContent=N(t.userId);const d=document.createElement("div");d.className="watch-tile-view",o.appendChild(s),o.appendChild(d),r.value.appendChild(o),n.set(e,{wrap:o,userId:t.userId,streamType:t.streamType});try{await a.startRemoteVideo({userId:t.userId,streamType:t.streamType,view:d})}catch(M){console.warn("[AssistantWatchCall] startRemoteVideo",M)}}async function V(t){if(!a)return;const e=w(t.userId,t.streamType),o=n.get(e);if(o){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}o.wrap.remove(),n.delete(e)}}function C(){a&&(a.on(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.on(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}function k(){a&&(a.off(c.EVENT.REMOTE_VIDEO_AVAILABLE,T),a.off(c.EVENT.REMOTE_VIDEO_UNAVAILABLE,V))}async function m(){if(k(),a){for(const[,t]of n){try{await a.stopRemoteVideo({userId:t.userId,streamType:t.streamType})}catch{}t.wrap.remove()}n.clear(),r.value&&(r.value.innerHTML="");try{await a.exitRoom()}catch{}try{a.destroy()}catch{}a=null}else n.clear(),r.value&&(r.value.innerHTML="")}async function L(){const t=++p;if(await m(),!u.diagnosisId){i.value="诊单无效";return}l.value=!0,i.value="",f.value="旁观视频通话";try{const e=await Y({diagnosis_id:u.diagnosisId});if(t!==p)return;e.patientName&&(f.value=`旁观视频通话 · ${e.patientName}`),a=c.create(),C();const o={sdkAppId:e.sdkAppId,userId:e.userId,userSig:e.userSig,autoReceiveAudio:!0,autoReceiveVideo:!0,...e.roomId!=null&&e.roomId>0?{roomId:e.roomId}:{strRoomId:e.strRoomId}};if(!(e.roomId!=null&&e.roomId>0)&&!e.strRoomId)throw new Error("缺少房间号");if(await a.enterRoom(o),t!==p){await m();return}l.value=!1}catch(e){l.value=!1;let o="进入房间失败";if(typeof e=="string")o=e;else if(e&&typeof e=="object"){const s=e;s.msg?o=String(s.msg):s.message&&(o=String(s.message))}i.value=o,await m()}}function b(){m(),l.value=!1,i.value="",f.value="旁观视频通话",g("closed")}return D(()=>[u.modelValue,u.diagnosisId],([t,e])=>{if(!t){p++,m();return}e>0&&L()}),(t,e)=>{const o=$,s=H;return E(),O(s,{modelValue:y.value,"onUpdate:modelValue":e[1]||(e[1]=d=>y.value=d),title:f.value,width:"760px","destroy-on-close":"","append-to-body":"","close-on-click-modal":!1,class:"assistant-watch-call-dialog",onClosed:b},{footer:I(()=>[e[3]||(e[3]=h("span",{class:"watch-hint"},"仅观看,不会开启摄像头与麦克风",-1)),U(o,{type:"primary",onClick:e[0]||(e[0]=d=>y.value=!1)},{default:I(()=>[...e[2]||(e[2]=[j("离开",-1)])]),_:1})]),default:I(()=>[l.value?(E(),_("div",z,"正在连接房间…")):i.value?(E(),_("div",F,S(i.value),1)):x("",!0),W(h("div",{ref_key:"gridRef",ref:r,class:"watch-grid"},null,512),[[P,!l.value&&!i.value]])]),_:1},8,["modelValue","title"])}}}),Z=q(G,[["__scopeId","data-v-7aff665d"]]);export{Z as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{o as N,dg as O,R as P,q as d,r as f,v as o,D as i,K as V,L as n,P as h,ac as D,O as w,bj as L,bk as z,T as u,s as y,bi as M,M as v}from"./.pnpm-CJoARskO.js";import j from"./RecordingPlaybackBlock-D_ZAVM_W.js";import{U as x}from"./index-0im0Afgh.js";import{i as c,_ as q}from"./index-vwMTrNXz.js";import{af as K,ag as k,ah as A}from"./tcm-BBytqd3B.js";import"./RecordingVideoPlayer-BdmuK8u1.js";import"./file-Dg-5v8cj.js";const F={class:"call-record-panel"},G={key:0,class:"call-record-toolbar"},H={class:"call-record-empty"},J={class:"call-record-empty__desc"},Q={key:0,class:"text-primary"},W={key:1,class:"text-gray-400"},X=N({__name:"CallRecordPanel",props:{diagnosisId:{},readOnly:{type:Boolean,default:!1}},setup(_,{expose:R}){const r=_,p=v(!1),g=v([]),U=O("toolbarUploadRef"),m=async()=>{if(r.diagnosisId){p.value=!0;try{g.value=await K({diagnosis_id:r.diagnosisId})||[]}catch(e){console.error(e),g.value=[]}finally{p.value=!1}}};P(()=>r.diagnosisId,()=>{m()},{immediate:!0}),R({refresh:m});function C(e){return{1:"进行中",2:"已结束",3:"未接听",4:"已取消"}[e]??"—"}async function S(e){const t=b(e);if(!t){c.msgError("上传成功但未返回视频地址");return}try{const a=await A({diagnosis_id:r.diagnosisId});await k({diagnosis_id:r.diagnosisId,call_record_id:Number((a==null?void 0:a.id)||0),file_url:t}),c.msgSuccess("视频回放上传成功"),await m()}catch(a){c.msgError((a==null?void 0:a.message)||"写入回放失败")}}async function E(e,t){const a=b(t);if(!a){c.msgError("上传成功但未返回视频地址");return}try{await k({diagnosis_id:r.diagnosisId,call_record_id:Number(e.id||0),file_url:a}),c.msgSuccess("视频回放上传成功"),await m()}catch(l){c.msgError((l==null?void 0:l.message)||"写入回放失败")}}function b(e){var t,a;return String(((t=e==null?void 0:e.data)==null?void 0:t.uri)||((a=e==null?void 0:e.data)==null?void 0:a.url)||"").trim()}return(e,t)=>{const a=V,l=z,I=L,B=M;return d(),f("div",F,[_.readOnly?h("",!0):(d(),f("div",G,[o(x,{ref_key:"toolbarUploadRef",ref:U,type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:S},{default:i(()=>[o(a,{type:"primary"},{default:i(()=>[...t[0]||(t[0]=[n("上传视频",-1)])]),_:1})]),_:1},512)])),D((d(),w(I,{data:g.value,border:"",stripe:""},{empty:i(()=>[y("div",H,[t[1]||(t[1]=y("div",{class:"call-record-empty__title"},"暂无通话记录",-1)),y("div",J,u(_.readOnly?"暂无录制回放数据。":"现在可以直接点击上方“上传视频”。系统会自动生成一条默认通话记录来承载回放。"),1)])]),default:i(()=>[o(l,{label:"录制回放","min-width":"320"},{default:i(({row:s})=>[o(j,{"record-id":s.id,urls:s.recording_urls_list},null,8,["record-id","urls"])]),_:1}),o(l,{label:"开始时间",width:"170",prop:"start_time_text"}),o(l,{label:"结束时间",width:"170",prop:"end_time_text"}),o(l,{label:"通话类型",width:"100"},{default:i(({row:s})=>[n(u(s.call_type===1?"语音":"视频"),1)]),_:1}),o(l,{label:"房间号",width:"180"},{default:i(({row:s})=>[s.room_id?(d(),f("span",Q,u(s.room_id),1)):(d(),f("span",W,"—"))]),_:1}),o(l,{label:"时长",width:"110",prop:"duration_text"}),o(l,{label:"状态",width:"90"},{default:i(({row:s})=>[n(u(C(s.status)),1)]),_:1}),o(l,{label:"录制",width:"100"},{default:i(({row:s})=>[n(u(s.recording_status_text||"—"),1)]),_:1}),_.readOnly?h("",!0):(d(),w(l,{key:0,label:"上传回放",width:"180"},{default:i(({row:s})=>[o(x,{type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:T=>E(s,T)},{default:i(()=>[o(a,{type:"primary",plain:"",size:"small"},{default:i(()=>[...t[2]||(t[2]=[n("上传视频",-1)])]),_:1})]),_:1},8,["onSuccess"])]),_:1}))]),_:1},8,["data"])),[[B,p.value]])])}}}),sa=q(X,[["__scopeId","data-v-78d5c9e4"]]);export{sa as default};
|
||||
import{o as N,dg as O,R as P,q as d,r as f,v as o,D as i,K as V,L as n,P as h,ac as D,O as w,bj as L,bk as z,T as u,s as y,bi as M,M as v}from"./.pnpm-BadRMC3e.js";import j from"./RecordingPlaybackBlock-W5bdjS_P.js";import{U as x}from"./index-CKJths3X.js";import{i as c,_ as q}from"./index-Do7fAtr3.js";import{af as K,ag as k,ah as A}from"./tcm-BDu8-eLA.js";import"./RecordingVideoPlayer-ActCwTef.js";import"./file-BfbRb1wZ.js";const F={class:"call-record-panel"},G={key:0,class:"call-record-toolbar"},H={class:"call-record-empty"},J={class:"call-record-empty__desc"},Q={key:0,class:"text-primary"},W={key:1,class:"text-gray-400"},X=N({__name:"CallRecordPanel",props:{diagnosisId:{},readOnly:{type:Boolean,default:!1}},setup(_,{expose:R}){const r=_,p=v(!1),g=v([]),U=O("toolbarUploadRef"),m=async()=>{if(r.diagnosisId){p.value=!0;try{g.value=await K({diagnosis_id:r.diagnosisId})||[]}catch(e){console.error(e),g.value=[]}finally{p.value=!1}}};P(()=>r.diagnosisId,()=>{m()},{immediate:!0}),R({refresh:m});function C(e){return{1:"进行中",2:"已结束",3:"未接听",4:"已取消"}[e]??"—"}async function S(e){const t=b(e);if(!t){c.msgError("上传成功但未返回视频地址");return}try{const a=await A({diagnosis_id:r.diagnosisId});await k({diagnosis_id:r.diagnosisId,call_record_id:Number((a==null?void 0:a.id)||0),file_url:t}),c.msgSuccess("视频回放上传成功"),await m()}catch(a){c.msgError((a==null?void 0:a.message)||"写入回放失败")}}async function E(e,t){const a=b(t);if(!a){c.msgError("上传成功但未返回视频地址");return}try{await k({diagnosis_id:r.diagnosisId,call_record_id:Number(e.id||0),file_url:a}),c.msgSuccess("视频回放上传成功"),await m()}catch(l){c.msgError((l==null?void 0:l.message)||"写入回放失败")}}function b(e){var t,a;return String(((t=e==null?void 0:e.data)==null?void 0:t.uri)||((a=e==null?void 0:e.data)==null?void 0:a.url)||"").trim()}return(e,t)=>{const a=V,l=z,I=L,B=M;return d(),f("div",F,[_.readOnly?h("",!0):(d(),f("div",G,[o(x,{ref_key:"toolbarUploadRef",ref:U,type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:S},{default:i(()=>[o(a,{type:"primary"},{default:i(()=>[...t[0]||(t[0]=[n("上传视频",-1)])]),_:1})]),_:1},512)])),D((d(),w(I,{data:g.value,border:"",stripe:""},{empty:i(()=>[y("div",H,[t[1]||(t[1]=y("div",{class:"call-record-empty__title"},"暂无通话记录",-1)),y("div",J,u(_.readOnly?"暂无录制回放数据。":"现在可以直接点击上方“上传视频”。系统会自动生成一条默认通话记录来承载回放。"),1)])]),default:i(()=>[o(l,{label:"录制回放","min-width":"320"},{default:i(({row:s})=>[o(j,{"record-id":s.id,urls:s.recording_urls_list},null,8,["record-id","urls"])]),_:1}),o(l,{label:"开始时间",width:"170",prop:"start_time_text"}),o(l,{label:"结束时间",width:"170",prop:"end_time_text"}),o(l,{label:"通话类型",width:"100"},{default:i(({row:s})=>[n(u(s.call_type===1?"语音":"视频"),1)]),_:1}),o(l,{label:"房间号",width:"180"},{default:i(({row:s})=>[s.room_id?(d(),f("span",Q,u(s.room_id),1)):(d(),f("span",W,"—"))]),_:1}),o(l,{label:"时长",width:"110",prop:"duration_text"}),o(l,{label:"状态",width:"90"},{default:i(({row:s})=>[n(u(C(s.status)),1)]),_:1}),o(l,{label:"录制",width:"100"},{default:i(({row:s})=>[n(u(s.recording_status_text||"—"),1)]),_:1}),_.readOnly?h("",!0):(d(),w(l,{key:0,label:"上传回放",width:"180"},{default:i(({row:s})=>[o(x,{type:"video",direct:"",multiple:!1,limit:1,"show-progress":!0,onSuccess:T=>E(s,T)},{default:i(()=>[o(a,{type:"primary",plain:"",size:"small"},{default:i(()=>[...t[2]||(t[2]=[n("上传视频",-1)])]),_:1})]),_:1},8,["onSuccess"])]),_:1}))]),_:1},8,["data"])),[[B,p.value]])])}}}),sa=q(X,[["__scopeId","data-v-78d5c9e4"]]);export{sa as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as T,ap as V,R as I,q as o,r as l,v as a,K as N,D as i,L as c,P as k,ac as z,bi as M,u as p,O as f,bk as O,T as _,F as P,br as j,s as F,bj as R,bB as A,M as w}from"./.pnpm-CJoARskO.js";import{ai as q}from"./tcm-BBytqd3B.js";import{_ as H}from"./index-vwMTrNXz.js";const K={class:"case-record-list"},Y={key:0,class:"mb-3 flex justify-end"},G={key:0},J={key:1,class:"text-gray-400"},Q={class:"void-detail text-xs text-gray-500 mt-1"},U=T({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const m=y,b=C,r=w([]),d=w(!1),u=async()=>{if(m.diagnosisId){d.value=!0;try{const t=await q({diagnosis_id:m.diagnosisId});r.value=Array.isArray(t)?t:[]}catch(t){console.error("获取病历记录失败:",t),r.value=[]}finally{d.value=!1}}},S=t=>{if(!t)return"";const e=new Date(t*1e3);return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")} ${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`},B=t=>{b("view",t)},$=()=>{b("openPrescription")};return V(()=>{u()}),I(()=>m.diagnosisId,()=>{u()}),x({refresh:u}),(t,e)=>{const h=N,n=O,v=j,E=R,D=A,L=M;return o(),l("div",K,[y.readOnly?k("",!0):(o(),l("div",Y,[a(h,{type:"primary",size:"small",onClick:$},{default:i(()=>[...e[0]||(e[0]=[c("开方",-1)])]),_:1})])),z((o(),f(E,{data:p(r),border:""},{default:i(()=>[a(n,{prop:"prescription_date",label:"就诊日期",width:"120"}),a(n,{prop:"visit_no",label:"门诊号",width:"120"}),a(n,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),a(n,{label:"处方摘要","min-width":"180"},{default:i(({row:s})=>[s.herbs&&s.herbs.length?(o(),l("span",G,_(s.herbs.slice(0,3).map(g=>`${g.name}${g.dosage}克`).join("、"))+_(s.herbs.length>3?"...":""),1)):(o(),l("span",J,"—"))]),_:1}),a(n,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),a(n,{label:"状态",width:"140",align:"center"},{default:i(({row:s})=>[s.void_status===1?(o(),l(P,{key:0},[a(v,{type:"danger",size:"small"},{default:i(()=>[...e[1]||(e[1]=[c("已作废",-1)])]),_:1}),F("div",Q,_(s.void_by_name||"—")+" "+_(S(s.void_time)),1)],64)):(o(),f(v,{key:1,type:"success",size:"small"},{default:i(()=>[...e[2]||(e[2]=[c("正常",-1)])]),_:1}))]),_:1}),a(n,{label:"操作",width:"120",fixed:"right"},{default:i(({row:s})=>[a(h,{link:"",type:"primary",size:"small",onClick:g=>B(s)},{default:i(()=>[...e[3]||(e[3]=[c(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[L,p(d)]]),!p(d)&&p(r).length===0?(o(),f(D,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):k("",!0)])}}}),ee=H(U,[["__scopeId","data-v-043d2738"]]);export{ee as default};
|
||||
import{o as T,ap as V,R as I,q as o,r as l,v as a,K as N,D as i,L as c,P as k,ac as z,bi as M,u as p,O as f,bk as O,T as _,F as P,br as j,s as F,bj as R,bB as A,M as w}from"./.pnpm-BadRMC3e.js";import{ai as q}from"./tcm-BDu8-eLA.js";import{_ as H}from"./index-Do7fAtr3.js";const K={class:"case-record-list"},Y={key:0,class:"mb-3 flex justify-end"},G={key:0},J={key:1,class:"text-gray-400"},Q={class:"void-detail text-xs text-gray-500 mt-1"},U=T({__name:"CaseRecordList",props:{diagnosisId:{type:Number,default:0},readOnly:{type:Boolean,default:!1}},emits:["view","openPrescription"],setup(y,{expose:x,emit:C}){const m=y,b=C,r=w([]),d=w(!1),u=async()=>{if(m.diagnosisId){d.value=!0;try{const t=await q({diagnosis_id:m.diagnosisId});r.value=Array.isArray(t)?t:[]}catch(t){console.error("获取病历记录失败:",t),r.value=[]}finally{d.value=!1}}},S=t=>{if(!t)return"";const e=new Date(t*1e3);return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")} ${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`},B=t=>{b("view",t)},$=()=>{b("openPrescription")};return V(()=>{u()}),I(()=>m.diagnosisId,()=>{u()}),x({refresh:u}),(t,e)=>{const h=N,n=O,v=j,E=R,D=A,L=M;return o(),l("div",K,[y.readOnly?k("",!0):(o(),l("div",Y,[a(h,{type:"primary",size:"small",onClick:$},{default:i(()=>[...e[0]||(e[0]=[c("开方",-1)])]),_:1})])),z((o(),f(E,{data:p(r),border:""},{default:i(()=>[a(n,{prop:"prescription_date",label:"就诊日期",width:"120"}),a(n,{prop:"visit_no",label:"门诊号",width:"120"}),a(n,{prop:"clinical_diagnosis",label:"临床诊断","min-width":"160","show-overflow-tooltip":""}),a(n,{label:"处方摘要","min-width":"180"},{default:i(({row:s})=>[s.herbs&&s.herbs.length?(o(),l("span",G,_(s.herbs.slice(0,3).map(g=>`${g.name}${g.dosage}克`).join("、"))+_(s.herbs.length>3?"...":""),1)):(o(),l("span",J,"—"))]),_:1}),a(n,{prop:"doctor_name",label:"医师",width:"90","show-overflow-tooltip":""}),a(n,{label:"状态",width:"140",align:"center"},{default:i(({row:s})=>[s.void_status===1?(o(),l(P,{key:0},[a(v,{type:"danger",size:"small"},{default:i(()=>[...e[1]||(e[1]=[c("已作废",-1)])]),_:1}),F("div",Q,_(s.void_by_name||"—")+" "+_(S(s.void_time)),1)],64)):(o(),f(v,{key:1,type:"success",size:"small"},{default:i(()=>[...e[2]||(e[2]=[c("正常",-1)])]),_:1}))]),_:1}),a(n,{label:"操作",width:"120",fixed:"right"},{default:i(({row:s})=>[a(h,{link:"",type:"primary",size:"small",onClick:g=>B(s)},{default:i(()=>[...e[3]||(e[3]=[c(" 查看 ",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["data"])),[[L,p(d)]]),!p(d)&&p(r).length===0?(o(),f(D,{key:1,description:"暂无病历记录,开方后会自动显示",class:"mt-4"})):k("",!0)])}}}),ee=H(U,[["__scopeId","data-v-043d2738"]]);export{ee as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./GancaoSubmissionReconcileButton.vue_vue_type_script_setup_true_lang-LIiWe8wu.js";import"./.pnpm-BadRMC3e.js";import"./tcm-BDu8-eLA.js";import"./index-Do7fAtr3.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./GancaoSubmissionReconcileButton.vue_vue_type_script_setup_true_lang-C23w1g-E.js";import"./.pnpm-CJoARskO.js";import"./tcm-BBytqd3B.js";import"./index-vwMTrNXz.js";export{o as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as U,be as F,q as f,r as k,F as M,ac as D,O as E,D as t,L as u,K as G,v as r,bt as B,bh as q,b9 as P,aw as A,b6 as L,bc as T,bf as K,b7 as z,P as g,p as W,M as v,ba as $}from"./.pnpm-CJoARskO.js";import{p as j}from"./tcm-BBytqd3B.js";import{i as C}from"./index-vwMTrNXz.js";const X=U({__name:"GancaoSubmissionReconcileButton",props:{order:{}},emits:["resolved"],setup(N,{emit:w}){const d=N,y=w,V=W(()=>{var m,p,a;const i=String(((m=d.order)==null?void 0:m.pharmacy_claim_target)||"").trim().toLowerCase(),e=String(((p=d.order)==null?void 0:p.pharmacy_claim_status)||"").trim().toUpperCase(),n=Number(((a=d.order)==null?void 0:a.pharmacy_claim_lease_expires_at)||0),c=e==="PENDING"&&n>0&&n<=Math.floor(Date.now()/1e3);return i==="gancao"&&(c||["UNKNOWN","PENDING_RECONCILE"].includes(e))}),s=v(!1),_=v(!1),o=$({resolution:"CONFIRM_SUCCESS",remote_order_no:"",note:""});function O(){o.resolution="CONFIRM_SUCCESS",o.remote_order_no="",o.note="",s.value=!0}async function b(){var e;const i=Number((e=d.order)==null?void 0:e.id);if(i){if(o.resolution==="CONFIRM_SUCCESS"&&!o.remote_order_no.trim()){C.msgError("请填写甘草药方单号");return}if(!o.note.trim()){C.msgError("请填写甘草后台核对依据");return}_.value=!0;try{await j({id:i,resolution:o.resolution,remote_order_no:o.resolution==="CONFIRM_SUCCESS"?o.remote_order_no.trim():"",note:o.note.trim()}),C.msgSuccess("甘草提交核对已记录"),s.value=!1,y("resolved")}catch{}finally{_.value=!1}}}return(i,e)=>{const n=G,c=q,m=K,p=T,a=L,S=z,R=P,x=B,I=F("perms");return V.value?(f(),k(M,{key:0},[D((f(),E(n,{type:"danger",size:"small",plain:"",onClick:O},{default:t(()=>[...e[5]||(e[5]=[u("核对甘草提交",-1)])]),_:1})),[[I,["tcm.prescriptionOrder/confirmGancaoSubmission"]]]),r(x,{modelValue:s.value,"onUpdate:modelValue":e[4]||(e[4]=l=>s.value=l),title:"人工核对甘草提交",width:"min(92vw, 520px)","append-to-body":"","destroy-on-close":"","close-on-click-modal":!1},{footer:t(()=>[r(n,{onClick:e[3]||(e[3]=l=>s.value=!1)},{default:t(()=>[...e[8]||(e[8]=[u("取消",-1)])]),_:1}),r(n,{type:"primary",loading:_.value,onClick:b},{default:t(()=>[...e[9]||(e[9]=[u("确认并记录",-1)])]),_:1},8,["loading"])]),default:t(()=>[r(c,{title:"请先在甘草后台核对。本操作会写入不可变更的审计记录。",type:"warning",closable:!1,"show-icon":"",class:"mb-4"}),r(R,{"label-width":"100px",onSubmit:A(b,["prevent"])},{default:t(()=>[r(a,{label:"核对结果",required:""},{default:t(()=>[r(p,{modelValue:o.resolution,"onUpdate:modelValue":e[0]||(e[0]=l=>o.resolution=l)},{default:t(()=>[r(m,{label:"CONFIRM_SUCCESS"},{default:t(()=>[...e[6]||(e[6]=[u("确认已创建",-1)])]),_:1}),r(m,{label:"CONFIRM_NOT_CREATED"},{default:t(()=>[...e[7]||(e[7]=[u("确认未创建",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),o.resolution==="CONFIRM_SUCCESS"?(f(),E(a,{key:0,label:"甘草单号",required:""},{default:t(()=>[r(S,{modelValue:o.remote_order_no,"onUpdate:modelValue":e[1]||(e[1]=l=>o.remote_order_no=l),maxlength:"64",clearable:""},null,8,["modelValue"])]),_:1})):g("",!0),r(a,{label:"核对依据",required:""},{default:t(()=>[r(S,{modelValue:o.note,"onUpdate:modelValue":e[2]||(e[2]=l=>o.note=l),type:"textarea",rows:3,maxlength:"1000","show-word-limit":"",placeholder:"例如:核对时间、甘草后台查询条件及结果"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)):g("",!0)}}});export{X as _};
|
||||
import{o as U,be as F,q as f,r as k,F as M,ac as D,O as E,D as t,L as u,K as G,v as r,bt as B,bh as q,b9 as P,aw as A,b6 as L,bc as T,bf as K,b7 as z,P as g,p as W,M as v,ba as $}from"./.pnpm-BadRMC3e.js";import{p as j}from"./tcm-BDu8-eLA.js";import{i as C}from"./index-Do7fAtr3.js";const X=U({__name:"GancaoSubmissionReconcileButton",props:{order:{}},emits:["resolved"],setup(N,{emit:w}){const d=N,y=w,V=W(()=>{var m,p,a;const i=String(((m=d.order)==null?void 0:m.pharmacy_claim_target)||"").trim().toLowerCase(),e=String(((p=d.order)==null?void 0:p.pharmacy_claim_status)||"").trim().toUpperCase(),n=Number(((a=d.order)==null?void 0:a.pharmacy_claim_lease_expires_at)||0),c=e==="PENDING"&&n>0&&n<=Math.floor(Date.now()/1e3);return i==="gancao"&&(c||["UNKNOWN","PENDING_RECONCILE"].includes(e))}),s=v(!1),_=v(!1),o=$({resolution:"CONFIRM_SUCCESS",remote_order_no:"",note:""});function O(){o.resolution="CONFIRM_SUCCESS",o.remote_order_no="",o.note="",s.value=!0}async function b(){var e;const i=Number((e=d.order)==null?void 0:e.id);if(i){if(o.resolution==="CONFIRM_SUCCESS"&&!o.remote_order_no.trim()){C.msgError("请填写甘草药方单号");return}if(!o.note.trim()){C.msgError("请填写甘草后台核对依据");return}_.value=!0;try{await j({id:i,resolution:o.resolution,remote_order_no:o.resolution==="CONFIRM_SUCCESS"?o.remote_order_no.trim():"",note:o.note.trim()}),C.msgSuccess("甘草提交核对已记录"),s.value=!1,y("resolved")}catch{}finally{_.value=!1}}}return(i,e)=>{const n=G,c=q,m=K,p=T,a=L,S=z,R=P,x=B,I=F("perms");return V.value?(f(),k(M,{key:0},[D((f(),E(n,{type:"danger",size:"small",plain:"",onClick:O},{default:t(()=>[...e[5]||(e[5]=[u("核对甘草提交",-1)])]),_:1})),[[I,["tcm.prescriptionOrder/confirmGancaoSubmission"]]]),r(x,{modelValue:s.value,"onUpdate:modelValue":e[4]||(e[4]=l=>s.value=l),title:"人工核对甘草提交",width:"min(92vw, 520px)","append-to-body":"","destroy-on-close":"","close-on-click-modal":!1},{footer:t(()=>[r(n,{onClick:e[3]||(e[3]=l=>s.value=!1)},{default:t(()=>[...e[8]||(e[8]=[u("取消",-1)])]),_:1}),r(n,{type:"primary",loading:_.value,onClick:b},{default:t(()=>[...e[9]||(e[9]=[u("确认并记录",-1)])]),_:1},8,["loading"])]),default:t(()=>[r(c,{title:"请先在甘草后台核对。本操作会写入不可变更的审计记录。",type:"warning",closable:!1,"show-icon":"",class:"mb-4"}),r(R,{"label-width":"100px",onSubmit:A(b,["prevent"])},{default:t(()=>[r(a,{label:"核对结果",required:""},{default:t(()=>[r(p,{modelValue:o.resolution,"onUpdate:modelValue":e[0]||(e[0]=l=>o.resolution=l)},{default:t(()=>[r(m,{label:"CONFIRM_SUCCESS"},{default:t(()=>[...e[6]||(e[6]=[u("确认已创建",-1)])]),_:1}),r(m,{label:"CONFIRM_NOT_CREATED"},{default:t(()=>[...e[7]||(e[7]=[u("确认未创建",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),o.resolution==="CONFIRM_SUCCESS"?(f(),E(a,{key:0,label:"甘草单号",required:""},{default:t(()=>[r(S,{modelValue:o.remote_order_no,"onUpdate:modelValue":e[1]||(e[1]=l=>o.remote_order_no=l),maxlength:"64",clearable:""},null,8,["modelValue"])]),_:1})):g("",!0),r(a,{label:"核对依据",required:""},{default:t(()=>[r(S,{modelValue:o.note,"onUpdate:modelValue":e[2]||(e[2]=l=>o.note=l),type:"textarea",rows:3,maxlength:"1000","show-word-limit":"",placeholder:"例如:核对时间、甘草后台查询条件及结果"},null,8,["modelValue"])]),_:1})]),_:1})]),_:1},8,["modelValue"])],64)):g("",!0)}}});export{X as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as F,R as q,q as t,r as n,v as i,D as c,s as o,L as _,bh as G,w as H,u as x,cV as $,K as j,by as J,ac as K,F as E,G as O,O as f,bB as W,P as v,bi as Q,M as m,p as U,ae as X,T as r,br as Z,aa as ee,bq as ae,a8 as se,E as C}from"./.pnpm-CJoARskO.js";import{d as te}from"./dayjs-HJJmW-S4.js";import{an as ne,ao as oe}from"./tcm-BBytqd3B.js";import{p as re}from"./im-business-message-parse-DTpJoEdY.js";import{_ as le}from"./index-vwMTrNXz.js";const ie={class:"im-chat-record-panel"},ce={class:"toolbar mb-3"},de={class:"chat-wrap"},_e={key:0,class:"chat-list"},ue={class:"meta"},fe={class:"name"},me={class:"time"},pe={class:"bubble"},ye={key:0,class:"friendly-text"},ge={class:"friendly-main"},ve={key:0,class:"friendly-sub"},he={key:1,class:"text-content"},we={key:2,class:"text-content"},be={key:3,class:"muted"},ke=F({__name:"ImChatRecordPanel",props:{diagnosisId:{}},setup(B,{expose:M}){const u=B,d=m(!1),p=m(!1),y=se([]),L=m(""),g=m(""),D={text:"",image:"图片",file:"文件",sound:"语音",video:"视频",location:"位置",custom:"自定义",face:"表情",other:""},h=U(()=>y.value.map(e=>{const s=P(e);let l="";return s!=null&&s.tag?l=s.tag:l=D[e.msg_type]||(e.msg_type!=="other"?e.msg_type:""),{raw:e,friendly:s,tag:l}}));function N(e){if(e==null||!e)return"—";const s=e>1e12?Math.floor(e/1e3):e;return te.unix(s).format("YYYY-MM-DD HH:mm:ss")}function P(e){const s=(e.text||"").trim();if(!s)return null;const l=s.startsWith("{")&&(/\bbusinessID\b/.test(s)||/\bcmd\b/.test(s));return e.msg_type==="custom"||l?re(s):null}function R(e){return e.is_from_doctor?e.from_staff_name?`医生(${e.from_staff_name})`:"医生/员工":g.value?`患者(${g.value})`:"患者"}async function w(){if(u.diagnosisId){d.value=!0;try{const e=await ne({diagnosis_id:u.diagnosisId,only_archived:1});y.value=(e==null?void 0:e.lists)||[],L.value=(e==null?void 0:e.patient_im_id)||"",g.value=(e==null?void 0:e.patient_name)||""}catch(e){console.error(e),y.value=[]}finally{d.value=!1}}}function b(){w()}async function S(){if(u.diagnosisId){p.value=!0;try{await oe({diagnosis_id:u.diagnosisId}),C.success('已发起后台同步,几秒后请点击"重新加载已归档"查看新消息')}catch(e){console.error(e),C.error("发起同步失败")}finally{p.value=!1}}}return q(()=>u.diagnosisId,()=>{w()},{immediate:!0}),M({refresh:b}),(e,s)=>{const l=G,k=H,I=j,T=Z,V=ee,Y=ae,z=W,A=Q;return t(),n("div",ie,[i(l,{type:"info","show-icon":"",closable:!1,class:"mb-4",title:"说明"},{default:c(()=>[...s[0]||(s[0]=[o("p",{class:"panel-tip"},[_(" 展示单聊记录:已合并患者 与 "),o("strong",null,"所有医生 / 医助账号"),_("分别产生的会话,按时间排序。 数据由后台定时任务从腾讯云 IM 漫游消息同步至本地归档;点击下方按钮可立即触发后台同步。 ")],-1)])]),_:1}),o("div",ce,[i(I,{type:"primary",link:"",loading:p.value,onClick:S},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x($))]),_:1}),s[1]||(s[1]=_(" 同步最新(后台异步) ",-1))]),_:1},8,["loading"]),i(I,{type:"primary",link:"",loading:d.value,onClick:b},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x(J))]),_:1}),s[2]||(s[2]=_(" 重新加载已归档 ",-1))]),_:1},8,["loading"])]),K((t(),n("div",de,[!d.value&&h.value.length?(t(),n("div",_e,[(t(!0),n(E,null,O(h.value,a=>(t(),n("div",{key:a.raw.msg_id,class:X(["chat-row",a.raw.is_from_doctor?"from-doctor":"from-patient"])},[o("div",ue,[o("span",fe,r(R(a.raw)),1),o("span",me,r(N(a.raw.time)),1),a.tag?(t(),f(T,{key:0,size:"small",type:"info",class:"ml-2"},{default:c(()=>[_(r(a.tag),1)]),_:2},1024)):v("",!0)]),o("div",pe,[a.raw.msg_type==="image"&&a.raw.image_url?(t(),f(V,{key:0,src:a.raw.image_url,"preview-src-list":[a.raw.image_url],fit:"contain",class:"chat-img"},null,8,["src","preview-src-list"])):(a.raw.msg_type==="file"||a.raw.msg_type==="sound"||a.raw.msg_type==="video")&&a.raw.file_url?(t(),f(Y,{key:1,href:a.raw.file_url,target:"_blank",type:"primary"},{default:c(()=>[_(r(a.raw.file_name||"打开文件"),1)]),_:2},1032,["href"])):(t(),n(E,{key:2},[a.friendly?(t(),n("div",ye,[o("div",ge,r(a.friendly.main),1),a.friendly.sub?(t(),n("div",ve,r(a.friendly.sub),1)):v("",!0)])):a.raw.msg_type==="text"&&a.raw.text?(t(),n("div",he,r(a.raw.text),1)):a.raw.text?(t(),n("div",we,r(a.raw.text),1)):(t(),n("span",be,"(无法展示该消息类型)"))],64))])],2))),128))])):d.value?v("",!0):(t(),f(z,{key:1,description:"暂无 IM 聊天记录"}))])),[[A,d.value]])])}}}),Me=le(ke,[["__scopeId","data-v-58b699dd"]]);export{Me as default};
|
||||
import{o as F,R as q,q as t,r as n,v as i,D as c,s as o,L as _,bh as G,w as H,u as x,cV as $,K as j,by as J,ac as K,F as E,G as O,O as f,bB as W,P as v,bi as Q,M as m,p as U,ae as X,T as r,br as Z,aa as ee,bq as ae,a8 as se,E as C}from"./.pnpm-BadRMC3e.js";import{d as te}from"./dayjs-Dr2Em-pV.js";import{an as ne,ao as oe}from"./tcm-BDu8-eLA.js";import{p as re}from"./im-business-message-parse-DoOLrDi4.js";import{_ as le}from"./index-Do7fAtr3.js";const ie={class:"im-chat-record-panel"},ce={class:"toolbar mb-3"},de={class:"chat-wrap"},_e={key:0,class:"chat-list"},ue={class:"meta"},fe={class:"name"},me={class:"time"},pe={class:"bubble"},ye={key:0,class:"friendly-text"},ge={class:"friendly-main"},ve={key:0,class:"friendly-sub"},he={key:1,class:"text-content"},we={key:2,class:"text-content"},be={key:3,class:"muted"},ke=F({__name:"ImChatRecordPanel",props:{diagnosisId:{}},setup(B,{expose:M}){const u=B,d=m(!1),p=m(!1),y=se([]),L=m(""),g=m(""),D={text:"",image:"图片",file:"文件",sound:"语音",video:"视频",location:"位置",custom:"自定义",face:"表情",other:""},h=U(()=>y.value.map(e=>{const s=P(e);let l="";return s!=null&&s.tag?l=s.tag:l=D[e.msg_type]||(e.msg_type!=="other"?e.msg_type:""),{raw:e,friendly:s,tag:l}}));function N(e){if(e==null||!e)return"—";const s=e>1e12?Math.floor(e/1e3):e;return te.unix(s).format("YYYY-MM-DD HH:mm:ss")}function P(e){const s=(e.text||"").trim();if(!s)return null;const l=s.startsWith("{")&&(/\bbusinessID\b/.test(s)||/\bcmd\b/.test(s));return e.msg_type==="custom"||l?re(s):null}function R(e){return e.is_from_doctor?e.from_staff_name?`医生(${e.from_staff_name})`:"医生/员工":g.value?`患者(${g.value})`:"患者"}async function w(){if(u.diagnosisId){d.value=!0;try{const e=await ne({diagnosis_id:u.diagnosisId,only_archived:1});y.value=(e==null?void 0:e.lists)||[],L.value=(e==null?void 0:e.patient_im_id)||"",g.value=(e==null?void 0:e.patient_name)||""}catch(e){console.error(e),y.value=[]}finally{d.value=!1}}}function b(){w()}async function S(){if(u.diagnosisId){p.value=!0;try{await oe({diagnosis_id:u.diagnosisId}),C.success('已发起后台同步,几秒后请点击"重新加载已归档"查看新消息')}catch(e){console.error(e),C.error("发起同步失败")}finally{p.value=!1}}}return q(()=>u.diagnosisId,()=>{w()},{immediate:!0}),M({refresh:b}),(e,s)=>{const l=G,k=H,I=j,T=Z,V=ee,Y=ae,z=W,A=Q;return t(),n("div",ie,[i(l,{type:"info","show-icon":"",closable:!1,class:"mb-4",title:"说明"},{default:c(()=>[...s[0]||(s[0]=[o("p",{class:"panel-tip"},[_(" 展示单聊记录:已合并患者 与 "),o("strong",null,"所有医生 / 医助账号"),_("分别产生的会话,按时间排序。 数据由后台定时任务从腾讯云 IM 漫游消息同步至本地归档;点击下方按钮可立即触发后台同步。 ")],-1)])]),_:1}),o("div",ce,[i(I,{type:"primary",link:"",loading:p.value,onClick:S},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x($))]),_:1}),s[1]||(s[1]=_(" 同步最新(后台异步) ",-1))]),_:1},8,["loading"]),i(I,{type:"primary",link:"",loading:d.value,onClick:b},{default:c(()=>[i(k,{class:"mr-1"},{default:c(()=>[i(x(J))]),_:1}),s[2]||(s[2]=_(" 重新加载已归档 ",-1))]),_:1},8,["loading"])]),K((t(),n("div",de,[!d.value&&h.value.length?(t(),n("div",_e,[(t(!0),n(E,null,O(h.value,a=>(t(),n("div",{key:a.raw.msg_id,class:X(["chat-row",a.raw.is_from_doctor?"from-doctor":"from-patient"])},[o("div",ue,[o("span",fe,r(R(a.raw)),1),o("span",me,r(N(a.raw.time)),1),a.tag?(t(),f(T,{key:0,size:"small",type:"info",class:"ml-2"},{default:c(()=>[_(r(a.tag),1)]),_:2},1024)):v("",!0)]),o("div",pe,[a.raw.msg_type==="image"&&a.raw.image_url?(t(),f(V,{key:0,src:a.raw.image_url,"preview-src-list":[a.raw.image_url],fit:"contain",class:"chat-img"},null,8,["src","preview-src-list"])):(a.raw.msg_type==="file"||a.raw.msg_type==="sound"||a.raw.msg_type==="video")&&a.raw.file_url?(t(),f(Y,{key:1,href:a.raw.file_url,target:"_blank",type:"primary"},{default:c(()=>[_(r(a.raw.file_name||"打开文件"),1)]),_:2},1032,["href"])):(t(),n(E,{key:2},[a.friendly?(t(),n("div",ye,[o("div",ge,r(a.friendly.main),1),a.friendly.sub?(t(),n("div",ve,r(a.friendly.sub),1)):v("",!0)])):a.raw.msg_type==="text"&&a.raw.text?(t(),n("div",he,r(a.raw.text),1)):a.raw.text?(t(),n("div",we,r(a.raw.text),1)):(t(),n("span",be,"(无法展示该消息类型)"))],64))])],2))),128))])):d.value?v("",!0):(t(),f(z,{key:1,description:"暂无 IM 聊天记录"}))])),[[A,d.value]])])}}}),Me=le(ke,[["__scopeId","data-v-58b699dd"]]);export{Me as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{_ as m}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-iYm8xAAz.js";import"./.pnpm-CJoARskO.js";export{m as default};
|
||||
import{_ as m}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-CWbovTuC.js";import"./.pnpm-BadRMC3e.js";export{m as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as g,q as n,O as s,D as V,r as C,F as v,G as h,bn as B,u as c,P as p,t as w,ae as S,bm as k,p as i}from"./.pnpm-CJoARskO.js";const z=g({__name:"MediaSourceSelect",props:{modelValue:{default:""},options:{},loading:{type:Boolean,default:!1},placeholder:{default:"请选择自媒体来源"},clearable:{type:Boolean,default:!0},filterable:{type:Boolean,default:!0},allowCreate:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},selectClass:{},selectStyle:{}},emits:["update:modelValue","change","visible-change"],setup(e,{emit:m}){const u=e,o=m,d=i(()=>(u.options||[]).map(l=>typeof l=="string"?{name:l}:{name:l.name,value:l.value})),f=i(()=>d.value.some(l=>l.name===u.modelValue)),b=l=>{o("visible-change",l)};return(l,t)=>{const r=B,y=k;return n(),s(y,{"model-value":e.modelValue,placeholder:e.placeholder,clearable:e.clearable,filterable:e.filterable,"allow-create":e.allowCreate,"default-first-option":e.allowCreate,disabled:e.disabled,loading:e.loading,class:S(e.selectClass),style:w(e.selectStyle),"onUpdate:modelValue":t[0]||(t[0]=a=>o("update:modelValue",a)),onChange:t[1]||(t[1]=a=>o("change",a)),onVisibleChange:b},{default:V(()=>[(n(!0),C(v,null,h(c(d),a=>(n(),s(r,{key:a.name,label:a.name,value:a.name},null,8,["label","value"]))),128)),e.modelValue&&!c(f)?(n(),s(r,{key:`__legacy_${e.modelValue}`,label:`${e.modelValue}(已停用)`,value:e.modelValue},null,8,["label","value"])):p("",!0)]),_:1},8,["model-value","placeholder","clearable","filterable","allow-create","default-first-option","disabled","loading","class","style"])}}});export{z as _};
|
||||
import{o as g,q as n,O as s,D as V,r as C,F as v,G as h,bn as B,u as c,P as p,t as w,ae as S,bm as k,p as i}from"./.pnpm-BadRMC3e.js";const z=g({__name:"MediaSourceSelect",props:{modelValue:{default:""},options:{},loading:{type:Boolean,default:!1},placeholder:{default:"请选择自媒体来源"},clearable:{type:Boolean,default:!0},filterable:{type:Boolean,default:!0},allowCreate:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},selectClass:{},selectStyle:{}},emits:["update:modelValue","change","visible-change"],setup(e,{emit:m}){const u=e,o=m,d=i(()=>(u.options||[]).map(l=>typeof l=="string"?{name:l}:{name:l.name,value:l.value})),f=i(()=>d.value.some(l=>l.name===u.modelValue)),b=l=>{o("visible-change",l)};return(l,t)=>{const r=B,y=k;return n(),s(y,{"model-value":e.modelValue,placeholder:e.placeholder,clearable:e.clearable,filterable:e.filterable,"allow-create":e.allowCreate,"default-first-option":e.allowCreate,disabled:e.disabled,loading:e.loading,class:S(e.selectClass),style:w(e.selectStyle),"onUpdate:modelValue":t[0]||(t[0]=a=>o("update:modelValue",a)),onChange:t[1]||(t[1]=a=>o("change",a)),onVisibleChange:b},{default:V(()=>[(n(!0),C(v,null,h(c(d),a=>(n(),s(r,{key:a.name,label:a.name,value:a.name},null,8,["label","value"]))),128)),e.modelValue&&!c(f)?(n(),s(r,{key:`__legacy_${e.modelValue}`,label:`${e.modelValue}(已停用)`,value:e.modelValue},null,8,["label","value"])):p("",!0)]),_:1},8,["model-value","placeholder","clearable","filterable","allow-create","default-first-option","disabled","loading","class","style"])}}});export{z as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as S,q as a,r as l,ae as x,v as r,D as m,L as d,T as i,a0 as D,s as n,O as g,br as G,P as B,aa as K,u as v,bE as L,w as q,bF as A,bG as H,bH as O,F as P,K as U,p as u}from"./.pnpm-CJoARskO.js";import{t as j,_ as J}from"./index-vwMTrNXz.js";const Q={class:"msg-body"},R={class:"msg-meta"},W={class:"msg-sender"},X={class:"msg-time"},Y={key:0,class:"content-text"},Z={key:2,class:"content-pending"},ee=["src"],se={key:4,class:"content-pending"},te=["src"],ae={key:6,class:"content-pending"},ne={key:7,class:"content-file"},le={class:"file-meta"},ie=["title"],oe={class:"file-info"},ce={key:8,class:"content-fallback"},re={class:"fallback-title"},me={class:"fallback-desc"},ue=S({__name:"MessageBubble",props:{message:{}},setup(s){const o=s,w=u(()=>o.message.from_is_staff),c=u(()=>(o.message.media||[]).find(t=>t.status===1)??null),_=u(()=>{const e=o.message.from_profile;return e&&e.name||o.message.from_user}),z=u(()=>{const e=o.message.from_profile;return(e==null?void 0:e.avatar)||""}),F=u(()=>(_.value||"").slice(-2)||"?"),E=u(()=>{const e=o.message.msgtype;return e==="image"||e==="video"||e==="voice"?"bubble-media":e==="file"?"bubble-file":"bubble-text"}),C=u(()=>{switch(o.message.msgtype){case"link":return"[链接]";case"weapp":return"[小程序]";case"chatrecord":return"[聊天记录]";case"location":return"[位置]";case"card":return"[名片]";case"meeting":return"[会议]";case"docmsg":return"[文档]";case"mixed":return"[混合消息]";case"emotion":return"[表情]";default:return`[${o.message.msgtype}]`}});function M(e){return e?j(e*1e3,"mm-dd hh:MM"):""}function N(e){return e?e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/1024/1024).toFixed(2)} MB`:`${(e/1024/1024/1024).toFixed(2)} GB`:""}function $(e){window.open(e,"_blank","noopener")}return(e,t)=>{var k,y,p,h;const T=D,b=G,V=K,f=q,I=U;return a(),l("div",{class:x(["msg-bubble",{"is-staff":w.value}])},[r(T,{class:"msg-avatar",size:36,src:z.value},{default:m(()=>[d(i(F.value),1)]),_:1},8,["src"]),n("div",Q,[n("div",R,[n("span",W,i(_.value),1),n("span",X,i(M(s.message.send_time)),1),s.message.action==="recall"?(a(),g(b,{key:0,size:"small",type:"info"},{default:m(()=>[...t[1]||(t[1]=[d(" 已撤回 ",-1)])]),_:1})):B("",!0)]),n("div",{class:x(["msg-content",E.value])},[s.message.msgtype==="text"||s.message.msgtype==="markdown"?(a(),l("div",Y,i(s.message.content),1)):s.message.msgtype==="image"&&((k=c.value)!=null&&k.file_url)?(a(),g(V,{key:1,src:c.value.file_url,"preview-src-list":[c.value.file_url],fit:"contain",class:"content-image","hide-on-click-modal":""},null,8,["src","preview-src-list"])):s.message.msgtype==="image"?(a(),l("div",Z,[r(f,null,{default:m(()=>[r(v(L))]),_:1}),t[2]||(t[2]=n("span",null,"图片下载中…",-1))])):s.message.msgtype==="video"&&((y=c.value)!=null&&y.file_url)?(a(),l("video",{key:3,src:c.value.file_url,controls:"",class:"content-video"},null,8,ee)):s.message.msgtype==="video"?(a(),l("div",se,[r(f,null,{default:m(()=>[r(v(A))]),_:1}),t[3]||(t[3]=n("span",null,"视频下载中…",-1))])):s.message.msgtype==="voice"&&((p=c.value)!=null&&p.file_url)?(a(),l("audio",{key:5,src:c.value.file_url,controls:"",class:"content-audio"},null,8,te)):s.message.msgtype==="voice"?(a(),l("div",ae,[r(f,null,{default:m(()=>[r(v(H))]),_:1}),n("span",null,"语音下载中…("+i(s.message.play_length)+"s)",1)])):s.message.msgtype==="file"?(a(),l("div",ne,[r(f,{size:24},{default:m(()=>[r(v(O))]),_:1}),n("div",le,[n("div",{class:"file-name",title:s.message.file_name},i(s.message.file_name||"未命名文件"),9,ie),n("div",oe,[d(i(N(s.message.file_size))+" ",1),s.message.file_ext?(a(),l(P,{key:0},[d("· "+i(s.message.file_ext),1)],64)):B("",!0)])]),(h=c.value)!=null&&h.file_url?(a(),g(I,{key:0,size:"small",type:"primary",link:"",onClick:t[0]||(t[0]=de=>$(c.value.file_url))},{default:m(()=>[...t[4]||(t[4]=[d(" 下载 ",-1)])]),_:1})):(a(),g(b,{key:1,size:"small",type:"info"},{default:m(()=>[...t[5]||(t[5]=[d("下载中…",-1)])]),_:1}))])):(a(),l("div",ce,[n("div",re,i(C.value),1),n("div",me,i(s.message.content),1)]))],2)])],2)}}}),ve=J(ue,[["__scopeId","data-v-b5ff1168"]]);export{ve as default};
|
||||
import{o as S,q as a,r as l,ae as x,v as r,D as m,L as d,T as i,a0 as D,s as n,O as g,br as G,P as B,aa as K,u as v,bE as L,w as q,bF as A,bG as H,bH as O,F as P,K as U,p as u}from"./.pnpm-BadRMC3e.js";import{t as j,_ as J}from"./index-Do7fAtr3.js";const Q={class:"msg-body"},R={class:"msg-meta"},W={class:"msg-sender"},X={class:"msg-time"},Y={key:0,class:"content-text"},Z={key:2,class:"content-pending"},ee=["src"],se={key:4,class:"content-pending"},te=["src"],ae={key:6,class:"content-pending"},ne={key:7,class:"content-file"},le={class:"file-meta"},ie=["title"],oe={class:"file-info"},ce={key:8,class:"content-fallback"},re={class:"fallback-title"},me={class:"fallback-desc"},ue=S({__name:"MessageBubble",props:{message:{}},setup(s){const o=s,w=u(()=>o.message.from_is_staff),c=u(()=>(o.message.media||[]).find(t=>t.status===1)??null),_=u(()=>{const e=o.message.from_profile;return e&&e.name||o.message.from_user}),z=u(()=>{const e=o.message.from_profile;return(e==null?void 0:e.avatar)||""}),F=u(()=>(_.value||"").slice(-2)||"?"),E=u(()=>{const e=o.message.msgtype;return e==="image"||e==="video"||e==="voice"?"bubble-media":e==="file"?"bubble-file":"bubble-text"}),C=u(()=>{switch(o.message.msgtype){case"link":return"[链接]";case"weapp":return"[小程序]";case"chatrecord":return"[聊天记录]";case"location":return"[位置]";case"card":return"[名片]";case"meeting":return"[会议]";case"docmsg":return"[文档]";case"mixed":return"[混合消息]";case"emotion":return"[表情]";default:return`[${o.message.msgtype}]`}});function M(e){return e?j(e*1e3,"mm-dd hh:MM"):""}function N(e){return e?e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/1024/1024).toFixed(2)} MB`:`${(e/1024/1024/1024).toFixed(2)} GB`:""}function $(e){window.open(e,"_blank","noopener")}return(e,t)=>{var k,y,p,h;const T=D,b=G,V=K,f=q,I=U;return a(),l("div",{class:x(["msg-bubble",{"is-staff":w.value}])},[r(T,{class:"msg-avatar",size:36,src:z.value},{default:m(()=>[d(i(F.value),1)]),_:1},8,["src"]),n("div",Q,[n("div",R,[n("span",W,i(_.value),1),n("span",X,i(M(s.message.send_time)),1),s.message.action==="recall"?(a(),g(b,{key:0,size:"small",type:"info"},{default:m(()=>[...t[1]||(t[1]=[d(" 已撤回 ",-1)])]),_:1})):B("",!0)]),n("div",{class:x(["msg-content",E.value])},[s.message.msgtype==="text"||s.message.msgtype==="markdown"?(a(),l("div",Y,i(s.message.content),1)):s.message.msgtype==="image"&&((k=c.value)!=null&&k.file_url)?(a(),g(V,{key:1,src:c.value.file_url,"preview-src-list":[c.value.file_url],fit:"contain",class:"content-image","hide-on-click-modal":""},null,8,["src","preview-src-list"])):s.message.msgtype==="image"?(a(),l("div",Z,[r(f,null,{default:m(()=>[r(v(L))]),_:1}),t[2]||(t[2]=n("span",null,"图片下载中…",-1))])):s.message.msgtype==="video"&&((y=c.value)!=null&&y.file_url)?(a(),l("video",{key:3,src:c.value.file_url,controls:"",class:"content-video"},null,8,ee)):s.message.msgtype==="video"?(a(),l("div",se,[r(f,null,{default:m(()=>[r(v(A))]),_:1}),t[3]||(t[3]=n("span",null,"视频下载中…",-1))])):s.message.msgtype==="voice"&&((p=c.value)!=null&&p.file_url)?(a(),l("audio",{key:5,src:c.value.file_url,controls:"",class:"content-audio"},null,8,te)):s.message.msgtype==="voice"?(a(),l("div",ae,[r(f,null,{default:m(()=>[r(v(H))]),_:1}),n("span",null,"语音下载中…("+i(s.message.play_length)+"s)",1)])):s.message.msgtype==="file"?(a(),l("div",ne,[r(f,{size:24},{default:m(()=>[r(v(O))]),_:1}),n("div",le,[n("div",{class:"file-name",title:s.message.file_name},i(s.message.file_name||"未命名文件"),9,ie),n("div",oe,[d(i(N(s.message.file_size))+" ",1),s.message.file_ext?(a(),l(P,{key:0},[d("· "+i(s.message.file_ext),1)],64)):B("",!0)])]),(h=c.value)!=null&&h.file_url?(a(),g(I,{key:0,size:"small",type:"primary",link:"",onClick:t[0]||(t[0]=de=>$(c.value.file_url))},{default:m(()=>[...t[4]||(t[4]=[d(" 下载 ",-1)])]),_:1})):(a(),g(b,{key:1,size:"small",type:"info"},{default:m(()=>[...t[5]||(t[5]=[d("下载中…",-1)])]),_:1}))])):(a(),l("div",ce,[n("div",re,i(C.value),1),n("div",me,i(s.message.content),1)]))],2)])],2)}}}),ve=J(ue,[["__scopeId","data-v-b5ff1168"]]);export{ve as default};
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
import{o as ie,R as oe,q as n,r as o,O as k,K as le,D as r,L as P,P as m,v as l,s as d,F as w,G as E,bB as ae,b7 as re,bt as de,p as ue,M as g,T as U,aa as me,u as V,d5 as z,aw as M,w as ce,bH as pe,e as ge}from"./.pnpm-CJoARskO.js";import{_ as fe}from"./picker-BtisB09w.js";import{e as ve,c as _e,i as f,_ as ye}from"./index-vwMTrNXz.js";import{a as T,d as he}from"./patient-CS5NmE93.js";import{h as ke}from"./perm-DNF01X-C.js";import"./index-pOzDs5Ek.js";import"./index-CfDlwT2y.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./index-CvWXUesg.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";import"./usePaging-DKtm3XdH.js";const we={class:"note-timeline-wrap"},Ie={key:0,class:"timeline-actions"},Ce={class:"upload-trigger"},be={class:"upload-trigger"},xe={key:1,class:"note-timeline"},Ee={class:"timeline-date"},Ve={class:"timeline-body"},Ne={key:0,class:"timeline-content"},Se={key:1,class:"timeline-images"},De={key:2,class:"timeline-images"},Be={key:0,class:"thumb-wrap"},Ae={key:1,class:"file-wrap"},Pe=["href","title"],Ue={class:"file-name"},W=8e3,ze=ie({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(u,{emit:j}){const c=u,I=j,X=ue(()=>ke(["doctor.appointment/addDoctorNote"])),v=g(!1),C=g(""),N=g(!1),q=ve(),_=t=>q.getImageUrl(t),S=g([]),D=g([]),y=g(0),h=g(0),H=["jpg","jpeg","png","gif","bmp","webp","svg"],B=t=>{var i;const e=((i=t.split(".").pop())==null?void 0:i.toLowerCase().split("?")[0])||"";return H.includes(e)},$=t=>{var i;const e=t.split("/");return decodeURIComponent(((i=e[e.length-1])==null?void 0:i.split("?")[0])||"文件")},K=t=>t.filter(B).map(_),Z=(t,e)=>{const i=t.filter(B),b=t[e];return i.indexOf(b)},J=t=>t?t.split(`
|
||||
import{o as ie,R as oe,q as n,r as o,O as k,K as le,D as r,L as P,P as m,v as l,s as d,F as w,G as E,bB as ae,b7 as re,bt as de,p as ue,M as g,T as U,aa as me,u as V,d5 as z,aw as M,w as ce,bH as pe,e as ge}from"./.pnpm-BadRMC3e.js";import{_ as fe}from"./picker-BTjkKuWn.js";import{e as ve,c as _e,i as f,_ as ye}from"./index-Do7fAtr3.js";import{a as T,d as he}from"./patient-Ch_UV86r.js";import{h as ke}from"./perm-DsEdVTDW.js";import"./index-DP_2jise.js";import"./index-BNYVM0wQ.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./index-VEyOoZ0z.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";import"./usePaging-DMqUvFG-.js";const we={class:"note-timeline-wrap"},Ie={key:0,class:"timeline-actions"},Ce={class:"upload-trigger"},be={class:"upload-trigger"},xe={key:1,class:"note-timeline"},Ee={class:"timeline-date"},Ve={class:"timeline-body"},Ne={key:0,class:"timeline-content"},Se={key:1,class:"timeline-images"},De={key:2,class:"timeline-images"},Be={key:0,class:"thumb-wrap"},Ae={key:1,class:"file-wrap"},Pe=["href","title"],Ue={class:"file-name"},W=8e3,ze=ie({__name:"NoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(u,{emit:j}){const c=u,I=j,X=ue(()=>ke(["doctor.appointment/addDoctorNote"])),v=g(!1),C=g(""),N=g(!1),q=ve(),_=t=>q.getImageUrl(t),S=g([]),D=g([]),y=g(0),h=g(0),H=["jpg","jpeg","png","gif","bmp","webp","svg"],B=t=>{var i;const e=((i=t.split(".").pop())==null?void 0:i.toLowerCase().split("?")[0])||"";return H.includes(e)},$=t=>{var i;const e=t.split("/");return decodeURIComponent(((i=e[e.length-1])==null?void 0:i.split("?")[0])||"文件")},K=t=>t.filter(B).map(_),Z=(t,e)=>{const i=t.filter(B),b=t[e];return i.indexOf(b)},J=t=>t?t.split(`
|
||||
`).filter(Boolean):[],Q=t=>{if(!c.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=y.value){y.value=e.length;return}const i=e.slice(y.value);y.value=e.length,i.length>0&&T({diagnosis_id:c.diagnosisId,tongue_images:i}).then(()=>{f.msgSuccess("舌苔照片已添加"),I("refresh")})},Y=t=>{if(!c.diagnosisId)return;const e=Array.isArray(t)?t:[t];if(e.length<=h.value){h.value=e.length;return}const i=e.slice(h.value);h.value=e.length,i.length>0&&T({diagnosis_id:c.diagnosisId,report_files:i}).then(()=>{f.msgSuccess("检查报告已添加"),I("refresh")})};oe(()=>c.notes,()=>{S.value=[],D.value=[],y.value=0,h.value=0});const ee=async()=>{if(!c.diagnosisId){f.msgWarning("当前无诊单,无法添加备注");return}const t=C.value.trim();if(!t){f.msgWarning("请输入备注内容");return}N.value=!0;try{await T({diagnosis_id:c.diagnosisId,content:t}),f.msgSuccess("备注保存成功"),C.value="",v.value=!1,I("refresh")}catch(e){f.msgError((e==null?void 0:e.message)||"保存失败")}finally{N.value=!1}},A=async(t,e,i)=>{try{await ge.confirm("确认删除?","提示",{type:"warning"})}catch{return}await he({note_id:t,image_type:e,image_path:i}),f.msgSuccess("已删除"),I("refresh")};return(t,e)=>{const i=le,b=_e,F=fe,L=me,x=ce,te=ae,se=re,ne=de;return n(),o("div",we,[!u.readonly&&u.diagnosisId?(n(),o("div",Ie,[X.value?(n(),k(i,{key:0,type:"primary",plain:"",size:"small",onClick:e[0]||(e[0]=s=>v.value=!0)},{default:r(()=>[...e[6]||(e[6]=[P(" 添加备注 ",-1)])]),_:1})):m("",!0),l(F,{modelValue:S.value,"onUpdate:modelValue":e[1]||(e[1]=s=>S.value=s),limit:99,type:"image","exclude-domain":!0,onChange:Q},{upload:r(()=>[d("div",Ce,[l(b,{size:20,name:"el-icon-Plus"}),e[7]||(e[7]=d("span",null,"舌苔照片",-1))])]),_:1},8,["modelValue"]),l(F,{modelValue:D.value,"onUpdate:modelValue":e[2]||(e[2]=s=>D.value=s),limit:99,type:"file","exclude-domain":!0,onChange:Y},{upload:r(()=>[d("div",be,[l(b,{size:20,name:"el-icon-Plus"}),e[8]||(e[8]=d("span",null,"检查报告",-1))])]),_:1},8,["modelValue"])])):m("",!0),u.notes.length?(n(),o("div",xe,[(n(!0),o(w,null,E(u.notes,s=>{var R,G;return n(),o("div",{key:s.id,class:"timeline-node"},[e[11]||(e[11]=d("div",{class:"timeline-dot"},null,-1)),d("div",Ee,U(s.note_date),1),d("div",Ve,[s.content?(n(),o("div",Ne,[(n(!0),o(w,null,E(J(s.content),(a,p)=>(n(),o("div",{key:p,class:"content-line"},U(a),1))),128))])):m("",!0),(R=s.tongue_images)!=null&&R.length?(n(),o("div",Se,[e[9]||(e[9]=d("span",{class:"images-label"},"舌苔照片",-1)),(n(!0),o(w,null,E(s.tongue_images,(a,p)=>(n(),o("div",{key:p,class:"thumb-wrap"},[l(L,{src:_(a),"preview-src-list":s.tongue_images.map(_),"initial-index":p,"z-index":W,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),u.readonly?m("",!0):(n(),k(x,{key:0,class:"thumb-delete",onClick:M(O=>A(s.id,"tongue_images",a),["stop"])},{default:r(()=>[l(V(z))]),_:1},8,["onClick"]))]))),128))])):m("",!0),(G=s.report_files)!=null&&G.length?(n(),o("div",De,[e[10]||(e[10]=d("span",{class:"images-label"},"检查报告",-1)),(n(!0),o(w,null,E(s.report_files,(a,p)=>(n(),o(w,{key:p},[B(a)?(n(),o("div",Be,[l(L,{src:_(a),"preview-src-list":K(s.report_files),"initial-index":Z(s.report_files,p),"z-index":W,fit:"cover",class:"timeline-thumb","preview-teleported":""},null,8,["src","preview-src-list","initial-index"]),u.readonly?m("",!0):(n(),k(x,{key:0,class:"thumb-delete",onClick:M(O=>A(s.id,"report_files",a),["stop"])},{default:r(()=>[l(V(z))]),_:1},8,["onClick"]))])):(n(),o("div",Ae,[d("a",{href:_(a),target:"_blank",class:"file-link",title:$(a)},[l(x,{size:20},{default:r(()=>[l(V(pe))]),_:1}),d("span",Ue,U($(a)),1)],8,Pe),u.readonly?m("",!0):(n(),k(x,{key:0,class:"file-delete",onClick:M(O=>A(s.id,"report_files",a),["stop"])},{default:r(()=>[l(V(z))]),_:1},8,["onClick"]))]))],64))),128))])):m("",!0)])])}),128))])):m("",!0),!u.notes.length&&u.readonly?(n(),k(te,{key:2,description:"暂无备注","image-size":48})):m("",!0),l(ne,{modelValue:v.value,"onUpdate:modelValue":e[5]||(e[5]=s=>v.value=s),title:"添加备注",width:"480px","append-to-body":""},{footer:r(()=>[l(i,{onClick:e[4]||(e[4]=s=>v.value=!1)},{default:r(()=>[...e[12]||(e[12]=[P("取消",-1)])]),_:1}),l(i,{type:"primary",loading:N.value,onClick:ee},{default:r(()=>[...e[13]||(e[13]=[P("保存",-1)])]),_:1},8,["loading"])]),default:r(()=>[l(se,{modelValue:C.value,"onUpdate:modelValue":e[3]||(e[3]=s=>C.value=s),type:"textarea",rows:4,placeholder:"输入今日备注...",maxlength:"500","show-word-limit":""},null,8,["modelValue"])]),_:1},8,["modelValue"])])}}}),Ke=ye(ze,[["__scopeId","data-v-530ad386"]]);export{Ke as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
.embedded-panel[data-v-78709492]{min-height:420px}.panel-toolbar[data-v-78709492]{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px}.panel-toolbar h2[data-v-78709492]{margin:0;font-size:17px}.panel-toolbar p[data-v-78709492]{margin:4px 0 0;color:#8a95a6;font-size:12px}.toolbar-actions[data-v-78709492],.scope-chip[data-v-78709492]{display:flex;align-items:center;gap:8px}.scope-chip[data-v-78709492]{color:#0f766e;font-size:12px}.filter-panel[data-v-78709492]{display:flex;align-items:center;flex-wrap:wrap;gap:10px;padding:14px;border:1px solid #e3e8ef;border-radius:10px;background:#fbfcfd}.filter-panel[data-v-78709492] .el-select{width:132px}.keyword-input[data-v-78709492]{width:min(340px,100%)}.date-range[data-v-78709492]{width:250px}.metric-grid[data-v-78709492]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:14px 0}.metric-card[data-v-78709492]{min-height:82px;padding:14px 16px;border:1px solid #e3e8ef;border-radius:10px;background:#fff}.metric-card span[data-v-78709492],.metric-card small[data-v-78709492]{color:#8a95a6;font-size:12px}.metric-card strong[data-v-78709492]{display:block;margin:7px 0 3px;color:#172033;font-size:22px;line-height:1}.metric-warning[data-v-78709492]{border-color:#f3d8aa;background:#fffcf5}.metric-success[data-v-78709492]{border-color:#b9e2dc;background:#f7fcfb}.embedded-table[data-v-78709492]{width:100%;border:1px solid #e7ebf0;border-radius:9px;overflow:hidden}.embedded-table[data-v-78709492] th.el-table__cell{height:44px;color:#5f6b7d;background:#f7f9fb;font-weight:600}.embedded-table[data-v-78709492] .order-row-risk>td.el-table__cell{background:#fff8f7}.embedded-table[data-v-78709492] .order-row-done>td.el-table__cell{background:#f8fcfb}.primary-cell[data-v-78709492],.id-stack[data-v-78709492]{display:flex;flex-direction:column;gap:3px}.primary-cell strong[data-v-78709492],.id-stack strong[data-v-78709492]{color:#202939;font-size:13px}.primary-cell span[data-v-78709492],.id-stack span[data-v-78709492]{color:#8b96a8;font-size:12px}.amount[data-v-78709492]{color:#d04f3f;font-variant-numeric:tabular-nums}.order-actions[data-v-78709492]{display:flex;align-items:center;gap:4px;white-space:nowrap}.order-actions[data-v-78709492] .el-button+.el-button{margin-left:0}.danger-menu-item{color:var(--el-color-danger)!important}.pagination-wrap[data-v-78709492]{display:flex;justify-content:flex-end;padding-top:16px}@media (max-width: 1080px){.metric-grid[data-v-78709492]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (max-width: 760px){.panel-toolbar[data-v-78709492]{align-items:flex-start;flex-direction:column}.metric-grid[data-v-78709492]{grid-template-columns:1fr}.filter-panel[data-v-78709492]>*,.filter-panel[data-v-78709492] .el-select,.date-range[data-v-78709492]{width:100%}}
|
||||
@@ -0,0 +1 @@
|
||||
.embedded-panel[data-v-31a6be90]{min-height:420px}.panel-toolbar[data-v-31a6be90]{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px}.panel-toolbar h2[data-v-31a6be90]{margin:0;font-size:17px}.panel-toolbar p[data-v-31a6be90]{margin:4px 0 0;color:#8a95a6;font-size:12px}.toolbar-actions[data-v-31a6be90],.scope-chip[data-v-31a6be90]{display:flex;align-items:center;gap:8px}.scope-chip[data-v-31a6be90]{color:#0f766e;font-size:12px}.filter-panel[data-v-31a6be90]{display:flex;align-items:center;flex-wrap:wrap;gap:10px;padding:14px;border:1px solid #e3e8ef;border-radius:10px;background:#fbfcfd}.filter-panel[data-v-31a6be90] .el-select{width:132px}.keyword-input[data-v-31a6be90]{width:min(340px,100%)}.date-range[data-v-31a6be90]{width:250px}.metric-grid[data-v-31a6be90]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:14px 0}.metric-card[data-v-31a6be90]{min-height:82px;padding:14px 16px;border:1px solid #e3e8ef;border-radius:10px;background:#fff}.metric-card span[data-v-31a6be90],.metric-card small[data-v-31a6be90]{color:#8a95a6;font-size:12px}.metric-card strong[data-v-31a6be90]{display:block;margin:7px 0 3px;color:#172033;font-size:22px;line-height:1}.metric-warning[data-v-31a6be90]{border-color:#f3d8aa;background:#fffcf5}.metric-success[data-v-31a6be90]{border-color:#b9e2dc;background:#f7fcfb}.embedded-table[data-v-31a6be90]{width:100%;border:1px solid #e7ebf0;border-radius:9px;overflow:hidden}.embedded-table[data-v-31a6be90] th.el-table__cell{height:44px;color:#5f6b7d;background:#f7f9fb;font-weight:600}.embedded-table[data-v-31a6be90] .order-row-risk>td.el-table__cell{background:#fff8f7}.embedded-table[data-v-31a6be90] .order-row-done>td.el-table__cell{background:#f8fcfb}.primary-cell[data-v-31a6be90],.id-stack[data-v-31a6be90]{display:flex;flex-direction:column;gap:3px}.primary-cell strong[data-v-31a6be90],.id-stack strong[data-v-31a6be90]{color:#202939;font-size:13px}.primary-cell span[data-v-31a6be90],.id-stack span[data-v-31a6be90]{color:#8b96a8;font-size:12px}.amount[data-v-31a6be90]{color:#d04f3f;font-variant-numeric:tabular-nums}.amount-excluded[data-v-31a6be90]{color:#9aa4b2;font-size:11px}.order-actions[data-v-31a6be90]{display:flex;align-items:center;gap:4px;white-space:nowrap}.order-actions[data-v-31a6be90] .el-button+.el-button{margin-left:0}.danger-menu-item{color:var(--el-color-danger)!important}.pagination-wrap[data-v-31a6be90]{display:flex;justify-content:flex-end;padding-top:16px}@media (max-width: 1080px){.metric-grid[data-v-31a6be90]{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (max-width: 760px){.panel-toolbar[data-v-31a6be90]{align-items:flex-start;flex-direction:column}.metric-grid[data-v-31a6be90]{grid-template-columns:1fr}.filter-panel[data-v-31a6be90]>*,.filter-panel[data-v-31a6be90] .el-select,.date-range[data-v-31a6be90]{width:100%}}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{m as I,f as p,a as w}from"./diag-display-DCz_VAqj.js";import{o as y,q as x,r as B,s as e,T as a,u as i,P as N}from"./.pnpm-CJoARskO.js";import{_ as V}from"./index-vwMTrNXz.js";const b={class:"card patient-card"},q={class:"patient-hero"},D={class:"patient-name"},E={class:"patient-meta"},G={key:0},S=y({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(T,n)=>{var d,s,o,c,m,l,r,g,u,f,h,v,k,P,C;return x(),B("div",b,[n[0]||(n[0]=e("div",{class:"card-title"},"患者信息",-1)),e("div",q,[e("div",D,a(((d=t.apt)==null?void 0:d.patient_name)||"—"),1),e("div",E,[e("div",null,a(i(I)((s=t.apt)==null?void 0:s.patient_phone))+" · "+a(i(p)((o=t.diag)==null?void 0:o.gender))+" · "+a(((c=t.diag)==null?void 0:c.age)!=null?t.diag.age+"岁":"—"),1),e("div",null,a((m=t.diag)!=null&&m.height?t.diag.height+"cm":"—")+" / "+a((l=t.diag)!=null&&l.weight?t.diag.weight+"kg":"—")+" · "+a(((r=t.diag)==null?void 0:r.region)||"—"),1),e("div",null," 预约:"+a((g=t.apt)==null?void 0:g.appointment_date)+" "+a((u=t.apt)==null?void 0:u.appointment_time)+" · "+a(i(w)((f=t.apt)==null?void 0:f.period)),1),e("div",null,"医生:"+a(((h=t.apt)==null?void 0:h.doctor_name)||"—")+" 客服:"+a(((v=t.apt)==null?void 0:v.assistant_name)||"—"),1),e("div",null," 状态:"+a(((k=t.apt)==null?void 0:k.status_desc)||"—")+" · "+a((P=t.apt)!=null&&P.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(x(),B("div",G,"备注:"+a(t.apt.remark),1)):N("",!0)])])])}}}),F=V(S,[["__scopeId","data-v-1b0de09c"]]);export{F as default};
|
||||
import{m as I,f as p,a as w}from"./diag-display-DCz_VAqj.js";import{o as y,q as x,r as B,s as e,T as a,u as i,P as N}from"./.pnpm-BadRMC3e.js";import{_ as V}from"./index-Do7fAtr3.js";const b={class:"card patient-card"},q={class:"patient-hero"},D={class:"patient-name"},E={class:"patient-meta"},G={key:0},S=y({__name:"PatientInfoCard",props:{apt:{default:()=>({})},diag:{default:()=>({})}},setup(t){return(T,n)=>{var d,s,o,c,m,l,r,g,u,f,h,v,k,P,C;return x(),B("div",b,[n[0]||(n[0]=e("div",{class:"card-title"},"患者信息",-1)),e("div",q,[e("div",D,a(((d=t.apt)==null?void 0:d.patient_name)||"—"),1),e("div",E,[e("div",null,a(i(I)((s=t.apt)==null?void 0:s.patient_phone))+" · "+a(i(p)((o=t.diag)==null?void 0:o.gender))+" · "+a(((c=t.diag)==null?void 0:c.age)!=null?t.diag.age+"岁":"—"),1),e("div",null,a((m=t.diag)!=null&&m.height?t.diag.height+"cm":"—")+" / "+a((l=t.diag)!=null&&l.weight?t.diag.weight+"kg":"—")+" · "+a(((r=t.diag)==null?void 0:r.region)||"—"),1),e("div",null," 预约:"+a((g=t.apt)==null?void 0:g.appointment_date)+" "+a((u=t.apt)==null?void 0:u.appointment_time)+" · "+a(i(w)((f=t.apt)==null?void 0:f.period)),1),e("div",null,"医生:"+a(((h=t.apt)==null?void 0:h.doctor_name)||"—")+" 客服:"+a(((v=t.apt)==null?void 0:v.assistant_name)||"—"),1),e("div",null," 状态:"+a(((k=t.apt)==null?void 0:k.status_desc)||"—")+" · "+a((P=t.apt)!=null&&P.has_prescription?"已开方":"未开方"),1),(C=t.apt)!=null&&C.remark?(x(),B("div",G,"备注:"+a(t.apt.remark),1)):N("",!0)])])])}}}),F=V(S,[["__scopeId","data-v-1b0de09c"]]);export{F as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{o as L,q as i,r as c,s as d,F as v,O as m,bq as q,D as _,L as k,P as y,G as B,p as f,T as w}from"./.pnpm-CJoARskO.js";import H from"./RecordingVideoPlayer-BdmuK8u1.js";import{e as I,_ as P}from"./index-vwMTrNXz.js";const R={key:0,class:"recording-list"},C={class:"recording-item"},V={key:1,class:"recording-alternates"},N={class:"recording-alternates__links"},D={key:1,class:"text-gray-400"},E=L({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(l){const $=l,h=I(),u=f(()=>{const e=$.urls||[],t=new Set,r=[];for(const o of e){const s=String(o??"").trim();!s||t.has(s)||(t.add(s),r.push(s))}return r}),a=f(()=>{const e=u.value;if(!e.length)return null;const t=e.find(n=>/\.mp4(\?|#|$)/i.test(n));if(t)return t;const r=e.find(n=>/\.m3u8(\?|#|$)/i.test(n)&&/vod-qcloud\.com/i.test(n));if(r)return r;const o=e.find(n=>/\.m3u8(\?|#|$)/i.test(n)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(n));if(o)return o;const s=e.find(n=>/\.m3u8(\?|#|$)/i.test(n));return s||e[0]}),p=f(()=>{const e=u.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function b(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function S(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=q;return u.value.length?(i(),c("div",R,[d("div",C,[a.value?(i(),c(v,{key:0},[b(a.value)?(i(),m(H,{key:`${l.recordId}-${a.value}`,src:a.value},null,8,["src"])):(i(),m(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),p.value.length?(i(),c("div",V,[t[1]||(t[1]=d("span",{class:"recording-alternates__label"},"备用地址",-1)),d("div",N,[(i(!0),c(v,null,B(p.value,(o,s)=>(i(),m(r,{key:`${l.recordId}-alt-${s}-${o.slice(-32)}`,href:g(o),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(w(S(o,s)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(i(),c("span",D,"暂无"))}}}),x=P(E,[["__scopeId","data-v-d67b2e91"]]);export{x as default};
|
||||
import{o as L,q as i,r as c,s as d,F as v,O as m,bq as q,D as _,L as k,P as y,G as B,p as f,T as w}from"./.pnpm-BadRMC3e.js";import H from"./RecordingVideoPlayer-ActCwTef.js";import{e as I,_ as P}from"./index-Do7fAtr3.js";const R={key:0,class:"recording-list"},C={class:"recording-item"},V={key:1,class:"recording-alternates"},N={class:"recording-alternates__links"},D={key:1,class:"text-gray-400"},E=L({__name:"RecordingPlaybackBlock",props:{recordId:{},urls:{}},setup(l){const $=l,h=I(),u=f(()=>{const e=$.urls||[],t=new Set,r=[];for(const o of e){const s=String(o??"").trim();!s||t.has(s)||(t.add(s),r.push(s))}return r}),a=f(()=>{const e=u.value;if(!e.length)return null;const t=e.find(n=>/\.mp4(\?|#|$)/i.test(n));if(t)return t;const r=e.find(n=>/\.m3u8(\?|#|$)/i.test(n)&&/vod-qcloud\.com/i.test(n));if(r)return r;const o=e.find(n=>/\.m3u8(\?|#|$)/i.test(n)&&/\.cos\.[^/]+\.myqcloud\.com/i.test(n));if(o)return o;const s=e.find(n=>/\.m3u8(\?|#|$)/i.test(n));return s||e[0]}),p=f(()=>{const e=u.value,t=a.value;return t?e.filter(r=>r!==t):e.slice(1)});function b(e){if(!e||typeof e!="string")return!1;const t=e.trim();return/^https?:\/\//i.test(t)?/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t):/\.(mp4|webm|ogg|mov|mkv|m4v|m3u8)(\?|#|$)/i.test(t)||t.startsWith("/uploads/")}function g(e){return h.getImageUrl(String(e||"").trim())}function S(e,t){const r=e.trim();return/\.mp4(\?|#|$)/i.test(r)?`MP4 ${t+1}`:/vod-qcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`点播 ${t+1}`:/\.cos\.[^/]+\.myqcloud\.com/i.test(r)&&/\.m3u8/i.test(r)?`COS HLS ${t+1}`:/\.m3u8/i.test(r)?`HLS ${t+1}`:`链接 ${t+1}`}return(e,t)=>{const r=q;return u.value.length?(i(),c("div",R,[d("div",C,[a.value?(i(),c(v,{key:0},[b(a.value)?(i(),m(H,{key:`${l.recordId}-${a.value}`,src:a.value},null,8,["src"])):(i(),m(r,{key:1,href:g(a.value),target:"_blank",type:"primary"},{default:_(()=>[...t[0]||(t[0]=[k(" 打开回放 ",-1)])]),_:1},8,["href"]))],64)):y("",!0),p.value.length?(i(),c("div",V,[t[1]||(t[1]=d("span",{class:"recording-alternates__label"},"备用地址",-1)),d("div",N,[(i(!0),c(v,null,B(p.value,(o,s)=>(i(),m(r,{key:`${l.recordId}-alt-${s}-${o.slice(-32)}`,href:g(o),target:"_blank",type:"primary",class:"recording-alternates__link"},{default:_(()=>[k(w(S(o,s)),1)]),_:2},1032,["href"]))),128))])])):y("",!0)])])):(i(),c("span",D,"暂无"))}}}),x=P(E,[["__scopeId","data-v-d67b2e91"]]);export{x as default};
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/.pnpm-CJoARskO.js","assets/.pnpm-BtiqMGM_.css"])))=>i.map(i=>d[i]);
|
||||
import{o as K,ap as z,n as S,R as H,aq as G,q as v,r as p,s as f,ac as O,ad as W,b8 as $,aw as j,F as w,v as k,D as I,u as J,b5 as Q,w as X,P,L as Y,bq as Z,M as _,p as E,al as ee}from"./.pnpm-CJoARskO.js";import{e as ae,_ as ne}from"./index-vwMTrNXz.js";const te={class:"recording-playback__shell"},re=["preload"],oe=["onKeydown"],le=K({__name:"RecordingVideoPlayer",props:{src:{}},setup(B){const V=B,A=ae(),b=_(null),m=_(null),c=_(!1),s=_(!1),t=_(!1);let d=null,r=null,l=0,u=null;function i(){u!==null&&(clearTimeout(u),u=null)}const y=E(()=>{const e=String(V.src||"").trim();return e?U(A.getImageUrl(e)):""}),D=E(()=>{if(!s.value)return"none";const e=y.value;return e&&!L(e)?"metadata":"none"}),q=E(()=>!s.value||t.value);function U(e){if(!e||!/^https?:\/\//i.test(e))return e;try{const a=new URL(e),n=a.hostname.toLowerCase();if(n.includes("myqcloud.com")||n.includes("vod-qcloud.com")||n.includes("vod-qcloud"))return a.pathname.includes("+")&&(a.pathname=a.pathname.replace(/\+/g,"%2B")),a.href}catch{}return e}function L(e){return/\.m3u8(\?|#|$)/i.test(e)}function C(){var n;t.value=!1,i();const e=m.value,a=(n=e==null?void 0:e.error)==null?void 0:n.code;a===MediaError.MEDIA_ERR_ABORTED||a===1||(c.value=!0)}function h(){d&&(d.destroy(),d=null);const e=m.value;e&&(e.pause(),e.removeAttribute("src"),e.load())}function R(e,a){let n=!1;const o=()=>{n||a!==l||(n=!0,i(),t.value=!1)};e.addEventListener("loadedmetadata",()=>o(),{once:!0}),e.addEventListener("loadeddata",()=>o(),{once:!0}),e.addEventListener("canplay",()=>o(),{once:!0}),e.addEventListener("playing",()=>o(),{once:!0}),e.addEventListener("progress",()=>{try{e.buffered.length>0&&e.buffered.end(0)>0&&o("progress-buffered")}catch{}},{passive:!0})}async function g(){if(!s.value)return;await S();const e=m.value,a=y.value;if(!e)return;if(!a){t.value=!1,i();return}const n=++l;if(t.value=!0,i(),h(),c.value=!1,L(a)){try{const o=await ee(()=>import("./.pnpm-CJoARskO.js").then(M=>M.dL),__vite__mapDeps([0,1])),x=o.default;if(x.isSupported()){d=new x({enableWorker:!0,lowLatencyMode:!1,maxBufferLength:20,maxMaxBufferLength:120}),d.on(o.Events.MANIFEST_PARSED,()=>{n===l&&(t.value=!1,i())}),d.on(o.Events.ERROR,(M,F)=>{if(F.fatal){if(n!==l)return;t.value=!1,i(),c.value=!0,h()}}),d.loadSource(a),d.attachMedia(e),u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},45e3);return}if(e.canPlayType("application/vnd.apple.mpegurl")){R(e,n),e.src=a,u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},45e3);return}}catch{if(n!==l)return;t.value=!1,i(),c.value=!0;return}if(n!==l)return;t.value=!1,i(),c.value=!0;return}R(e,n),e.src=a,u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},25e3)}function N(){const e=b.value;if(!e||typeof IntersectionObserver>"u"){s.value=!0,g();return}r=new IntersectionObserver(a=>{for(const n of a)if(n.isIntersecting){r==null||r.disconnect(),r=null,s.value=!0,g();break}},{root:null,rootMargin:"240px 0px",threshold:0}),r.observe(e)}function T(){r==null||r.disconnect(),r=null,s.value||(s.value=!0),g()}return z(()=>{S(()=>{N()})}),H(y,()=>{s.value&&g()}),G(()=>{r==null||r.disconnect(),r=null,i(),l++,h()}),(e,a)=>{const n=X,o=Z;return v(),p("div",{ref_key:"wrapRef",ref:b,class:"recording-playback"},[f("div",te,[O(f("video",{ref_key:"videoRef",ref:m,class:"recording-video",controls:"",playsinline:"","webkit-playsinline":"",preload:D.value,onError:C},null,40,re),[[W,!c.value]]),!c.value&&q.value?(v(),p("div",{key:0,class:"recording-playback__overlay",role:"button",tabindex:"0",onClick:T,onKeydown:$(j(T,["prevent"]),["enter"])},[s.value?(v(),p(w,{key:1},[k(n,{class:"recording-playback__spin"},{default:I(()=>[k(J(Q))]),_:1}),a[2]||(a[2]=f("span",null,"正在加载…",-1))],64)):(v(),p(w,{key:0},[a[0]||(a[0]=f("span",{class:"recording-playback__overlay-title"},"预览待加载",-1)),a[1]||(a[1]=f("span",{class:"recording-playback__overlay-desc"},"滚动至此处将自动加载(减轻并发卡顿),也可点击立即加载",-1))],64))],40,oe)):P("",!0)]),c.value?(v(),p(w,{key:0},[k(o,{href:y.value||"#",target:"_blank",type:"primary"},{default:I(()=>[...a[3]||(a[3]=[Y("在新窗口打开",-1)])]),_:1},8,["href"]),a[4]||(a[4]=f("div",{class:"recording-playback__hint"}," 内嵌播放失败(编码不支持、跨域或私有存储等)时可尝试在新窗口打开。 ",-1))],64)):P("",!0)],512)}}}),ie=ne(le,[["__scopeId","data-v-749b0199"]]);export{ie as default};
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/.pnpm-BadRMC3e.js","assets/.pnpm-BtiqMGM_.css"])))=>i.map(i=>d[i]);
|
||||
import{o as K,ap as z,n as S,R as H,aq as G,q as v,r as p,s as f,ac as O,ad as W,b8 as $,aw as j,F as w,v as k,D as I,u as J,b5 as Q,w as X,P,L as Y,bq as Z,M as _,p as E,al as ee}from"./.pnpm-BadRMC3e.js";import{e as ae,_ as ne}from"./index-Do7fAtr3.js";const te={class:"recording-playback__shell"},re=["preload"],oe=["onKeydown"],le=K({__name:"RecordingVideoPlayer",props:{src:{}},setup(B){const V=B,A=ae(),b=_(null),m=_(null),c=_(!1),s=_(!1),t=_(!1);let d=null,r=null,l=0,u=null;function i(){u!==null&&(clearTimeout(u),u=null)}const y=E(()=>{const e=String(V.src||"").trim();return e?U(A.getImageUrl(e)):""}),D=E(()=>{if(!s.value)return"none";const e=y.value;return e&&!L(e)?"metadata":"none"}),q=E(()=>!s.value||t.value);function U(e){if(!e||!/^https?:\/\//i.test(e))return e;try{const a=new URL(e),n=a.hostname.toLowerCase();if(n.includes("myqcloud.com")||n.includes("vod-qcloud.com")||n.includes("vod-qcloud"))return a.pathname.includes("+")&&(a.pathname=a.pathname.replace(/\+/g,"%2B")),a.href}catch{}return e}function L(e){return/\.m3u8(\?|#|$)/i.test(e)}function C(){var n;t.value=!1,i();const e=m.value,a=(n=e==null?void 0:e.error)==null?void 0:n.code;a===MediaError.MEDIA_ERR_ABORTED||a===1||(c.value=!0)}function h(){d&&(d.destroy(),d=null);const e=m.value;e&&(e.pause(),e.removeAttribute("src"),e.load())}function R(e,a){let n=!1;const o=()=>{n||a!==l||(n=!0,i(),t.value=!1)};e.addEventListener("loadedmetadata",()=>o(),{once:!0}),e.addEventListener("loadeddata",()=>o(),{once:!0}),e.addEventListener("canplay",()=>o(),{once:!0}),e.addEventListener("playing",()=>o(),{once:!0}),e.addEventListener("progress",()=>{try{e.buffered.length>0&&e.buffered.end(0)>0&&o("progress-buffered")}catch{}},{passive:!0})}async function g(){if(!s.value)return;await S();const e=m.value,a=y.value;if(!e)return;if(!a){t.value=!1,i();return}const n=++l;if(t.value=!0,i(),h(),c.value=!1,L(a)){try{const o=await ee(()=>import("./.pnpm-BadRMC3e.js").then(M=>M.dL),__vite__mapDeps([0,1])),x=o.default;if(x.isSupported()){d=new x({enableWorker:!0,lowLatencyMode:!1,maxBufferLength:20,maxMaxBufferLength:120}),d.on(o.Events.MANIFEST_PARSED,()=>{n===l&&(t.value=!1,i())}),d.on(o.Events.ERROR,(M,F)=>{if(F.fatal){if(n!==l)return;t.value=!1,i(),c.value=!0,h()}}),d.loadSource(a),d.attachMedia(e),u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},45e3);return}if(e.canPlayType("application/vnd.apple.mpegurl")){R(e,n),e.src=a,u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},45e3);return}}catch{if(n!==l)return;t.value=!1,i(),c.value=!0;return}if(n!==l)return;t.value=!1,i(),c.value=!0;return}R(e,n),e.src=a,u=setTimeout(()=>{u=null,n===l&&t.value&&(t.value=!1)},25e3)}function N(){const e=b.value;if(!e||typeof IntersectionObserver>"u"){s.value=!0,g();return}r=new IntersectionObserver(a=>{for(const n of a)if(n.isIntersecting){r==null||r.disconnect(),r=null,s.value=!0,g();break}},{root:null,rootMargin:"240px 0px",threshold:0}),r.observe(e)}function T(){r==null||r.disconnect(),r=null,s.value||(s.value=!0),g()}return z(()=>{S(()=>{N()})}),H(y,()=>{s.value&&g()}),G(()=>{r==null||r.disconnect(),r=null,i(),l++,h()}),(e,a)=>{const n=X,o=Z;return v(),p("div",{ref_key:"wrapRef",ref:b,class:"recording-playback"},[f("div",te,[O(f("video",{ref_key:"videoRef",ref:m,class:"recording-video",controls:"",playsinline:"","webkit-playsinline":"",preload:D.value,onError:C},null,40,re),[[W,!c.value]]),!c.value&&q.value?(v(),p("div",{key:0,class:"recording-playback__overlay",role:"button",tabindex:"0",onClick:T,onKeydown:$(j(T,["prevent"]),["enter"])},[s.value?(v(),p(w,{key:1},[k(n,{class:"recording-playback__spin"},{default:I(()=>[k(J(Q))]),_:1}),a[2]||(a[2]=f("span",null,"正在加载…",-1))],64)):(v(),p(w,{key:0},[a[0]||(a[0]=f("span",{class:"recording-playback__overlay-title"},"预览待加载",-1)),a[1]||(a[1]=f("span",{class:"recording-playback__overlay-desc"},"滚动至此处将自动加载(减轻并发卡顿),也可点击立即加载",-1))],64))],40,oe)):P("",!0)]),c.value?(v(),p(w,{key:0},[k(o,{href:y.value||"#",target:"_blank",type:"primary"},{default:I(()=>[...a[3]||(a[3]=[Y("在新窗口打开",-1)])]),_:1},8,["href"]),a[4]||(a[4]=f("div",{class:"recording-playback__hint"}," 内嵌播放失败(编码不支持、跨域或私有存储等)时可尝试在新窗口打开。 ",-1))],64)):P("",!0)],512)}}}),ie=ne(le,[["__scopeId","data-v-749b0199"]]);export{ie as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,2 +1,2 @@
|
||||
import{o as T,q as e,r as t,v as m,b7 as V,s as d,D as w,L as I,K as E,P as r,F as u,G as v,T as g,O as C,bB as z,M as p}from"./.pnpm-CJoARskO.js";import{$ as L}from"./tcm-BBytqd3B.js";import{i as M,_ as S}from"./index-vwMTrNXz.js";/* empty css */const D={class:"tracking-timeline-wrap"},F={key:0,class:"timeline-input"},H={class:"input-actions"},q={key:1,class:"tracking-timeline"},A={class:"timeline-date"},G={class:"timeline-body"},K={key:0,class:"timeline-content"},O=T({__name:"TrackingNoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(n,{emit:f}){const c=n,y=f,o=p(""),l=p(!1),_=s=>s?s.split(`
|
||||
import{o as T,q as e,r as t,v as m,b7 as V,s as d,D as w,L as I,K as E,P as r,F as u,G as v,T as g,O as C,bB as z,M as p}from"./.pnpm-BadRMC3e.js";import{$ as L}from"./tcm-BDu8-eLA.js";import{i as M,_ as S}from"./index-Do7fAtr3.js";/* empty css */const D={class:"tracking-timeline-wrap"},F={key:0,class:"timeline-input"},H={class:"input-actions"},q={key:1,class:"tracking-timeline"},A={class:"timeline-date"},G={class:"timeline-body"},K={key:0,class:"timeline-content"},O=T({__name:"TrackingNoteTimeline",props:{notes:{},diagnosisId:{},readonly:{type:Boolean}},emits:["refresh"],setup(n,{emit:f}){const c=n,y=f,o=p(""),l=p(!1),_=s=>s?s.split(`
|
||||
`).filter(Boolean):[],k=async()=>{if(!c.diagnosisId)return;const s=o.value.trim();if(s){l.value=!0;try{await L({diagnosis_id:c.diagnosisId,tracking_content:s}),M.msgSuccess("已添加"),o.value="",y("refresh")}finally{l.value=!1}}};return(s,i)=>{const b=V,h=E,B=z;return e(),t("div",D,[!n.readonly&&n.diagnosisId?(e(),t("div",F,[m(b,{modelValue:o.value,"onUpdate:modelValue":i[0]||(i[0]=a=>o.value=a),type:"textarea",rows:2,placeholder:"输入跟踪备注,回车换行;保存后将以「[HH:MM] 内容」追加到当天记录",maxlength:"1000","show-word-limit":"",resize:"none",disabled:l.value},null,8,["modelValue","disabled"]),d("div",H,[m(h,{type:"primary",size:"small",loading:l.value,disabled:!o.value.trim(),onClick:k},{default:w(()=>[...i[1]||(i[1]=[I(" 添加 ",-1)])]),_:1},8,["loading","disabled"])])])):r("",!0),n.notes.length?(e(),t("div",q,[(e(!0),t(u,null,v(n.notes,a=>(e(),t("div",{key:a.id,class:"timeline-node"},[i[2]||(i[2]=d("div",{class:"timeline-dot"},null,-1)),d("div",A,g(a.note_date),1),d("div",G,[a.content?(e(),t("div",K,[(e(!0),t(u,null,v(_(a.content),(x,N)=>(e(),t("div",{key:N,class:"content-line"},g(x),1))),128))])):r("",!0)])]))),128))])):r("",!0),!n.notes.length&&n.readonly?(e(),C(B,{key:2,description:"暂无跟踪备注","image-size":48})):r("",!0)])}}}),J=S(O,[["__scopeId","data-v-82b635bd"]]);export{J as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-C7A7XOyZ.js";import"./.pnpm-CJoARskO.js";import"./index-pOzDs5Ek.js";import"./index-vwMTrNXz.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-UMSPjaUQ.js";import"./.pnpm-BadRMC3e.js";import"./index-DP_2jise.js";import"./index-Do7fAtr3.js";export{o as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as h,R as v,q,O as B,D as l,s as D,v as t,b9 as F,u as n,b6 as I,L as u,T as w,bc as j,bf as S,b7 as T,a8 as y,ba as U,p as G}from"./.pnpm-CJoARskO.js";import{_ as L}from"./index-pOzDs5Ek.js";import{i as V}from"./index-vwMTrNXz.js";const M={class:"pr-8"},H=h({__name:"account-adjust",props:{show:{type:Boolean,required:!0},value:{type:[Number,String],required:!0}},emits:["update:show","confirm"],setup(d,{emit:k}){const s=y(),i=d,f=k,o=U({action:1,num:"",remark:""}),m=y(),c=G(()=>Number(i.value)+Number(o.num)*(o.action==1?1:-1)),R={num:[{required:!0,message:"请输入调整的金额"}]},g=e=>{if(e.includes("-"))return V.msgError("请输入正整数");o.num=e},x=async()=>{var e;await((e=s.value)==null?void 0:e.validate()),f("confirm",o)},E=()=>{var e;f("update:show",!1),(e=s.value)==null||e.resetFields()};return v(()=>i.show,e=>{var a,r;e?(a=m.value)==null||a.open():(r=m.value)==null||r.close()}),v(c,e=>{e<0&&(V.msgError("调整后余额需大于0"),o.num="")}),(e,a)=>{const r=I,_=S,C=j,b=T,N=F;return q(),B(L,{ref_key:"popupRef",ref:m,title:"余额调整",width:"500px",onConfirm:x,async:!0,onClose:E},{default:l(()=>[D("div",M,[t(N,{ref_key:"formRef",ref:s,model:n(o),"label-width":"120px",rules:R},{default:l(()=>[t(r,{label:"当前余额"},{default:l(()=>[u("¥ "+w(d.value),1)]),_:1}),t(r,{label:"余额增减",required:"",prop:"action"},{default:l(()=>[t(C,{modelValue:n(o).action,"onUpdate:modelValue":a[0]||(a[0]=p=>n(o).action=p)},{default:l(()=>[t(_,{value:1},{default:l(()=>[...a[2]||(a[2]=[u("增加余额",-1)])]),_:1}),t(_,{value:2},{default:l(()=>[...a[3]||(a[3]=[u("扣减余额",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),t(r,{label:"调整余额",prop:"num"},{default:l(()=>[t(b,{"model-value":n(o).num,placeholder:"请输入调整的金额",type:"number",onInput:g},null,8,["model-value"])]),_:1}),t(r,{label:"调整后余额"},{default:l(()=>[u(" ¥ "+w(n(c)),1)]),_:1}),t(r,{label:"备注",prop:"remark"},{default:l(()=>[t(b,{modelValue:n(o).remark,"onUpdate:modelValue":a[1]||(a[1]=p=>n(o).remark=p),type:"textarea",rows:4},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])])]),_:1},512)}}});export{H as _};
|
||||
import{o as h,R as v,q,O as B,D as l,s as D,v as t,b9 as F,u as n,b6 as I,L as u,T as w,bc as j,bf as S,b7 as T,a8 as y,ba as U,p as G}from"./.pnpm-BadRMC3e.js";import{_ as L}from"./index-DP_2jise.js";import{i as V}from"./index-Do7fAtr3.js";const M={class:"pr-8"},H=h({__name:"account-adjust",props:{show:{type:Boolean,required:!0},value:{type:[Number,String],required:!0}},emits:["update:show","confirm"],setup(d,{emit:k}){const s=y(),i=d,f=k,o=U({action:1,num:"",remark:""}),m=y(),c=G(()=>Number(i.value)+Number(o.num)*(o.action==1?1:-1)),R={num:[{required:!0,message:"请输入调整的金额"}]},g=e=>{if(e.includes("-"))return V.msgError("请输入正整数");o.num=e},x=async()=>{var e;await((e=s.value)==null?void 0:e.validate()),f("confirm",o)},E=()=>{var e;f("update:show",!1),(e=s.value)==null||e.resetFields()};return v(()=>i.show,e=>{var a,r;e?(a=m.value)==null||a.open():(r=m.value)==null||r.close()}),v(c,e=>{e<0&&(V.msgError("调整后余额需大于0"),o.num="")}),(e,a)=>{const r=I,_=S,C=j,b=T,N=F;return q(),B(L,{ref_key:"popupRef",ref:m,title:"余额调整",width:"500px",onConfirm:x,async:!0,onClose:E},{default:l(()=>[D("div",M,[t(N,{ref_key:"formRef",ref:s,model:n(o),"label-width":"120px",rules:R},{default:l(()=>[t(r,{label:"当前余额"},{default:l(()=>[u("¥ "+w(d.value),1)]),_:1}),t(r,{label:"余额增减",required:"",prop:"action"},{default:l(()=>[t(C,{modelValue:n(o).action,"onUpdate:modelValue":a[0]||(a[0]=p=>n(o).action=p)},{default:l(()=>[t(_,{value:1},{default:l(()=>[...a[2]||(a[2]=[u("增加余额",-1)])]),_:1}),t(_,{value:2},{default:l(()=>[...a[3]||(a[3]=[u("扣减余额",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),t(r,{label:"调整余额",prop:"num"},{default:l(()=>[t(b,{"model-value":n(o).num,placeholder:"请输入调整的金额",type:"number",onInput:g},null,8,["model-value"])]),_:1}),t(r,{label:"调整后余额"},{default:l(()=>[u(" ¥ "+w(n(c)),1)]),_:1}),t(r,{label:"备注",prop:"remark"},{default:l(()=>[t(b,{modelValue:n(o).remark,"onUpdate:modelValue":a[1]||(a[1]=p=>n(o).remark=p),type:"textarea",rows:4},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])])]),_:1},512)}}});export{H as _};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-D3OZX8cR.js";import"./.pnpm-BadRMC3e.js";import"./index-VEyOoZ0z.js";import"./index-Do7fAtr3.js";import"./picker-CCYShZB-.js";import"./index-DP_2jise.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./article-DHOuLFy5.js";import"./usePaging-DMqUvFG-.js";import"./picker-BTjkKuWn.js";import"./index-BNYVM0wQ.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-1RdLHUAJ.js";import"./.pnpm-CJoARskO.js";import"./index-CvWXUesg.js";import"./index-vwMTrNXz.js";import"./picker-_oSJRmY0.js";import"./index-pOzDs5Ek.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./article-DfNLL0Dd.js";import"./usePaging-DKtm3XdH.js";import"./picker-BtisB09w.js";import"./index-CfDlwT2y.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";export{o as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as E,q as p,r as C,s as l,v as a,u as c,bQ as B,C as N,D as d,O as $,b7 as z,b6 as D,I,K as A,L,p as R}from"./.pnpm-CJoARskO.js";import{_ as q}from"./index-CvWXUesg.js";import{_ as F}from"./picker-_oSJRmY0.js";import{_ as K}from"./picker-BtisB09w.js";import{c as O,i as r}from"./index-vwMTrNXz.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},Q={class:"upload-btn w-[60px] h-[60px]"},S={class:"ml-3 flex-1"},T={class:"flex items-center"},j={class:"flex items-center mt-[18px]"},G={class:"flex-1 flex items-center"},H={class:"drag-move cursor-move ml-auto"},Z=E({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=R({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}个`)},v=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}个`);m.value.splice(s,1)};return(s,e)=>{const u=O,g=K,b=z,h=F,k=I,w=D,y=q,U=A;return p(),C("div",null,[l("div",null,[a(c(B),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>N(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:i})=>[(p(),$(y,{class:"w-[467px]",key:i,onClose:n=>v(i)},{default:d(()=>[l("div",P,[a(g,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",Q,[a(u,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",S,[l("div",T,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(b,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",j,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(h,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",G,[a(k,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",H,[a(u,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[L("添加",-1)])]),_:1})])])}}});export{Z as _};
|
||||
import{o as E,q as p,r as C,s as l,v as a,u as c,bQ as B,C as N,D as d,O as $,b7 as z,b6 as D,I,K as A,L,p as R}from"./.pnpm-BadRMC3e.js";import{_ as q}from"./index-VEyOoZ0z.js";import{_ as F}from"./picker-CCYShZB-.js";import{_ as K}from"./picker-BTjkKuWn.js";import{c as O,i as r}from"./index-Do7fAtr3.js";const P={class:"bg-fill-light flex items-center w-full p-4 mb-4"},Q={class:"upload-btn w-[60px] h-[60px]"},S={class:"ml-3 flex-1"},T={class:"flex items-center"},j={class:"flex items-center mt-[18px]"},G={class:"flex-1 flex items-center"},H={class:"drag-move cursor-move ml-auto"},Z=E({__name:"add-nav",props:{modelValue:{type:Array,default:()=>[]},max:{type:Number,default:100},min:{type:Number,default:1}},emits:["update:modelValue"],setup(_,{emit:f}){const t=_,V=f,m=R({get(){return t.modelValue},set(s){V("update:modelValue",s)}}),x=()=>{var s;((s=t.modelValue)==null?void 0:s.length)<t.max?m.value.push({image:"",name:"导航名称",link:{},is_show:"1"}):r.msgError(`最多添加${t.max}个`)},v=s=>{var e;if(((e=t.modelValue)==null?void 0:e.length)<=t.min)return r.msgError(`最少保留${t.min}个`);m.value.splice(s,1)};return(s,e)=>{const u=O,g=K,b=z,h=F,k=I,w=D,y=q,U=A;return p(),C("div",null,[l("div",null,[a(c(B),{class:"draggable",modelValue:c(m),"onUpdate:modelValue":e[0]||(e[0]=o=>N(m)?m.value=o:null),animation:"300",handle:".drag-move","item-key":"index"},{item:d(({element:o,index:i})=>[(p(),$(y,{class:"w-[467px]",key:i,onClose:n=>v(i)},{default:d(()=>[l("div",P,[a(g,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body",size:"60px","exclude-domain":""},{upload:d(()=>[l("div",Q,[a(u,{name:"el-icon-Plus",size:20})])]),_:1},8,["modelValue","onUpdate:modelValue"]),l("div",S,[l("div",T,[e[1]||(e[1]=l("span",{class:"text-tx-regular flex-none mr-3"},"名称",-1)),a(b,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),l("div",j,[e[2]||(e[2]=l("span",{class:"text-tx-regular flex-none mr-3"},"链接",-1)),a(h,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),a(w,{label:"是否显示",class:"mt-[18px]"},{default:d(()=>[l("div",G,[a(k,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),l("div",H,[a(u,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),l("div",null,[a(U,{type:"primary",onClick:x},{default:d(()=>[...e[3]||(e[3]=[L("添加",-1)])]),_:1})])])}}});export{Z as _};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{r as n}from"./index-vwMTrNXz.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e};
|
||||
import{r as n}from"./index-Do7fAtr3.js";function e(t){return n.get({url:"/auth.admin/lists",params:t},{ignoreCancelToken:!0})}function i(t){return n.post({url:"/auth.admin/add",params:t})}function r(t){return n.post({url:"/auth.admin/edit",params:t})}function u(t){return n.post({url:"/auth.admin/delete",params:t})}function d(t){return n.get({url:"/auth.admin/detail",params:t})}export{e as a,r as b,u as c,i as d,d as e};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{r as e}from"./index-vwMTrNXz.js";function a(t){return e.get({url:"/article.articleCate/lists",params:t})}function l(t){return e.get({url:"/article.articleCate/all",params:t})}function i(t){return e.post({url:"/article.articleCate/add",params:t})}function c(t){return e.post({url:"/article.articleCate/edit",params:t})}function u(t){return e.post({url:"/article.articleCate/delete",params:t})}function n(t){return e.get({url:"/article.articleCate/detail",params:t})}function s(t){return e.post({url:"/article.articleCate/updateStatus",params:t})}function o(t){return e.get({url:"/article.article/lists",params:t})}function d(t){return e.post({url:"/article.article/add",params:t})}function f(t){return e.post({url:"/article.article/edit",params:t})}function C(t){return e.post({url:"/article.article/delete",params:t})}function p(t){return e.get({url:"/article.article/detail",params:t})}function g(t){return e.post({url:"/article.article/updateStatus",params:t})}export{a,u as b,s as c,c as d,i as e,n as f,p as g,f as h,d as i,l as j,o as k,g as l,C as m};
|
||||
import{r as e}from"./index-Do7fAtr3.js";function a(t){return e.get({url:"/article.articleCate/lists",params:t})}function l(t){return e.get({url:"/article.articleCate/all",params:t})}function i(t){return e.post({url:"/article.articleCate/add",params:t})}function c(t){return e.post({url:"/article.articleCate/edit",params:t})}function u(t){return e.post({url:"/article.articleCate/delete",params:t})}function n(t){return e.get({url:"/article.articleCate/detail",params:t})}function s(t){return e.post({url:"/article.articleCate/updateStatus",params:t})}function o(t){return e.get({url:"/article.article/lists",params:t})}function d(t){return e.post({url:"/article.article/add",params:t})}function f(t){return e.post({url:"/article.article/edit",params:t})}function C(t){return e.post({url:"/article.article/delete",params:t})}function p(t){return e.get({url:"/article.article/detail",params:t})}function g(t){return e.post({url:"/article.article/updateStatus",params:t})}export{a,u as b,s as c,c as d,i as e,n as f,p as g,f as h,d as i,l as j,o as k,g as l,C as m};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{r as e}from"./index-vwMTrNXz.js";function r(s){return e.get({url:"/asset.AssetUser/lists",params:s})}function u(s){return e.post({url:"/asset.AssetUser/add",params:s})}function a(s){return e.post({url:"/asset.AssetUser/edit",params:s})}function i(s){return e.post({url:"/asset.AssetUser/delete",params:s})}function o(s){return e.get({url:"/asset.AssetResource/lists",params:s})}function n(s){return e.post({url:"/asset.AssetResource/add",params:s})}function A(s){return e.post({url:"/asset.AssetResource/edit",params:s})}function c(s){return e.post({url:"/asset.AssetResource/delete",params:s})}export{o as a,c as b,A as c,n as d,r as e,a as f,i as g,u as h};
|
||||
import{r as e}from"./index-Do7fAtr3.js";function r(s){return e.get({url:"/asset.AssetUser/lists",params:s})}function u(s){return e.post({url:"/asset.AssetUser/add",params:s})}function a(s){return e.post({url:"/asset.AssetUser/edit",params:s})}function i(s){return e.post({url:"/asset.AssetUser/delete",params:s})}function o(s){return e.get({url:"/asset.AssetResource/lists",params:s})}function n(s){return e.post({url:"/asset.AssetResource/add",params:s})}function A(s){return e.post({url:"/asset.AssetResource/edit",params:s})}function c(s){return e.post({url:"/asset.AssetResource/delete",params:s})}export{o as a,c as b,A as c,n as d,r as e,a as f,i as g,u as h};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-14i9ZtFy.js";import"./.pnpm-CJoARskO.js";import"./index-CvWXUesg.js";import"./index-vwMTrNXz.js";import"./picker-_oSJRmY0.js";import"./index-pOzDs5Ek.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./article-DfNLL0Dd.js";import"./usePaging-DKtm3XdH.js";import"./picker-BtisB09w.js";import"./index-CfDlwT2y.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-B9i_Csmx.js";import"./.pnpm-CJoARskO.js";import"./add-nav.vue_vue_type_script_setup_true_lang-1RdLHUAJ.js";import"./index-CvWXUesg.js";import"./index-vwMTrNXz.js";import"./picker-_oSJRmY0.js";import"./index-pOzDs5Ek.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./article-DfNLL0Dd.js";import"./usePaging-DKtm3XdH.js";import"./picker-BtisB09w.js";import"./index-CfDlwT2y.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as m}from"./attr.vue_vue_type_script_setup_true_lang-Ck9fwFyC.js";import"./.pnpm-BadRMC3e.js";export{m as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-CZpwv8m7.js";import"./.pnpm-BadRMC3e.js";import"./index.vue_vue_type_script_setup_true_lang-BhTBtKLt.js";import"./picker-BTjkKuWn.js";import"./index-DP_2jise.js";import"./index-Do7fAtr3.js";import"./index-BNYVM0wQ.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./index-VEyOoZ0z.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";import"./usePaging-DMqUvFG-.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-xkpL7agD.js";import"./.pnpm-CJoARskO.js";import"./index-CvWXUesg.js";import"./index-vwMTrNXz.js";import"./picker-_oSJRmY0.js";import"./index-pOzDs5Ek.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./article-DfNLL0Dd.js";import"./usePaging-DKtm3XdH.js";import"./picker-BtisB09w.js";import"./index-CfDlwT2y.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-C991mJZb.js";import"./.pnpm-CJoARskO.js";import"./add-nav.vue_vue_type_script_setup_true_lang-1RdLHUAJ.js";import"./index-CvWXUesg.js";import"./index-vwMTrNXz.js";import"./picker-_oSJRmY0.js";import"./index-pOzDs5Ek.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./article-DfNLL0Dd.js";import"./usePaging-DKtm3XdH.js";import"./picker-BtisB09w.js";import"./index-CfDlwT2y.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-CR3MAcLK.js";import"./.pnpm-BadRMC3e.js";import"./index-VEyOoZ0z.js";import"./index-Do7fAtr3.js";import"./picker-CCYShZB-.js";import"./index-DP_2jise.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./article-DHOuLFy5.js";import"./usePaging-DMqUvFG-.js";import"./picker-BTjkKuWn.js";import"./index-BNYVM0wQ.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";import"./index.vue_vue_type_script_setup_true_lang-BhTBtKLt.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as m}from"./attr.vue_vue_type_script_setup_true_lang-BDicSE3X.js";import"./.pnpm-CJoARskO.js";export{m as default};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{o as b,q as c,r as V,s as v,v as t,D as a,Y as x,X as E,u as l,b9 as g,F as k,p as w}from"./.pnpm-CJoARskO.js";import{_ as p}from"./menu-set.vue_vue_type_script_setup_true_lang-D79ZNkE-.js";import"./index-CvWXUesg.js";import"./index-vwMTrNXz.js";import"./picker-_oSJRmY0.js";import"./index-pOzDs5Ek.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./article-DfNLL0Dd.js";import"./usePaging-DKtm3XdH.js";import"./picker-BtisB09w.js";import"./index-CfDlwT2y.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";const $=b({__name:"attr",props:{modelValue:{type:Object,default:()=>({nav:[],menu:{}})}},emits:["update:modelValue"],setup(s,{emit:u}){const d=s,i=u,o=w({get(){return d.modelValue},set(n){i("update:modelValue",n)}});return(n,e)=>{const r=E,f=x,_=g;return c(),V(k,null,[e[2]||(e[2]=v("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"}," pc导航设置 ",-1)),t(_,{class:"mt-4","label-width":"70px"},{default:a(()=>[t(f,{"model-value":"nav"},{default:a(()=>[t(r,{label:"主导航设置",name:"nav"},{default:a(()=>[t(p,{modelValue:l(o).nav,"onUpdate:modelValue":e[0]||(e[0]=m=>l(o).nav=m)},null,8,["modelValue"])]),_:1}),t(r,{label:"菜单设置",name:"menu"},{default:a(()=>[t(p,{modelValue:l(o).menu,"onUpdate:modelValue":e[1]||(e[1]=m=>l(o).menu=m)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})],64)}}});export{$ as default};
|
||||
import{o as b,q as c,r as V,s as v,v as t,D as a,Y as x,X as E,u as l,b9 as g,F as k,p as w}from"./.pnpm-BadRMC3e.js";import{_ as p}from"./menu-set.vue_vue_type_script_setup_true_lang-KCrGFDT0.js";import"./index-VEyOoZ0z.js";import"./index-Do7fAtr3.js";import"./picker-CCYShZB-.js";import"./index-DP_2jise.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./article-DHOuLFy5.js";import"./usePaging-DMqUvFG-.js";import"./picker-BTjkKuWn.js";import"./index-BNYVM0wQ.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";const $=b({__name:"attr",props:{modelValue:{type:Object,default:()=>({nav:[],menu:{}})}},emits:["update:modelValue"],setup(s,{emit:u}){const d=s,i=u,o=w({get(){return d.modelValue},set(n){i("update:modelValue",n)}});return(n,e)=>{const r=E,f=x,_=g;return c(),V(k,null,[e[2]||(e[2]=v("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"}," pc导航设置 ",-1)),t(_,{class:"mt-4","label-width":"70px"},{default:a(()=>[t(f,{"model-value":"nav"},{default:a(()=>[t(r,{label:"主导航设置",name:"nav"},{default:a(()=>[t(p,{modelValue:l(o).nav,"onUpdate:modelValue":e[0]||(e[0]=m=>l(o).nav=m)},null,8,["modelValue"])]),_:1}),t(r,{label:"菜单设置",name:"menu"},{default:a(()=>[t(p,{modelValue:l(o).menu,"onUpdate:modelValue":e[1]||(e[1]=m=>l(o).menu=m)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})],64)}}});export{$ as default};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-D7SebzRa.js";import"./.pnpm-CJoARskO.js";import"./index.vue_vue_type_script_setup_true_lang-BZrtzFNp.js";import"./picker-BtisB09w.js";import"./index-pOzDs5Ek.js";import"./index-vwMTrNXz.js";import"./index-CfDlwT2y.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./index-CvWXUesg.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";import"./usePaging-DKtm3XdH.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-CETYhEN2.js";import"./.pnpm-BadRMC3e.js";import"./add-nav.vue_vue_type_script_setup_true_lang-D3OZX8cR.js";import"./index-VEyOoZ0z.js";import"./index-Do7fAtr3.js";import"./picker-CCYShZB-.js";import"./index-DP_2jise.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./article-DHOuLFy5.js";import"./usePaging-DMqUvFG-.js";import"./picker-BTjkKuWn.js";import"./index-BNYVM0wQ.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-BZOVOw6C.js";import"./.pnpm-BadRMC3e.js";import"./index-VEyOoZ0z.js";import"./index-Do7fAtr3.js";import"./picker-CCYShZB-.js";import"./index-DP_2jise.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./article-DHOuLFy5.js";import"./usePaging-DMqUvFG-.js";import"./picker-BTjkKuWn.js";import"./index-BNYVM0wQ.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-nmpJC7wW.js";import"./.pnpm-CJoARskO.js";import"./index-CvWXUesg.js";import"./index-vwMTrNXz.js";import"./picker-_oSJRmY0.js";import"./index-pOzDs5Ek.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./article-DfNLL0Dd.js";import"./usePaging-DKtm3XdH.js";import"./picker-BtisB09w.js";import"./index-CfDlwT2y.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";import"./index.vue_vue_type_script_setup_true_lang-BZrtzFNp.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as m}from"./attr.vue_vue_type_script_setup_true_lang-CgrUv7Vh.js";import"./.pnpm-CJoARskO.js";export{m as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-B_X-6joR.js";import"./.pnpm-BadRMC3e.js";import"./add-nav.vue_vue_type_script_setup_true_lang-D3OZX8cR.js";import"./index-VEyOoZ0z.js";import"./index-Do7fAtr3.js";import"./picker-CCYShZB-.js";import"./index-DP_2jise.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./article-DHOuLFy5.js";import"./usePaging-DMqUvFG-.js";import"./picker-BTjkKuWn.js";import"./index-BNYVM0wQ.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as m}from"./attr.vue_vue_type_script_setup_true_lang-CbeXAM_5.js";import"./.pnpm-CJoARskO.js";export{m as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-C3rhWyvi.js";import"./.pnpm-BadRMC3e.js";import"./picker-BTjkKuWn.js";import"./index-DP_2jise.js";import"./index-Do7fAtr3.js";import"./index-BNYVM0wQ.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./index-VEyOoZ0z.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";import"./usePaging-DMqUvFG-.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DEUuv3T9.js";import"./.pnpm-CJoARskO.js";import"./picker-BtisB09w.js";import"./index-pOzDs5Ek.js";import"./index-vwMTrNXz.js";import"./index-CfDlwT2y.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./index-CvWXUesg.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";import"./usePaging-DKtm3XdH.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as m}from"./attr.vue_vue_type_script_setup_true_lang-DU_nXh8M.js";import"./.pnpm-BadRMC3e.js";export{m as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as m}from"./attr.vue_vue_type_script_setup_true_lang-XYrW2c3f.js";import"./.pnpm-BadRMC3e.js";export{m as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-B_eBI6xm.js";import"./.pnpm-BadRMC3e.js";import"./index-VEyOoZ0z.js";import"./index-Do7fAtr3.js";import"./picker-CCYShZB-.js";import"./index-DP_2jise.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./article-DHOuLFy5.js";import"./usePaging-DMqUvFG-.js";import"./picker-BTjkKuWn.js";import"./index-BNYVM0wQ.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";export{o as default};
|
||||
@@ -1 +0,0 @@
|
||||
import{_ as o}from"./attr-setting.vue_vue_type_script_setup_true_lang-DDp138og.js";import"./.pnpm-CJoARskO.js";import"./index-DGRqux-9.js";import"./attr-Cz_-CGg8.js";import"./index-CvWXUesg.js";import"./index-vwMTrNXz.js";import"./picker-_oSJRmY0.js";import"./index-pOzDs5Ek.js";import"./index.vue_vue_type_script_setup_true_lang-CI_v6sRz.js";import"./article-DfNLL0Dd.js";import"./usePaging-DKtm3XdH.js";import"./picker-BtisB09w.js";import"./index-CfDlwT2y.js";import"./index-0im0Afgh.js";import"./file-Dg-5v8cj.js";import"./index.vue_vue_type_script_setup_true_lang-rPwCQIWi.js";import"./content.vue_vue_type_script_setup_true_lang-DF8pWGsr.js";import"./decoration-img-CPRGZ5aq.js";import"./attr.vue_vue_type_script_setup_true_lang-DEUuv3T9.js";import"./content-BWlqOjJD.js";import"./attr.vue_vue_type_script_setup_true_lang-xkpL7agD.js";import"./content.vue_vue_type_script_setup_true_lang-D410UMxO.js";import"./attr.vue_vue_type_script_setup_true_lang-B9i_Csmx.js";import"./add-nav.vue_vue_type_script_setup_true_lang-1RdLHUAJ.js";import"./content-CobUYz5t.js";import"./attr.vue_vue_type_script_setup_true_lang-C991mJZb.js";import"./content.vue_vue_type_script_setup_true_lang-RYMnsKiH.js";import"./attr.vue_vue_type_script_setup_true_lang-CgrUv7Vh.js";import"./content-BpsV3Yt7.js";import"./decoration-BlMAHrYB.js";import"./attr.vue_vue_type_script_setup_true_lang-D7SebzRa.js";import"./index.vue_vue_type_script_setup_true_lang-BZrtzFNp.js";import"./content-C2vy62ef.js";import"./content.vue_vue_type_script_setup_true_lang-B5D1fxZJ.js";import"./attr.vue_vue_type_script_setup_true_lang-BDicSE3X.js";import"./content-D9LroEig.js";import"./attr.vue_vue_type_script_setup_true_lang-14i9ZtFy.js";import"./content.vue_vue_type_script_setup_true_lang-DUGOcjwM.js";import"./attr.vue_vue_type_script_setup_true_lang-CbeXAM_5.js";import"./content-B-B36d3B.js";export{o as default};
|
||||
@@ -0,0 +1 @@
|
||||
import{_ as o}from"./attr-setting.vue_vue_type_script_setup_true_lang-_vdeZ3_G.js";import"./.pnpm-BadRMC3e.js";import"./index-jn7AqtXG.js";import"./attr-CaS4-vjx.js";import"./index-VEyOoZ0z.js";import"./index-Do7fAtr3.js";import"./picker-CCYShZB-.js";import"./index-DP_2jise.js";import"./index.vue_vue_type_script_setup_true_lang-BZQ0jmei.js";import"./article-DHOuLFy5.js";import"./usePaging-DMqUvFG-.js";import"./picker-BTjkKuWn.js";import"./index-BNYVM0wQ.js";import"./index-CKJths3X.js";import"./file-BfbRb1wZ.js";import"./index.vue_vue_type_script_setup_true_lang-QSNac8Ky.js";import"./content.vue_vue_type_script_setup_true_lang-D07ajdkx.js";import"./decoration-img-B0LkMFfS.js";import"./attr.vue_vue_type_script_setup_true_lang-C3rhWyvi.js";import"./content-CYOEh4RL.js";import"./attr.vue_vue_type_script_setup_true_lang-B_eBI6xm.js";import"./content.vue_vue_type_script_setup_true_lang-DrPQyt1L.js";import"./attr.vue_vue_type_script_setup_true_lang-CETYhEN2.js";import"./add-nav.vue_vue_type_script_setup_true_lang-D3OZX8cR.js";import"./content-BrPzGkcF.js";import"./attr.vue_vue_type_script_setup_true_lang-B_X-6joR.js";import"./content.vue_vue_type_script_setup_true_lang-DTQ6DjhZ.js";import"./attr.vue_vue_type_script_setup_true_lang-Ck9fwFyC.js";import"./content-DdIAx6E1.js";import"./decoration-0oah8UE6.js";import"./attr.vue_vue_type_script_setup_true_lang-CZpwv8m7.js";import"./index.vue_vue_type_script_setup_true_lang-BhTBtKLt.js";import"./content-BKXs3H1I.js";import"./content.vue_vue_type_script_setup_true_lang-C8DdB-3S.js";import"./attr.vue_vue_type_script_setup_true_lang-XYrW2c3f.js";import"./content-CVm2jRFT.js";import"./attr.vue_vue_type_script_setup_true_lang-BZOVOw6C.js";import"./content.vue_vue_type_script_setup_true_lang-BUyFKrza.js";import"./attr.vue_vue_type_script_setup_true_lang-DU_nXh8M.js";import"./content-D87zRUIO.js";export{o as default};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user