Compare commits

..
1 Commits
Author SHA1 Message Date
Your Name 037e5ba450 更新 2026-08-06 14:11:52 +08:00
345 changed files with 347 additions and 1980 deletions
+1 -14
View File
@@ -4,7 +4,7 @@ export interface MyPatientListParams {
page_no: number
page_size: number
keyword?: string
status_filter?: '' | 'unbooked' | 'pending_interview' | 'completed' | 'missed'
status_filter?: '' | 'unconfirmed' | 'booked' | 'completed' | 'missed'
start_date?: string
end_date?: string
}
@@ -125,23 +125,10 @@ export function myPatientCancelAppointment(params: { id: number }) {
return request.post({ url: '/firstvisit.myPatient/cancelAppointment', params })
}
export function myPatientAssistants() {
return request.get({ url: '/firstvisit.myPatient/assistants' })
}
export function myPatientAssign(params: { id: number; assistant_id: number; is_inherit?: 0 | 1 }) {
return request.post({ url: '/firstvisit.myPatient/assign', params })
}
export function myPatientFillIdCard(params: { id: number; id_card: string }) {
return request.post({ url: '/firstvisit.myPatient/fillIdCard', params })
}
export interface FirstVisitConversionParams {
time_type: 'today' | 'yesterday' | 'week' | 'month' | 'quarter' | 'year'
dept_id?: number
assistant_id?: number
media_channel_code?: string
}
/** 一诊综合数据转化:服务端按当前角色 DataScope 与所选部门/员工取交集。 */
+2 -2
View File
@@ -1,9 +1,9 @@
import request from '@/utils/request'
/** 角色数据驾驶舱:服务端统一按当前管理员的数据范围聚合。 */
export function performanceDashboardOverview(params?: { ranking_dept_id?: number; _t?: number }) {
export function performanceDashboardOverview() {
return request.get(
{ url: '/stats.performanceDashboard/overview', params, timeout: 120000 },
{ url: '/stats.performanceDashboard/overview', timeout: 120000 },
{ ignoreCancelToken: true }
)
}
@@ -51,24 +51,6 @@
@change="handleDeptChange"
/>
</div>
<div class="filter-item">
<span class="filter-label">渠道</span>
<el-select
v-model="query.media_channel_code"
clearable
filterable
placeholder="全部渠道"
class="channel-select"
@change="loadDashboard"
>
<el-option
v-for="item in dashboard.filters.media_channels"
:key="item.code"
:label="item.name"
:value="item.code"
/>
</el-select>
</div>
<span class="range-text">{{ dashboard.meta.start_date }} {{ dashboard.meta.end_date }}</span>
</section>
@@ -85,7 +67,7 @@
<div class="panel-heading">
<div>
<h2>部门订单量占比</h2>
<p>按订单创建人归属排除取消拒收及退款</p>
<p>双审通过排除取消拒收及退款</p>
</div>
<span>单位</span>
</div>
@@ -122,7 +104,7 @@
<div class="panel-heading panel-heading--table">
<div>
<h2>明细数据列表</h2>
<p>展开部门可查看人员明细挂号=已支付且实收低于 10 元的订单预约=有效预约记录开口率=开口/加粉挂号率=挂号/加粉面诊率=面诊/预约接诊率=接诊诊单/加粉</p>
<p>部门层级汇总父级包含其下级数据</p>
</div>
<span>{{ dashboard.rows.length }} 个顶层节点</span>
</div>
@@ -133,42 +115,22 @@
default-expand-all
class="detail-table"
>
<el-table-column prop="name" label="部门 / 人员" min-width="250" fixed="left">
<el-table-column prop="name" label="部门" min-width="230" fixed="left">
<template #default="{ row }">
<strong :class="{ 'is-parent': Array.isArray(row.children) && row.children.length, 'is-member': row.type === 'member' }">
<strong :class="{ 'is-parent': Array.isArray(row.children) && row.children.length }">
{{ row.name }}
</strong>
<el-tag v-if="row.type === 'member' && row.role" size="small" type="info" effect="plain" class="role-tag">
{{ row.role }}{{ row.is_leader ? ' · 组长' : '' }}
</el-tag>
<el-tag v-else-if="row.type === 'unbound'" size="small" type="warning" effect="plain" class="role-tag">
未绑定账号
</el-tag>
</template>
</el-table-column>
<el-table-column prop="add_fans_count" label="加粉" min-width="88" align="right" />
<el-table-column prop="total_open_count" label="开口" min-width="88" align="right" />
<el-table-column prop="paid_appointment_count" label="挂号" min-width="88" align="right" />
<el-table-column prop="appointment_total_count" label="预约" min-width="88" align="right" />
<el-table-column prop="interview_count" label="面诊" min-width="88" align="right" />
<el-table-column prop="completed_order_count" label="接诊诊单" min-width="104" align="right" />
<el-table-column label="诊金额" min-width="120" align="right">
<el-table-column label="诊金额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.completed_order_amount) }}</template>
</el-table-column>
<el-table-column label="开口率" min-width="96" align="right">
<template #default="{ row }">{{ formatPercent(row.total_open_rate) }}</template>
</el-table-column>
<el-table-column label="挂号率" min-width="96" align="right">
<template #default="{ row }">{{ formatPercent(row.paid_appointment_rate) }}</template>
</el-table-column>
<el-table-column label="面诊率" min-width="96" align="right">
<template #default="{ row }">{{ formatPercent(row.interview_rate) }}</template>
</el-table-column>
<el-table-column label="面诊接诊率" min-width="116" align="right">
<template #default="{ row }">{{ formatPercent(row.interview_receive_rate) }}</template>
</el-table-column>
<el-table-column label="接诊率" min-width="96" align="right">
<template #default="{ row }">{{ formatPercent(row.receive_rate) }}</template>
<template #default="{ row }">{{ formatPercent(row.interview_receive_rate) }}</template>
</el-table-column>
<el-table-column label="ROI" min-width="86" align="right">
<template #default="{ row }">{{ formatRatio(row.roi) }}</template>
@@ -250,14 +212,9 @@ type MetricType = 'count' | 'money' | 'ratio'
const emptyDashboard = () => ({
meta: {
time_type: 'today', time_label: '今日', start_date: '', end_date: '', generated_at: '',
scope_value: 4, scope_label: '', selected_dept_name: '', selected_assistant_name: '',
selected_media_channel_code: '', selected_media_channel_name: '', open_count_source: ''
},
filters: {
departments: [] as any[],
assistants: [] as Array<{ id: number; name: string }>,
media_channels: [] as Array<{ code: string; name: string }>
scope_value: 4, scope_label: '', selected_dept_name: '', selected_assistant_name: '', open_count_source: ''
},
filters: { departments: [] as any[], assistants: [] as Array<{ id: number; name: string }> },
summary: {} as Record<string, any>,
rankings: { orders: [] as any[], amounts: [] as any[] },
rows: [] as any[],
@@ -270,12 +227,7 @@ const emptyDashboard = () => ({
const dashboard = reactive(emptyDashboard())
const loading = ref(false)
const query = reactive<FirstVisitConversionParams>({
time_type: 'today',
dept_id: undefined,
assistant_id: undefined,
media_channel_code: ''
})
const query = reactive<FirstVisitConversionParams>({ time_type: 'today', dept_id: undefined, assistant_id: undefined })
const deptTreeProps = { value: 'id', label: 'name', children: 'children' }
const timeOptions = [
{ label: '今日', value: 'today' },
@@ -288,9 +240,9 @@ const timeOptions = [
const metricCards: Array<{ key: string; label: string; type: MetricType; hint: string }> = [
{ 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: 'interview_count', label: '面诊', type: 'count', 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: '诊单金额 / 投放成本' }
@@ -300,7 +252,6 @@ const scopeDescription = computed(() => {
const parts = [`${dashboard.meta.time_label || '当前区间'}数据`, dashboard.meta.scope_label || '当前权限范围']
if (dashboard.meta.selected_dept_name) parts.push(dashboard.meta.selected_dept_name)
if (dashboard.meta.selected_assistant_name) parts.push(dashboard.meta.selected_assistant_name)
if (dashboard.meta.selected_media_channel_name) parts.push(`渠道:${dashboard.meta.selected_media_channel_name}`)
return parts.join(' · ')
})
const maxOrderValue = computed(() => Math.max(0, ...dashboard.rankings.orders.map(item => Number(item.value || 0))))
@@ -437,7 +388,6 @@ onMounted(loadDashboard)
.range-text { margin-left: auto; }
.employee-select { width: 190px; }
.dept-select { width: 220px; }
.channel-select { width: 180px; }
.metric-grid {
display: grid;
@@ -485,8 +435,6 @@ onMounted(loadDashboard)
:deep(td.el-table__cell) { color: #273347; font-size: 12px; }
:deep(.el-table__row--level-0 > td.el-table__cell) { background: #edf7f5; font-weight: 650; }
strong.is-parent { color: #172033; font-weight: 700; }
strong.is-member { color: #314158; font-weight: 600; }
.role-tag { margin-left: 7px; vertical-align: middle; }
}
.target-panel { padding-bottom: 18px; }
@@ -521,7 +469,7 @@ onMounted(loadDashboard)
.page-heading, .heading-meta { align-items: flex-start; flex-direction: column; }
.metric-grid, .ranking-grid { grid-template-columns: 1fr; }
.filter-item, .filter-item--time { width: 100%; align-items: flex-start; flex-direction: column; }
.employee-select, .dept-select, .channel-select { width: 100%; }
.employee-select, .dept-select { width: 100%; }
.bar-row { grid-template-columns: 90px minmax(70px, 1fr) 82px; }
.target-summary { grid-template-columns: 1fr; }
}
@@ -5,7 +5,7 @@
<span class="heading-mark"><el-icon><DataLine /></el-icon></span>
<div>
<h1>医生看板</h1>
<p>从挂号预约面诊到接诊成交统一观察医生经营表现</p>
<p>从挂号面诊到接诊成交统一观察医生经营表现</p>
</div>
</div>
<div class="heading-actions">
@@ -70,8 +70,8 @@
<div class="metric-icon"><el-icon><Calendar /></el-icon></div>
<div>
<span>总挂号</span>
<strong>{{ formatNumber(dashboard.summary.registration_total) }}</strong>
<small>已支付且实收金额大于 0低于 10 元的订单</small>
<strong>{{ formatNumber(dashboard.summary.appointment_total) }}</strong>
<small>完成 {{ formatNumber(dashboard.summary.interview_count) }} · 完成率 {{ formatPercent(dashboard.summary.appointment_completion_rate) }}</small>
</div>
</article>
<article class="metric-card metric-card--green">
@@ -98,20 +98,12 @@
<small>客单价 {{ nullableMoney(dashboard.summary.avg_order_amount) }}</small>
</div>
</article>
<article class="metric-card metric-card--orange">
<div class="metric-icon"><el-icon><DataLine /></el-icon></div>
<div>
<span>总接诊率</span>
<strong>{{ formatPercent(dashboard.summary.receive_conversion_rate) }}</strong>
<small>总接诊 {{ formatNumber(dashboard.summary.order_count) }} ÷ 总面诊 {{ formatNumber(dashboard.summary.interview_count) }}</small>
</div>
</article>
<article class="metric-card metric-card--cyan">
<div class="metric-icon"><el-icon><CircleCheck /></el-icon></div>
<div>
<span>预约完成率</span>
<span>挂号完成率</span>
<strong>{{ formatPercent(dashboard.summary.appointment_completion_rate) }}</strong>
<small>总预约 {{ formatNumber(dashboard.summary.appointment_total) }} · {{ dashboard.meta.doctor_count }} 位有数据医生</small>
<small>{{ dashboard.meta.time_label }} · {{ dashboard.meta.doctor_count }} 位有数据医生</small>
</div>
</article>
</section>
@@ -151,7 +143,28 @@
</article>
</section>
<section class="analytics-grid">
<section class="two-column-grid analytics-grid">
<article class="panel funnel-panel">
<div class="panel-heading">
<div><h2>经营转化漏斗</h2><p>挂号 面诊 接诊 成交</p></div>
<span>人数 / 单数</span>
</div>
<div class="funnel-wrap">
<div
v-for="(stage, index) in dashboard.funnel"
:key="stage.key"
class="funnel-stage"
:class="`stage-${index + 1}`"
:style="{ width: `${100 - index * 15}%` }"
>
<span>{{ stage.label }}</span><strong>{{ formatNumber(stage.value) }}</strong>
</div>
</div>
<div class="funnel-insight">
最大流失发生在 <strong>{{ funnelLoss.label }}</strong>流失 {{ formatNumber(funnelLoss.value) }}
</div>
</article>
<article class="panel trend-panel">
<div class="panel-heading">
<div><h2> 30 天成交金额趋势</h2><p>{{ dashboard.trend.start_date }} {{ dashboard.trend.end_date }}</p></div>
@@ -212,7 +225,7 @@
</span>
</template>
</el-table-column>
<el-table-column prop="appointment_total" label="预约" min-width="90" sortable />
<el-table-column prop="appointment_total" label="挂号" min-width="90" sortable />
<el-table-column prop="interview_count" label="面诊" min-width="90" sortable />
<el-table-column prop="order_count" label="接诊" min-width="90" sortable />
<el-table-column prop="receive_conversion_rate" label="接诊率" min-width="110" sortable>
@@ -248,7 +261,7 @@
<footer class="data-note">
<el-icon><InfoFilled /></el-icon>
<span>{{ dashboard.meta.registration_rule }}{{ dashboard.meta.appointment_rule }}{{ dashboard.meta.performance_rule }}</span>
<span>{{ dashboard.meta.appointment_rule }}{{ dashboard.meta.performance_rule }}</span>
</footer>
</div>
</template>
@@ -279,18 +292,19 @@ const emptyDashboard = () => ({
meta: {
time_type: 'month', time_label: '本月', start_date: '', end_date: '', generated_at: '',
scope_value: 4, scope_label: '', scope_kind: '', selected_dept_name: '', selected_doctor_name: '',
doctor_count: 0, registration_rule: '', appointment_rule: '', performance_rule: ''
doctor_count: 0, appointment_rule: '', performance_rule: ''
},
filters: {
departments: [] as any[], doctors: [] as Array<{ id: number; name: string; disable: number }>,
can_filter_department: true
},
summary: {
registration_total: 0, appointment_total: 0, interview_count: 0, order_count: 0, deal_amount: 0,
appointment_total: 0, interview_count: 0, order_count: 0, deal_amount: 0,
avg_order_amount: null as number | null, appointment_completion_rate: null as number | null,
receive_conversion_rate: null as number | null, missed_count: 0, cancelled_count: 0
},
rankings: { amounts: [] as any[], conversion: [] as any[] },
funnel: [] as Array<{ key: string; label: string; value: number }>,
trend: { start_date: '', end_date: '', dates: [] as string[], labels: [] as string[], amounts: [] as number[] },
alerts: [] as any[],
alert_threshold: 15,
@@ -325,6 +339,19 @@ const visibleDetailRows = computed(() => {
if (query.doctor_id || showZeroRows.value) return dashboard.rows
return businessRows.value
})
const funnelLoss = computed(() => {
const stages = dashboard.funnel
let result = { label: '暂无可比阶段', value: 0 }
let maxLoss = -1
for (let index = 0; index < stages.length - 1; index++) {
const loss = Math.max(0, Number(stages[index].value) - Number(stages[index + 1].value))
if (loss > maxLoss) {
maxLoss = loss
result = { label: `${stages[index].label}${stages[index + 1].label}`, value: loss }
}
}
return result
})
const trendChartOption = computed(() => ({
animationDuration: 450,
grid: { left: 20, right: 20, top: 18, bottom: 18, containLabel: true },
@@ -453,7 +480,7 @@ function exportRows() {
ElMessage.warning('当前范围暂无可导出的医生数据')
return
}
const headers = ['医生', '状态', '预约', '面诊', '接诊', '接诊率', '成交金额', '过号', '取消']
const headers = ['医生', '状态', '挂号', '面诊', '接诊', '接诊率', '成交金额', '过号', '取消']
const lines = dashboard.rows.map((row: any) => [
row.doctor_name, row.status === 'disabled' ? '停用' : '活跃', row.appointment_total,
row.interview_count, row.order_count, formatPercent(row.receive_conversion_rate), row.deal_amount,
@@ -553,7 +580,7 @@ h2 { font-size: 15px; line-height: 1.4; }
.metric-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 14px;
margin-top: 14px;
}
@@ -578,7 +605,6 @@ h2 { font-size: 15px; line-height: 1.4; }
.metric-card--green .metric-icon { color: #37a465; background: #eef8f1; }
.metric-card--indigo .metric-icon { color: #556dde; background: #eef0fd; }
.metric-card--blue .metric-icon { color: #3976e6; background: #edf3ff; }
.metric-card--orange .metric-icon { color: #d77a2c; background: #fff4e9; }
.metric-card--cyan .metric-icon { color: #138da1; background: #eaf7f8; }
.metric-card span { display: block; color: #748296; font-size: 12px; }
.metric-card strong { display: block; margin: 4px 0 3px; font-size: 24px; line-height: 1.1; }
@@ -611,8 +637,25 @@ h2 { font-size: 15px; line-height: 1.4; }
.bar-track .is-blue { background: var(--blue); }
.bar-track .is-teal { background: var(--teal); }
.analytics-grid { display: grid; grid-template-columns: minmax(0, 1fr); }
.analytics-grid .panel { min-height: 300px; }
.funnel-wrap { display: flex; align-items: center; flex-direction: column; gap: 5px; padding: 12px 48px 7px; }
.funnel-stage {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 38px;
clip-path: polygon(4% 0, 96% 0, 88% 100%, 12% 100%);
color: #fff;
font-size: 12px;
}
.funnel-stage strong { font-size: 14px; }
.stage-1 { background: #258bd4; }
.stage-2 { background: #20a290; }
.stage-3 { background: #ed913f; }
.stage-4 { background: #45a96d; }
.funnel-insight { margin: 8px 16px 16px; padding: 8px 10px; border-radius: 7px; color: #617085; background: #f4f7f8; font-size: 11px; }
.funnel-insight strong { color: #d4772c; }
.trend-chart { width: 100%; height: 238px; }
.alert-panel { border-color: #dfe8e7; }
@@ -12,71 +12,37 @@
</div>
<div class="filter-panel">
<div class="filter-heading">
<div>
<h3>订单检索</h3>
<p>审核与履约状态直接展示点击后立即筛选</p>
</div>
<el-button link :disabled="!hasActiveFilters" @click="resetFilters">清空全部条件</el-button>
</div>
<div class="status-filter-list">
<div class="status-filter-row">
<span class="filter-label">处方审核</span>
<el-radio-group v-model="formData.prescription_audit_status" @change="handleSearch">
<el-radio-button v-for="item in auditFilterOptions" :key="String(item.value)" :value="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
<div class="status-filter-row">
<span class="filter-label">支付单审核</span>
<el-radio-group v-model="formData.payment_slip_audit_status" @change="handleSearch">
<el-radio-button v-for="item in auditFilterOptions" :key="String(item.value)" :value="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
<div class="status-filter-row status-filter-row--fulfillment">
<span class="filter-label">履约状态</span>
<el-radio-group v-model="formData.fulfillment_status" @change="handleSearch">
<el-radio-button v-for="item in fulfillmentFilterOptions" :key="String(item.value)" :value="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="search-filter-row">
<label class="filter-field filter-field--keyword">
<span>关键词</span>
<el-input
v-model="formData.keyword"
clearable
:prefix-icon="Search"
placeholder="订单号 / 患者 / 手机号 / 处方ID / 诊单ID"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
</label>
<label class="filter-field filter-field--date">
<span>创建时间</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
@change="handleDateChange"
/>
</label>
<div class="filter-actions">
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
</div>
<el-input
v-model="formData.keyword"
class="keyword-input"
clearable
:prefix-icon="Search"
placeholder="订单号 / 患者 / 手机号 / 处方ID / 诊单ID"
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
<el-select v-model="formData.prescription_audit_status" clearable placeholder="处方审核" @change="handleSearch">
<el-option v-for="item in auditOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="formData.payment_slip_audit_status" clearable placeholder="支付单审核" @change="handleSearch">
<el-option v-for="item in auditOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="formData.fulfillment_status" clearable placeholder="履约状态" @change="handleSearch">
<el-option v-for="item in fulfillmentOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-date-picker
v-model="dateRange"
class="date-range"
type="daterange"
range-separator=""
start-placeholder="创建开始"
end-placeholder="创建结束"
value-format="YYYY-MM-DD"
clearable
@change="handleDateChange"
/>
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
<div class="metric-grid">
@@ -100,26 +66,6 @@
<strong>{{ summary.completed }}</strong>
<small></small>
</div>
<button
class="metric-card metric-card--interactive metric-rejected"
:class="{ 'is-active': Number(formData.fulfillment_status) === 9 }"
type="button"
@click="toggleRejectedFilter"
>
<span>拒收订单</span>
<strong>{{ summary.rejected }}</strong>
<small> · 点击查看明细</small>
</button>
<button
class="metric-card metric-card--interactive metric-rejected-rate"
:class="{ 'is-active': Number(formData.fulfillment_status) === 9 }"
type="button"
@click="toggleRejectedFilter"
>
<span>拒收率</span>
<strong>{{ percent(summary.rejectionRate) }}</strong>
<small>拒收订单 ÷ 同检索范围订单</small>
</button>
</div>
<el-table
@@ -257,14 +203,12 @@ const formData = reactive({
end_date: ''
})
const auditFilterOptions: Array<{ label: string; value: SelectValue }> = [
{ label: '全部', value: '' },
const auditOptions = [
{ label: '待审核', value: 0 },
{ label: '已通过', value: 1 },
{ label: '已驳回', value: 2 }
]
const fulfillmentFilterOptions: Array<{ label: string; value: SelectValue }> = [
{ label: '全部', value: '' },
const fulfillmentOptions = [
{ label: '待双审通过', value: 1 },
{ label: '待发货', value: 2 },
{ label: '已完成', value: 3 },
@@ -290,19 +234,9 @@ const summary = computed(() => ({
orders: Number(pager.extend?.summary?.orders || 0),
amount: Number(pager.extend?.summary?.amount || 0),
pending: Number(pager.extend?.summary?.pending || 0),
completed: Number(pager.extend?.summary?.completed || 0),
rejected: Number(pager.extend?.summary?.rejected || 0),
rejectionRate: Number(pager.extend?.summary?.rejection_rate || 0)
completed: Number(pager.extend?.summary?.completed || 0)
}))
const scopeLabel = computed(() => pager.extend?.scope?.label || '按权限加载')
const hasActiveFilters = computed(() => Boolean(
formData.keyword.trim()
|| formData.prescription_audit_status !== ''
|| formData.payment_slip_audit_status !== ''
|| formData.fulfillment_status !== ''
|| formData.start_date
|| formData.end_date
))
function handleSearch() {
resetPage()
@@ -329,20 +263,10 @@ function resetFilters() {
resetPage()
}
function toggleRejectedFilter() {
formData.fulfillment_status = Number(formData.fulfillment_status) === 9 ? '' : 9
resetPage()
}
function money(value: number | string) {
return Number(value || 0).toFixed(2)
}
function percent(value: number | string) {
const number = Number(value || 0)
return `${Number.isInteger(number) ? number.toFixed(0) : number.toFixed(2)}%`
}
function auditTagType(status: number): 'success' | 'warning' | 'danger' | 'info' {
if (Number(status) === 1) return 'success'
if (Number(status) === 2) return 'danger'
@@ -411,121 +335,31 @@ onMounted(getLists)
}
.filter-panel {
padding: 16px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
padding: 14px;
border: 1px solid #e3e8ef;
border-radius: 10px;
background: #fbfcfd;
}
.filter-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e8edf2;
h3 {
margin: 0;
color: #273244;
font-size: 14px;
font-weight: 600;
}
p {
margin: 4px 0 0;
color: #8a95a6;
font-size: 12px;
:deep(.el-select) {
width: 132px;
}
}
.status-filter-list {
display: grid;
gap: 10px;
padding: 13px 0;
border-bottom: 1px solid #e8edf2;
.keyword-input {
width: min(340px, 100%);
}
.status-filter-row {
display: flex;
align-items: flex-start;
gap: 12px;
:deep(.el-radio-group) {
display: flex;
flex: 1;
flex-wrap: wrap;
gap: 6px;
}
:deep(.el-radio-button__inner) {
min-width: 72px;
padding: 7px 12px;
color: #5f6b7d;
border: 1px solid #dfe5ec;
border-radius: 6px;
box-shadow: none;
background: #fff;
transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
:deep(.el-radio-button:first-child .el-radio-button__inner),
:deep(.el-radio-button:last-child .el-radio-button__inner) {
border-radius: 6px;
}
:deep(.el-radio-button.is-active .el-radio-button__inner) {
color: #fff;
border-color: #0f9185;
background: #0f9185;
box-shadow: none;
}
}
.filter-label {
flex: 0 0 76px;
padding-top: 7px;
color: #475467;
font-size: 12px;
font-weight: 600;
}
.search-filter-row {
display: grid;
grid-template-columns: minmax(280px, 1fr) minmax(300px, 360px) auto;
align-items: end;
gap: 12px;
padding-top: 13px;
}
.filter-field {
display: grid;
gap: 6px;
min-width: 0;
> span {
color: #667085;
font-size: 12px;
font-weight: 500;
}
:deep(.el-date-editor) {
width: 100%;
}
}
.filter-actions {
display: flex;
align-items: center;
:deep(.el-button + .el-button) {
margin-left: 8px;
}
.date-range {
width: 250px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin: 14px 0;
}
@@ -533,8 +367,6 @@ onMounted(getLists)
.metric-card {
min-height: 82px;
padding: 14px 16px;
text-align: left;
font: inherit;
border: 1px solid #e3e8ef;
border-radius: 10px;
background: #fff;
@@ -554,39 +386,6 @@ onMounted(getLists)
}
}
.metric-card--interactive {
color: inherit;
cursor: pointer;
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
&:hover,
&:focus-visible {
border-color: #e5a9a3;
box-shadow: 0 7px 18px rgba(132, 45, 40, 0.08);
transform: translateY(-1px);
outline: none;
}
&:active {
transform: translateY(0);
}
&.is-active {
border-color: #d9685f;
box-shadow: 0 0 0 2px rgba(217, 104, 95, 0.11);
}
}
.metric-rejected,
.metric-rejected-rate {
border-color: #f0cbc7;
background: #fff9f8;
strong {
color: #c64f47;
}
}
.metric-warning {
border-color: #f3d8aa;
background: #fffcf5;
@@ -667,24 +466,10 @@ onMounted(getLists)
padding-top: 16px;
}
@media (max-width: 1400px) {
.metric-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1080px) {
.metric-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.search-filter-row {
grid-template-columns: 1fr 1fr;
}
.filter-actions {
grid-column: 1 / -1;
}
}
@media (max-width: 760px) {
@@ -697,24 +482,10 @@ onMounted(getLists)
grid-template-columns: 1fr;
}
.status-filter-row,
.search-filter-row {
display: flex;
align-items: stretch;
flex-direction: column;
}
.filter-label {
padding-top: 0;
}
.filter-actions {
display: grid;
grid-template-columns: 1fr 1fr;
:deep(.el-button) {
width: 100%;
}
.filter-panel > *,
.filter-panel :deep(.el-select),
.date-range {
width: 100%;
}
}
</style>
@@ -72,7 +72,7 @@
<div class="summary-grid">
<button class="summary-card summary-today" type="button" @click="selectDateType('today')">
<span class="summary-icon"><el-icon><Calendar /></el-icon></span>
<span class="summary-copy"><small>今日预约</small><strong>{{ summary.today }}</strong><em></em></span>
<span class="summary-copy"><small>今日挂号</small><strong>{{ summary.today }}</strong><em></em></span>
<span class="summary-date">{{ summaryDates.today || '—' }}</span>
</button>
<button class="summary-card summary-tomorrow" type="button" @click="selectDateType('tomorrow')">
@@ -146,28 +146,12 @@
<el-table-column label="诊单日期" width="118">
<template #default="{ row }">{{ row.diagnosis_date_text || '—' }}</template>
</el-table-column>
<el-table-column label="操作" min-width="430" fixed="right">
<el-table-column label="操作" min-width="285" fixed="right">
<template #default="{ row }">
<div class="row-actions">
<el-button v-if="canEditDiagnosis" type="primary" link @click="openDiagnosis(row)">诊单</el-button>
<el-button v-else-if="canReadDiagnosis" type="primary" link @click="openReadonlyDiagnosis(row)">查看</el-button>
<el-button v-if="canBookAppointment" type="primary" link @click="openAppointment(row)">预约</el-button>
<el-button
v-if="canAssignPatient"
type="warning"
link
@click="openAssignDialog(row)"
>
{{ Number(row.assistant_id) > 0 ? '重新指派' : '指派' }}
</el-button>
<el-button
v-if="canFillIdCard && !Number(row.has_id_card)"
type="warning"
link
@click="openFillIdCardDialog(row)"
>
补全身份证
</el-button>
<el-button
v-if="canShowDiagnosisQRCode(row)"
type="primary"
@@ -212,84 +196,6 @@
<edit-popup ref="editRef" @success="refreshPage" />
<appointment-popup ref="appointmentRef" api-scene="my_patient" @success="refreshPage" />
<el-dialog
v-model="assignDialogVisible"
title="指派医助"
width="500px"
:close-on-click-modal="false"
>
<el-form :model="assignForm" label-width="88px">
<el-form-item label="患者">
<span class="dialog-patient-name">{{ currentActionPatient?.patient_name || '—' }}</span>
</el-form-item>
<el-form-item label="当前助理">
<span>{{ currentActionPatient?.assistant_name || '未分配' }}</span>
</el-form-item>
<el-form-item label="选择医助" required>
<el-select
v-model="assignForm.assistant_id"
class="dialog-full-width"
filterable
clearable
:loading="assistantOptionsLoading"
placeholder="请选择当前范围内的医助"
>
<el-option
v-for="item in assistantOptions"
:key="item.id"
:label="assistantOptionLabel(item)"
:value="Number(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item label="继承">
<div class="inherit-field">
<el-checkbox v-model="assignForm.is_inherit">作为继承指派</el-checkbox>
<small>用于区分继承关系指派操作仍会完整记录</small>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="assignLoading" @click="submitAssign">确定指派</el-button>
</template>
</el-dialog>
<el-dialog
v-model="fillIdCardDialogVisible"
title="补全身份证号"
width="430px"
:close-on-click-modal="false"
@closed="resetFillIdCardForm"
>
<el-form
ref="fillIdCardFormRef"
:model="fillIdCardForm"
:rules="fillIdCardRules"
label-width="88px"
>
<el-form-item label="患者">
<span class="dialog-patient-name">{{ fillIdCardForm.patient_name || '—' }}</span>
</el-form-item>
<el-form-item label="身份证号" prop="id_card">
<el-input
v-model="fillIdCardForm.id_card"
maxlength="18"
clearable
show-word-limit
placeholder="请输入15或18位身份证号"
@keyup.enter="submitFillIdCard"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="fillIdCardDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="fillIdCardLoading" @click="submitFillIdCard">
提交并更新年龄
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="qrcodeDialogVisible"
title="诊单二维码"
@@ -327,13 +233,7 @@ import { Calendar, Clock, Loading, Lock, Refresh, Search } from '@element-plus/i
import { usePaging } from '@/hooks/usePaging'
import { hasPermission } from '@/utils/perm'
import feedback from '@/utils/feedback'
import {
myPatientAssign,
myPatientAssistants,
myPatientCancelAppointment,
myPatientFillIdCard,
myPatientLists
} from '@/api/first_visit'
import { myPatientCancelAppointment, myPatientLists } from '@/api/first_visit'
import { generateMiniProgramQrcode } from '@/api/tcm'
import { getWeappConfig } from '@/api/channel/weapp'
import useUserStore from '@/stores/modules/user'
@@ -343,7 +243,7 @@ const AppointmentPopup = defineAsyncComponent(() => import('@/views/tcm/diagnosi
const OrderPanel = defineAsyncComponent(() => import('./components/OrderPanel.vue'))
const ProgressPanel = defineAsyncComponent(() => import('./components/ProgressPanel.vue'))
type StatusFilter = '' | 'unbooked' | 'pending_interview' | 'completed' | 'missed'
type StatusFilter = '' | 'unconfirmed' | 'booked' | 'completed' | 'missed'
type DateType = 'all' | 'today' | 'tomorrow' | 'day_after' | 'last7' | 'last30' | 'custom'
const router = useRouter()
@@ -359,23 +259,6 @@ const qrcodeDialogVisible = ref(false)
const qrcodeLoading = ref(false)
const qrcodeUrl = ref('')
const currentQRCodePatient = ref<any>(null)
const assignDialogVisible = ref(false)
const assignLoading = ref(false)
const assistantOptionsLoading = ref(false)
const assistantOptions = ref<any[]>([])
const currentActionPatient = ref<any>(null)
const assignForm = reactive({
assistant_id: null as number | null,
is_inherit: false
})
const fillIdCardDialogVisible = ref(false)
const fillIdCardLoading = ref(false)
const fillIdCardFormRef = ref<any>()
const fillIdCardForm = reactive({
id: 0,
patient_name: '',
id_card: ''
})
const formData = reactive({
keyword: '',
@@ -386,8 +269,8 @@ const formData = reactive({
const statusOptions: Array<{ label: string; value: StatusFilter }> = [
{ label: '全部', value: '' },
{ label: '未预约', value: 'unbooked' },
{ label: '待面诊', value: 'pending_interview' },
{ label: '未确认', value: 'unconfirmed' },
{ label: '已挂号', value: 'booked' },
{ label: '已完成', value: 'completed' },
{ label: '已过号', value: 'missed' }
]
@@ -418,8 +301,6 @@ const scopeLabel = computed(() => pager.extend?.scope?.label || '按权限加载
const canEditDiagnosis = computed(() => hasPermission(['tcm.diagnosis/edit']))
const canReadDiagnosis = computed(() => hasPermission(['tcm.diagnosis/readonlyDetail']))
const canBookAppointment = computed(() => hasPermission(['tcm.diagnosis/guahao']))
const canAssignPatient = computed(() => hasPermission(['tcm.diagnosis/assign']))
const canFillIdCard = computed(() => hasPermission(['tcm.diagnosis/edit']))
const workspaceLoading = computed(() => {
if (activeWorkspace.value === 'orders') return Boolean(orderPanelRef.value?.loading)
if (activeWorkspace.value === 'progress') return Boolean(progressPanelRef.value?.loading)
@@ -518,115 +399,6 @@ function openAppointment(row: any) {
})
}
function assistantOptionLabel(item: any) {
const name = String(item?.name || item?.account || `医助${item?.id || ''}`)
const departments = String(item?.dept_names || '').trim()
return departments ? `${name} · ${departments}` : name
}
async function loadAssistantOptions() {
assistantOptionsLoading.value = true
try {
const result = await myPatientAssistants()
assistantOptions.value = Array.isArray(result) ? result : []
} catch (error: any) {
assistantOptions.value = []
feedback.msgError(error?.msg || '医助列表加载失败')
} finally {
assistantOptionsLoading.value = false
}
}
async function openAssignDialog(row: any) {
currentActionPatient.value = row
assignForm.assistant_id = Number(row.assistant_id) > 0 ? Number(row.assistant_id) : null
assignForm.is_inherit = false
assignDialogVisible.value = true
if (assistantOptions.value.length === 0) {
await loadAssistantOptions()
}
}
async function submitAssign() {
const diagnosisId = Number(currentActionPatient.value?.diagnosis_id || currentActionPatient.value?.id)
if (diagnosisId <= 0) {
feedback.msgWarning('患者诊单信息不完整')
return
}
if (!assignForm.assistant_id) {
feedback.msgWarning('请选择医助')
return
}
assignLoading.value = true
try {
await myPatientAssign({
id: diagnosisId,
assistant_id: Number(assignForm.assistant_id),
is_inherit: assignForm.is_inherit ? 1 : 0
})
feedback.msgSuccess('指派成功')
assignDialogVisible.value = false
await getLists()
} catch (error: any) {
feedback.msgError(error?.msg || '指派失败')
} finally {
assignLoading.value = false
}
}
const validateIdCard = (_rule: any, value: string, callback: (error?: Error) => void) => {
const idCard = String(value || '').trim()
if (!idCard) {
callback(new Error('请输入身份证号'))
return
}
const valid18 = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/.test(idCard)
const valid15 = /^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$/.test(idCard)
callback(valid18 || valid15 ? undefined : new Error('请输入15或18位有效身份证号'))
}
const fillIdCardRules = {
id_card: [{ required: true, validator: validateIdCard, trigger: 'blur' }]
}
function openFillIdCardDialog(row: any) {
fillIdCardForm.id = Number(row.diagnosis_id || row.id)
fillIdCardForm.patient_name = String(row.patient_name || '')
fillIdCardForm.id_card = ''
fillIdCardDialogVisible.value = true
}
function resetFillIdCardForm() {
fillIdCardForm.id = 0
fillIdCardForm.patient_name = ''
fillIdCardForm.id_card = ''
fillIdCardFormRef.value?.clearValidate?.()
}
async function submitFillIdCard() {
if (fillIdCardLoading.value) return
try {
await fillIdCardFormRef.value?.validate?.()
} catch {
return
}
fillIdCardLoading.value = true
try {
await myPatientFillIdCard({
id: fillIdCardForm.id,
id_card: fillIdCardForm.id_card.trim()
})
feedback.msgSuccess('补全成功,年龄已自动更新')
fillIdCardDialogVisible.value = false
await getLists()
} catch (error: any) {
feedback.msgError(error?.msg || '补全身份证失败')
} finally {
fillIdCardLoading.value = false
}
}
function canShowDiagnosisQRCode(row: any) {
return canBookAppointment.value
&& Number(row.appointment_id) > 0
@@ -1078,25 +850,6 @@ onMounted(() => {
}
}
.dialog-full-width {
width: 100%;
}
.dialog-patient-name {
color: #1f2937;
font-weight: 600;
}
.inherit-field {
display: grid;
gap: 2px;
small {
color: #98a2b3;
line-height: 1.5;
}
}
.pagination-wrap {
display: flex;
justify-content: flex-end;
@@ -2,14 +2,14 @@
<div
class="registration-stats"
v-loading="loading"
element-loading-text="正在汇总权限范围内的挂号与预约数据"
element-loading-text="正在汇总权限范围内的挂号数据"
>
<header class="page-heading">
<div class="heading-copy">
<span class="heading-mark"><el-icon><Histogram /></el-icon></span>
<div>
<h1>挂号统计</h1>
<p>挂号预约诊单与目标数据按当前角色和部门权限实时汇总</p>
<p>挂号诊单与目标数据按当前角色和部门权限实时汇总</p>
</div>
</div>
<div class="heading-actions">
@@ -66,26 +66,16 @@
<section class="metric-grid" aria-label="挂号统计核心指标">
<article class="metric-card metric-card--teal">
<div class="metric-icon"><el-icon><Wallet /></el-icon></div>
<div>
<span>{{ dashboard.meta.time_label || '今日' }}总挂号</span>
<strong>{{ formatNumber(dashboard.summary.registration_count) }}</strong>
<small :class="compareClass(dashboard.summary.registration_compare_rate)">
{{ compareText(dashboard.summary.registration_compare_rate) }}
</small>
</div>
</article>
<article class="metric-card metric-card--blue">
<div class="metric-icon"><el-icon><Calendar /></el-icon></div>
<div>
<span>{{ dashboard.meta.time_label || '今日' }}预约</span>
<span>{{ dashboard.meta.time_label || '今日' }}挂号</span>
<strong>{{ formatNumber(dashboard.summary.appointment_count) }}</strong>
<small :class="compareClass(dashboard.summary.appointment_compare_rate)">
{{ compareText(dashboard.summary.appointment_compare_rate) }}
</small>
</div>
</article>
<article class="metric-card metric-card--violet">
<article class="metric-card metric-card--blue">
<div class="metric-icon"><el-icon><DocumentChecked /></el-icon></div>
<div>
<span>{{ dashboard.meta.time_label || '今日' }}总诊单</span>
@@ -106,8 +96,8 @@
<section class="panel employee-panel">
<div class="panel-heading">
<div>
<h2>本组员工挂号与预约统计</h2>
<p>挂号按已支付且实收低于 10 元的支付订单统计预约按预约记录统计</p>
<h2>本组员工挂号统计</h2>
<p>部门汇总可展开查看员工明日后日预约始终使用对应自然日</p>
</div>
<span class="panel-badge">{{ dashboard.meta.time_label || '当前范围' }}</span>
</div>
@@ -128,22 +118,14 @@
</div>
</template>
</el-table-column>
<el-table-column prop="registration_count" :label="`${dashboard.meta.time_label || '当前'}挂号`" min-width="118" align="right" sortable />
<el-table-column prop="appointment_count" :label="`${dashboard.meta.time_label || '当前'}预约`" min-width="118" align="right" sortable />
<el-table-column prop="appointment_count" :label="`${dashboard.meta.time_label || '当前'}挂号`" min-width="118" align="right" sortable />
<el-table-column prop="tomorrow_count" label="明日预约" min-width="105" align="right" sortable />
<el-table-column prop="day_after_count" label="后日预约" min-width="105" align="right" sortable />
<el-table-column prop="order_count" label="诊单" min-width="86" align="right" sortable />
<el-table-column label="业绩" min-width="128" align="right" sortable :sort-method="sortAmount">
<template #default="{ row }">{{ formatMoney(row.order_amount) }}</template>
</el-table-column>
<el-table-column label="挂号较上期" min-width="112" align="right">
<template #default="{ row }">
<span :class="['rate-text', compareClass(row.registration_compare_rate)]">
{{ compactCompare(row.registration_compare_rate) }}
</span>
</template>
</el-table-column>
<el-table-column label="预约较上期" min-width="112" align="right">
<el-table-column label="较上期" min-width="105" align="right">
<template #default="{ row }">
<span :class="['rate-text', compareClass(row.appointment_compare_rate)]">
{{ compactCompare(row.appointment_compare_rate) }}
@@ -208,24 +190,9 @@
</article>
<article class="panel ranking-panel">
<div class="panel-heading">
<div><h2>{{ dashboard.meta.time_label || '今日' }}挂号 TOP</h2><p>低于 10 元的已支付订单笔数排序</p></div>
<div><h2>{{ dashboard.meta.time_label || '今日' }}挂号 TOP</h2><p>有效挂号数量排序</p></div>
<span class="panel-badge">挂号</span>
</div>
<div v-if="dashboard.rankings.registrations.length" class="ranking-list ranking-list--blue">
<div v-for="(item, index) in dashboard.rankings.registrations" :key="`registration-${item.admin_id}`" class="ranking-row">
<b :class="{ 'is-top': index < 3 }">{{ index + 1 }}</b>
<span>{{ item.name }}<small>已支付小额订单</small></span>
<div class="rank-track"><i :style="{ width: rankWidth(item.value, maxRegistrations) }" /></div>
<strong>{{ formatNumber(item.value) }} </strong>
</div>
</div>
<el-empty v-else :image-size="52" description="暂无挂号数据" />
</article>
<article class="panel ranking-panel">
<div class="panel-heading">
<div><h2>{{ dashboard.meta.time_label || '今日' }}预约 TOP</h2><p>按有效预约记录数量排序</p></div>
<span class="panel-badge">预约</span>
</div>
<div v-if="dashboard.rankings.appointments.length" class="ranking-list ranking-list--blue">
<div v-for="(item, index) in dashboard.rankings.appointments" :key="`appointment-${item.admin_id}`" class="ranking-row">
<b :class="{ 'is-top': index < 3 }">{{ index + 1 }}</b>
@@ -234,7 +201,7 @@
<strong>{{ formatNumber(item.value) }} </strong>
</div>
</div>
<el-empty v-else :image-size="52" description="暂无预约数据" />
<el-empty v-else :image-size="52" description="暂无挂号数据" />
</article>
</section>
@@ -248,21 +215,13 @@
<template #default="{ row }"><strong>{{ row.name }}</strong></template>
</el-table-column>
<el-table-column prop="member_count" label="人数" min-width="90" align="right" sortable />
<el-table-column prop="registration_count" :label="`${dashboard.meta.time_label || '当前'}挂号`" min-width="118" align="right" sortable />
<el-table-column prop="appointment_count" :label="`${dashboard.meta.time_label || '当前'}预约`" min-width="118" align="right" sortable />
<el-table-column prop="appointment_count" :label="`${dashboard.meta.time_label || '当前'}挂号`" min-width="118" align="right" sortable />
<el-table-column prop="tomorrow_count" label="明日预约" min-width="105" align="right" sortable />
<el-table-column prop="order_count" label="诊单" min-width="90" align="right" sortable />
<el-table-column label="业绩" min-width="130" align="right">
<template #default="{ row }">{{ formatMoney(row.order_amount) }}</template>
</el-table-column>
<el-table-column label="挂号环比" min-width="110" align="right">
<template #default="{ row }">
<span :class="['rate-text', compareClass(row.registration_compare_rate)]">
{{ compactCompare(row.registration_compare_rate) }}
</span>
</template>
</el-table-column>
<el-table-column label="预约环比" min-width="110" align="right">
<template #default="{ row }">
<span :class="['rate-text', compareClass(row.appointment_compare_rate)]">
{{ compactCompare(row.appointment_compare_rate) }}
@@ -275,7 +234,7 @@
<footer class="data-note">
<el-icon><InfoFilled /></el-icon>
<span>{{ dashboard.meta.registration_rule }}{{ dashboard.meta.appointment_rule }}{{ dashboard.meta.performance_rule }}</span>
<span>{{ dashboard.meta.appointment_rule }}{{ dashboard.meta.performance_rule }}</span>
</footer>
</div>
</template>
@@ -302,16 +261,15 @@ const emptyDashboard = () => ({
meta: {
time_type: 'today', time_label: '今日', start_date: '', end_date: '', generated_at: '',
scope_value: 4, scope_label: '', selected_dept_name: '', selected_assistant_name: '',
member_count: 0, registration_rule: '', appointment_rule: '', performance_rule: ''
member_count: 0, appointment_rule: '', performance_rule: ''
},
filters: { departments: [] as any[], assistants: [] as Array<{ id: number; name: string }> },
summary: {
registration_count: 0, registration_compare_count: 0, registration_compare_rate: null as number | null,
appointment_count: 0, appointment_compare_count: 0, appointment_compare_rate: null as number | null,
order_count: 0, order_amount: 0, range_label: '今日'
},
employee_rows: [] as any[],
rankings: { performance: [] as any[], registrations: [] as any[], appointments: [] as any[] },
rankings: { performance: [] as any[], appointments: [] as any[] },
departments: [] as any[],
target: {
year: new Date().getFullYear(), target_amount: 0, actual_amount: 0,
@@ -331,7 +289,6 @@ const timeOptions = [
const deptTreeProps = { label: 'name', value: 'id', children: 'children' }
const maxPerformance = computed(() => Math.max(0, ...dashboard.rankings.performance.map((item: any) => Number(item.value) || 0)))
const maxRegistrations = computed(() => Math.max(0, ...dashboard.rankings.registrations.map((item: any) => Number(item.value) || 0)))
const maxAppointments = computed(() => Math.max(0, ...dashboard.rankings.appointments.map((item: any) => Number(item.value) || 0)))
const targetChartOption = computed(() => ({
animationDuration: 450,
@@ -533,7 +490,7 @@ h2 { font-size: 15px; line-height: 1.4; }
.metric-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-top: 14px;
}
@@ -556,7 +513,6 @@ h2 { font-size: 15px; line-height: 1.4; }
}
.metric-card--teal .metric-icon { color: #107f75; background: #e9f7f5; }
.metric-card--blue .metric-icon { color: #416bd7; background: #edf1ff; }
.metric-card--violet .metric-icon { color: #7257c7; background: #f2efff; }
.metric-card--amber .metric-icon { color: #bc7428; background: #fff4e7; }
.metric-card span { display: block; color: #748296; font-size: 13px; }
.metric-card strong { display: block; margin: 4px 0 2px; font-size: 27px; line-height: 1.15; }
@@ -609,10 +565,10 @@ h2 { font-size: 15px; line-height: 1.4; }
.chart-legend .actual { background: var(--teal); }
.chart-legend .target { background: repeating-linear-gradient(90deg, var(--blue) 0 5px, transparent 5px 8px); }
.ranking-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.ranking-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.ranking-panel { min-height: 286px; }
.ranking-list { padding: 2px 16px 16px; }
.ranking-row { display: grid; grid-template-columns: 30px minmax(86px, .8fr) minmax(64px, 1fr) auto; align-items: center; gap: 10px; min-height: 43px; border-top: 1px solid #eff2f5; }
.ranking-row { display: grid; grid-template-columns: 30px minmax(110px, .8fr) minmax(100px, 1fr) 105px; align-items: center; gap: 10px; min-height: 43px; border-top: 1px solid #eff2f5; }
.ranking-row > b { display: grid; width: 22px; height: 22px; place-items: center; border-radius: 7px; color: #8491a1; background: #f1f4f6; font-size: 11px; }
.ranking-row > b.is-top { color: #fff; background: var(--teal); }
.ranking-list--blue .ranking-row > b.is-top { background: var(--blue); }
@@ -7,7 +7,7 @@
</div>
<div>
<div class="dashboard-title-row">
<h1>我的首页</h1>
<h1>数据驾驶舱</h1>
<span class="scope-badge">
<el-icon><Lock /></el-icon>
{{ dashboard.scope.label || '数据范围' }}
@@ -66,22 +66,6 @@
</div>
</article>
<article class="metric-card">
<div class="metric-label">今日新增业绩</div>
<div class="metric-value metric-value--money">
{{ formatMoney(dashboard.performance.today_amount) }}
</div>
<div class="metric-foot" :class="comparisonClass(dashboard.performance.today_compare_rate)">
<template v-if="dashboard.performance.today_compare_rate !== null">
<el-icon v-if="dashboard.performance.today_compare_rate >= 0"><CaretTop /></el-icon>
<el-icon v-else><CaretBottom /></el-icon>
{{ dashboard.performance.today_compare_label }}
{{ formatSignedPercent(dashboard.performance.today_compare_rate) }}
</template>
<template v-else>昨日暂无可比数据</template>
</div>
</article>
<article class="metric-card">
<div class="metric-label">昨日新增业绩</div>
<div class="metric-value metric-value--money">
@@ -153,23 +137,7 @@
<h2>{{ dashboard.rankings.appointments.title }}</h2>
<p>{{ appointmentRankingSubtitle }}</p>
</div>
<div class="panel-header-actions">
<el-tree-select
v-if="dashboard.rankings.appointments.kind !== 'doctor'"
v-model="rankingDeptId"
:data="dashboard.filters.ranking_departments"
:props="departmentTreeProps"
node-key="id"
check-strictly
clearable
filterable
default-expand-all
placeholder="全部可见部门"
class="ranking-dept-select"
@change="handleRankingDeptChange"
/>
<span class="panel-meta">实时</span>
</div>
<span class="panel-meta">实时</span>
</div>
<div v-if="dashboard.rankings.appointments.items.length" class="ranking-list">
<div
@@ -181,7 +149,7 @@
<div class="ranking-main">
<div class="ranking-copy">
<strong>{{ item.name || '未命名成员' }}</strong>
<span>{{ formatNumber(item.count) }} 挂号</span>
<span>{{ formatNumber(item.count) }} 挂号</span>
</div>
<div class="ranking-meter" aria-hidden="true">
<span :style="{ width: rankingWidth(item.count, appointmentRankingMax) }" />
@@ -314,7 +282,7 @@
</template>
<script setup lang="ts" name="performanceDashboardPage">
import { computed, onActivated, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import {
CaretBottom,
CaretTop,
@@ -328,7 +296,7 @@ import vCharts from 'vue-echarts'
import { performanceDashboardOverview } from '@/api/stats'
type TrendKey = 'registrations' | 'appointments' | 'leads' | 'orders'
type TrendKey = 'appointments' | 'leads' | 'orders'
interface RankingItem {
id: number
@@ -368,9 +336,6 @@ const createInitialDashboard = () => ({
month_amount: 0,
month_compare_rate: null as number | null,
month_compare_label: '',
today_amount: 0,
today_compare_rate: null as number | null,
today_compare_label: '',
yesterday_amount: 0,
yesterday_compare_rate: null as number | null,
yesterday_compare_label: '',
@@ -379,17 +344,14 @@ const createInitialDashboard = () => ({
today: {
add_fans_count: 0,
appointment_total_count: 0,
low_amount_payment_count: 0,
interview_count: 0,
completed_order_count: 0,
completed_order_amount: 0,
paid_appointment_count: 0,
paid_appointment_rate: 0,
interview_receive_rate: 0,
comparisons: {
add_fans_count: emptyComparison(),
appointment_total_count: emptyComparison(),
low_amount_payment_count: emptyComparison(),
interview_count: emptyComparison(),
completed_order_count: emptyComparison(),
completed_order_amount: emptyComparison(),
@@ -410,14 +372,9 @@ const createInitialDashboard = () => ({
items: [] as RankingItem[],
},
},
filters: {
ranking_departments: [] as any[],
ranking_dept_id: 0,
},
trend: {
date_range: [] as string[],
dates: [] as string[],
registrations: [] as number[],
appointments: [] as number[],
leads: [] as number[],
orders: [] as number[],
@@ -443,15 +400,12 @@ const dashboard = reactive(createInitialDashboard())
const loading = ref(false)
const loaded = ref(false)
const errorMessage = ref('')
const activeTrend = ref<TrendKey>('registrations')
const rankingDeptId = ref<number | undefined>()
const activeTrend = ref<TrendKey>('appointments')
const now = ref(new Date())
let clockTimer: ReturnType<typeof setInterval> | undefined
const departmentTreeProps = { value: 'id', label: 'name', children: 'children' }
const trendOptions = [
{ label: '挂号', value: 'registrations' },
{ label: '预约', value: 'appointments' },
{ label: '挂号', value: 'appointments' },
{ label: '进线', value: 'leads' },
{ label: '诊单', value: 'orders' },
]
@@ -474,18 +428,11 @@ const todayMetrics = computed(() => [
},
{
key: 'appointments',
label: '今日预约',
label: '今日挂号',
value: formatNumber(dashboard.today.appointment_total_count),
hint: '状态为已预约、已完成或改期,排除已取消',
comparisons: [dashboard.today.comparisons.appointment_total_count],
},
{
key: 'lowAmountPayments',
label: '今日挂号',
value: `${formatNumber(dashboard.today.low_amount_payment_count)}`,
hint: '已支付且实收金额大于 0、低于 10 元的订单',
comparisons: [dashboard.today.comparisons.low_amount_payment_count],
},
{
key: 'orders',
label: '今日接诊 / 诊单金额',
@@ -498,14 +445,14 @@ const todayMetrics = computed(() => [
},
{
key: 'appointmentRate',
label: '挂号率',
label: '付费挂号率',
value: formatPercent(dashboard.today.paid_appointment_rate),
hint: `挂号 ${formatNumber(dashboard.today.paid_appointment_count)} / 加粉数`,
hint: '付费挂号数 / 加粉数',
comparisons: [dashboard.today.comparisons.paid_appointment_rate],
},
{
key: 'receiveRate',
label: '接诊率',
label: '面诊接诊率',
value: formatPercent(dashboard.today.interview_receive_rate),
hint: '诊单数 / 面诊数',
comparisons: [dashboard.today.comparisons.interview_receive_rate],
@@ -514,7 +461,7 @@ const todayMetrics = computed(() => [
key: 'interviews',
label: '今日面诊',
value: formatNumber(dashboard.today.interview_count),
hint: '状态为已完成的预约',
hint: '状态为已完成的挂号',
comparisons: [dashboard.today.comparisons.interview_count],
},
])
@@ -534,8 +481,8 @@ const currentDate = computed(() => new Intl.DateTimeFormat('zh-CN', {
}).format(now.value))
const appointmentRankingSubtitle = computed(() => {
const actor = dashboard.rankings.appointments.kind === 'doctor' ? '医生' : '成员'
return `${dashboard.rankings.appointments.scope_label}${actor}已支付且实收低于 10 元的订单笔数`
const actor = dashboard.rankings.appointments.kind === 'doctor' ? '医生' : '医助'
return `${dashboard.rankings.appointments.scope_label}${actor}有效挂号数(不含已取消)`
})
const appointmentRankingMax = computed(() => Math.max(
@@ -550,8 +497,7 @@ const performanceRankingMax = computed(() => Math.max(
const trendChartOption = computed(() => {
const config: Record<TrendKey, { label: string; data: number[] }> = {
registrations: { label: '挂号', data: dashboard.trend.registrations },
appointments: { label: '预约', data: dashboard.trend.appointments },
appointments: { label: '挂号', data: dashboard.trend.appointments },
leads: { label: '进线', data: dashboard.trend.leads },
orders: { label: '诊单', data: dashboard.trend.orders },
}
@@ -655,13 +601,8 @@ const loadDashboard = async () => {
loading.value = true
errorMessage.value = ''
try {
const res: any = await performanceDashboardOverview({
ranking_dept_id: rankingDeptId.value,
// 驾驶舱是实时数据,避免浏览器或反向代理复用旧的 GET 响应。
_t: Date.now(),
})
const res: any = await performanceDashboardOverview()
Object.assign(dashboard, createInitialDashboard(), res || {})
rankingDeptId.value = Number(dashboard.filters.ranking_dept_id || 0) || undefined
loaded.value = true
} catch (error: any) {
errorMessage.value = error?.msg || error?.message || '驾驶舱数据加载失败,请稍后重试'
@@ -670,10 +611,6 @@ const loadDashboard = async () => {
}
}
const handleRankingDeptChange = () => {
loadDashboard()
}
onMounted(() => {
loadDashboard()
clockTimer = setInterval(() => {
@@ -681,11 +618,6 @@ onMounted(() => {
}, 1000)
})
// 后台标签页使用 KeepAlive;从其它菜单返回驾驶舱时必须重新取实时数据。
onActivated(() => {
if (loaded.value) loadDashboard()
})
onBeforeUnmount(() => {
if (clockTimer) clearInterval(clockTimer)
})
@@ -818,12 +750,12 @@ onBeforeUnmount(() => {
}
.performance-strip {
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-bottom: 12px;
}
.operating-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-bottom: 14px;
}
@@ -969,17 +901,6 @@ onBeforeUnmount(() => {
}
}
.panel-header-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.ranking-dept-select {
width: 180px;
}
.panel-meta {
padding: 4px 7px;
color: var(--dash-text-muted);
@@ -1147,10 +1068,6 @@ onBeforeUnmount(() => {
.dashboard-grid--bottom {
grid-template-columns: 1fr;
}
.operating-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
@@ -1195,15 +1112,6 @@ onBeforeUnmount(() => {
flex-direction: column;
}
.panel-header-actions {
align-items: flex-end;
flex-direction: column;
}
.ranking-dept-select {
width: 150px;
}
.ranking-copy span {
display: none;
}
File diff suppressed because one or more lines are too long
@@ -11,10 +11,8 @@ use app\adminapi\lists\firstvisit\MyPatientProgressLists;
use app\adminapi\logic\auth\AuthLogic;
use app\adminapi\logic\doctor\AppointmentLogic;
use app\adminapi\logic\firstvisit\MyPatientLogic;
use app\adminapi\logic\tcm\DiagnosisLogic;
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
use app\adminapi\validate\doctor\AppointmentValidate;
use app\adminapi\validate\tcm\DiagnosisValidate;
use app\adminapi\validate\tcm\PrescriptionOrderValidate;
use app\common\model\doctor\Appointment;
use app\common\model\tcm\PrescriptionOrder;
@@ -54,74 +52,6 @@ class MyPatientController extends BaseAdminController
return $this->dataLists(new MyPatientProgressLists());
}
/** 当前账号数据范围内可被指派的医助。 */
public function assistants()
{
if (!$this->hasPagePermission() || !$this->hasOriginalPermission('tcm.diagnosis/assign')) {
return $this->fail('权限不足,无法获取医助列表');
}
return $this->data(DiagnosisLogic::getAssistants($this->adminId, $this->adminInfo));
}
/** 从“我的患者”指派医助,先校验患者行级数据范围和目标医助范围。 */
public function assign()
{
if (!$this->hasPagePermission() || !$this->hasOriginalPermission('tcm.diagnosis/assign')) {
return $this->fail('权限不足,无法指派患者');
}
$params = $this->request->post();
$diagnosisId = (int) ($params['id'] ?? 0);
$assistantId = (int) ($params['assistant_id'] ?? 0);
if (!MyPatientLogic::canAccessDiagnosis($diagnosisId, $this->adminId, $this->adminInfo)) {
return $this->fail('患者不存在或无权操作');
}
if ($assistantId <= 0 || !$this->canAssignToAssistant($assistantId)) {
return $this->fail('所选医助不在当前可指派范围内');
}
$result = DiagnosisLogic::assign([
'id' => $diagnosisId,
'assistant_id' => $assistantId,
'is_inherit' => (int) ($params['is_inherit'] ?? 0) === 1 ? 1 : 0,
]);
if ($result === false) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->success('指派成功');
}
/** 从“我的患者”补全身份证,复用诊单身份证校验和年龄计算。 */
public function fillIdCard()
{
if (!$this->hasPagePermission() || !$this->hasOriginalPermission('tcm.diagnosis/edit')) {
return $this->fail('权限不足,无法补全身份证');
}
$params = (new DiagnosisValidate())->post()->goCheck('fillIdCard');
$diagnosisId = (int) ($params['id'] ?? 0);
if (!MyPatientLogic::canAccessDiagnosis($diagnosisId, $this->adminId, $this->adminInfo)) {
return $this->fail('患者不存在或无权操作');
}
$duplicate = DiagnosisLogic::checkIdCard([
'id' => $diagnosisId,
'id_card' => trim((string) ($params['id_card'] ?? '')),
]);
if (!empty($duplicate['exists'])) {
return $this->fail((string) ($duplicate['message'] ?? '该身份证号已存在'));
}
$result = DiagnosisLogic::fillIdCard($params);
if ($result === false) {
return $this->fail(DiagnosisLogic::getError());
}
return $this->success('补全成功,年龄已自动更新');
}
/** 当前患者范围内的订单详情;仍要求原订单详情权限。 */
public function orderDetail()
{
@@ -454,17 +384,6 @@ class MyPatientController extends BaseAdminController
return $order;
}
private function canAssignToAssistant(int $assistantId): bool
{
foreach (DiagnosisLogic::getAssistants($this->adminId, $this->adminInfo) as $assistant) {
if ((int) ($assistant['id'] ?? 0) === $assistantId) {
return true;
}
}
return false;
}
/** @param array<string,mixed> $params @param array<int,string> $keys */
private function onlyParams(array $params, array $keys): array
{
@@ -18,17 +18,6 @@ class PerformanceDashboardController extends BaseAdminController
{
@set_time_limit(120);
$response = $this->data(PerformanceDashboardLogic::overview(
$this->adminId,
$this->adminInfo,
$this->request->get()
));
// 驾驶舱包含分钟级实时数据,禁止浏览器和中间代理缓存旧统计结果。
return $response->header([
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
'Pragma' => 'no-cache',
'Expires' => '0',
]);
return $this->data(PerformanceDashboardLogic::overview($this->adminId, $this->adminInfo));
}
}
@@ -37,7 +37,7 @@ class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface,
$rows = $query
->field([
'd.id', 'd.patient_id', 'd.patient_name', 'd.phone', 'd.id_card', 'd.gender', 'd.age',
'd.id', 'd.patient_id', 'd.patient_name', 'd.phone', 'd.gender', 'd.age',
'd.diagnosis_date', 'd.diagnosis_type', 'd.syndrome_type', 'd.assistant_id',
'd.assign_read_at', 'd.create_time',
])
@@ -89,17 +89,18 @@ class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface,
$this->applyKeyword($query);
$statusFilter = $applyStatusFilter ? trim((string) ($this->params['status_filter'] ?? '')) : '';
$appointmentTable = (new Appointment())->getTable();
if ($statusFilter === 'unbooked') {
if ($statusFilter === 'unconfirmed') {
$viewTable = (new DiagnosisViewRecord())->getTable();
$query->whereNotExists(
"SELECT 1 FROM {$appointmentTable} unbooked_apt"
. ' WHERE unbooked_apt.patient_id = d.id'
. ' AND unbooked_apt.status IN (' . implode(',', self::EFFECTIVE_APPOINTMENT_STATUSES) . ')'
"SELECT 1 FROM {$viewTable} confirm_row"
. ' WHERE confirm_row.diagnosis_id = d.id'
. ' AND confirm_row.is_confirmed = 1'
. ' AND confirm_row.delete_time IS NULL'
);
}
$appointmentStatuses = self::EFFECTIVE_APPOINTMENT_STATUSES;
if (in_array($statusFilter, ['pending_interview', 'booked'], true)) {
if ($statusFilter === 'booked') {
$appointmentStatuses = [1];
} elseif ($statusFilter === 'completed') {
$appointmentStatuses = [3];
@@ -107,17 +108,14 @@ class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface,
$appointmentStatuses = [4];
}
$needsAppointmentFilter = in_array(
$statusFilter,
['pending_interview', 'booked', 'completed', 'missed'],
true
);
$needsAppointmentFilter = in_array($statusFilter, ['booked', 'completed', 'missed'], true);
[$startDate, $endDate] = $applyDateFilter ? $this->dateRange() : ['', ''];
if ($startDate !== '' || $endDate !== '') {
$needsAppointmentFilter = true;
}
if ($needsAppointmentFilter) {
$appointmentTable = (new Appointment())->getTable();
$conditions = [
'filter_apt.patient_id = d.id',
'filter_apt.status IN (' . implode(',', $appointmentStatuses) . ')',
@@ -247,7 +245,6 @@ class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface,
$today = date('Y-m-d');
$statusFilter = trim((string) ($this->params['status_filter'] ?? ''));
$preferredStatuses = [
'pending_interview' => [1],
'booked' => [1],
'completed' => [3],
'missed' => [4],
@@ -272,8 +269,6 @@ class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface,
$row['source_patient_id'] = (int) ($row['patient_id'] ?? 0);
$row['phone_masked'] = $this->maskPhone((string) ($row['phone'] ?? ''));
unset($row['phone']);
$row['has_id_card'] = trim((string) ($row['id_card'] ?? '')) !== '' ? 1 : 0;
unset($row['id_card']);
$row['gender_desc'] = (int) ($row['gender'] ?? 0) === 1 ? '男' : '女';
$row['diagnosis_date_text'] = $this->formatDiagnosisDate($row['diagnosis_date'] ?? '');
$row['assistant_name'] = (string) ($adminNames[$assistantId] ?? '未分配');
@@ -367,6 +362,6 @@ class MyPatientLists extends BaseAdminDataLists implements ListsSearchInterface,
private function appointmentStatusText(int $status): string
{
return [1 => '待面诊', 3 => '已完成', 4 => '已过号'][$status] ?? '未预约';
return [1 => '已挂号', 3 => '已完成', 4 => '已过号'][$status] ?? '未挂号';
}
}
@@ -65,18 +65,9 @@ class MyPatientOrderLists extends BaseAdminDataLists implements ListsSearchInter
$effectiveAmountQuery = clone $query;
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($effectiveAmountQuery, 'po');
// 拒收指标保留关键词、审核和日期条件,但不受当前履约状态按钮影响,
// 避免点击“拒收订单”后分母被收窄为拒收状态而固定显示 100%。
$rejectionScopeQuery = $this->buildQuery(true);
$rejectionScopeOrderCount = (int) (clone $rejectionScopeQuery)->count('po.id');
$rejectedCount = (int) (clone $rejectionScopeQuery)
->where('po.fulfillment_status', 9)
->count('po.id');
$orderCount = (int) (clone $query)->count('po.id');
return [
'summary' => [
'orders' => $orderCount,
'orders' => (int) (clone $query)->count('po.id'),
'amount' => round((float) $effectiveAmountQuery->sum('po.amount'), 2),
'pending' => (int) $pendingQuery
->where(function ($q) {
@@ -85,16 +76,12 @@ class MyPatientOrderLists extends BaseAdminDataLists implements ListsSearchInter
})
->count('po.id'),
'completed' => (int) (clone $query)->whereIn('po.fulfillment_status', [3, 6])->count('po.id'),
'rejected' => $rejectedCount,
'rejection_rate' => $rejectionScopeOrderCount > 0
? round($rejectedCount / $rejectionScopeOrderCount * 100, 2)
: 0.0,
],
'scope' => MyPatientLogic::scopeMeta($this->adminId, $this->adminInfo),
];
}
private function buildQuery(bool $ignoreFulfillmentStatus = false): Query
private function buildQuery(): Query
{
$query = PrescriptionOrder::alias('po')
->join('tcm_diagnosis d', 'po.diagnosis_id = d.id')
@@ -104,7 +91,7 @@ class MyPatientOrderLists extends BaseAdminDataLists implements ListsSearchInter
MyPatientLogic::applyScope($query, $this->adminId, $this->adminInfo);
$this->applyKeyword($query);
$this->applyStatusFilters($query, $ignoreFulfillmentStatus);
$this->applyStatusFilters($query);
$this->applyDateFilter($query);
return $query;
@@ -135,12 +122,9 @@ class MyPatientOrderLists extends BaseAdminDataLists implements ListsSearchInter
});
}
private function applyStatusFilters(Query $query, bool $ignoreFulfillmentStatus = false): void
private function applyStatusFilters(Query $query): void
{
foreach (['prescription_audit_status', 'payment_slip_audit_status', 'fulfillment_status'] as $field) {
if ($ignoreFulfillmentStatus && $field === 'fulfillment_status') {
continue;
}
$raw = $this->params[$field] ?? '';
if ($raw === '' || $raw === null) {
continue;
@@ -11,7 +11,6 @@ use app\common\model\auth\Admin;
use app\common\model\auth\AdminDept;
use app\common\model\stats\PersonalYeji;
use app\common\service\DataScope\DataScopeService;
use app\common\service\qywx\MediaChannelService;
use think\facade\Db;
/**
@@ -33,12 +32,6 @@ class FirstVisitConversionLogic
$scopeValue = DataScopeService::getEffectiveScope($adminInfo);
$selectedDeptId = max(0, (int) ($params['dept_id'] ?? 0));
$selectedAssistantId = max(0, (int) ($params['assistant_id'] ?? 0));
$selectedMediaChannelCode = MediaChannelService::normalizeStatsCode(
trim((string) ($params['media_channel_code'] ?? ''))
);
$selectedMediaChannel = $selectedMediaChannelCode !== ''
? MediaChannelService::getChannelByCode($selectedMediaChannelCode)
: null;
$deptSelectionValid = $selectedDeptId <= 0
|| $allowedDeptSet === null
@@ -80,19 +73,14 @@ class FirstVisitConversionLogic
'time_type' => 'custom',
'start_date' => $startDate,
'end_date' => $endDate,
'include_filters' => 1,
'include_members' => 1,
'exclude_cancelled_appointments' => 1,
'order_metric_mode' => 'performance',
'include_filters' => 0,
'include_members' => 0,
'page_no' => 1,
'page_size' => 100,
];
if ($selectedDeptId > 0 && $deptSelectionValid) {
$conversionParams['dept_id'] = $selectedDeptId;
}
if ($selectedMediaChannelCode !== '') {
$conversionParams['media_channel_code'] = $selectedMediaChannelCode;
}
$conversion = ConversionLogic::overview(
$conversionParams,
@@ -109,26 +97,16 @@ class FirstVisitConversionLogic
$rowDeptIdSet = [];
self::collectRowDeptIds($rows, $rowDeptIdSet);
$openCounts = self::loadOpenCounts(
$openDirect = self::loadOpenCountByDept(
$startDate,
$endDate,
$effectiveAdminIds,
array_fill_keys(array_keys($rowDeptIdSet), true),
self::personalYejiMediaSources($selectedMediaChannelCode, $selectedMediaChannel)
array_fill_keys(array_keys($rowDeptIdSet), true)
);
$openDirect = $openCounts['dept'];
self::applyOpenCounts($rows, $openDirect, $openCounts['admin']);
self::applyOpenCounts($rows, $openDirect);
$summary = is_array($conversion['summary'] ?? null) ? $conversion['summary'] : [];
$summary['total_open_count'] = array_sum($openDirect);
$summary['total_open_rate'] = self::percent(
(int) $summary['total_open_count'],
(int) ($summary['add_fans_count'] ?? 0)
);
$summary['open_appointment_rate'] = self::percent(
(int) ($summary['paid_appointment_count'] ?? 0),
(int) $summary['total_open_count']
);
$summary['open_receive_rate'] = self::percent(
(int) ($summary['completed_order_count'] ?? 0),
(int) $summary['total_open_count']
@@ -147,12 +125,6 @@ class FirstVisitConversionLogic
$selectedAssistantName = $selectedAssistantId > 0
? (string) (Admin::where('id', $selectedAssistantId)->whereNull('delete_time')->value('name') ?? '')
: '';
$selectedMediaChannelName = $selectedMediaChannelCode !== ''
? (string) ($selectedMediaChannel['channel_name'] ?? $selectedMediaChannelCode)
: '';
$conversionFilters = is_array($conversion['extend']['filters'] ?? null)
? $conversion['extend']['filters']
: [];
return [
'meta' => [
@@ -165,21 +137,11 @@ class FirstVisitConversionLogic
'scope_label' => DataScopeService::scopeLabel($scopeValue),
'selected_dept_name' => $selectedDeptName,
'selected_assistant_name' => $selectedAssistantName,
'selected_media_channel_code' => $selectedMediaChannelCode,
'selected_media_channel_name' => $selectedMediaChannelName,
'open_count_source' => $selectedMediaChannelCode === ''
? '个人业绩录入'
: '个人业绩录入(按渠道名称匹配)',
'appointment_rule' => '按预约日期统计,归属优先挂号医助、再回退诊单医助;仅含已预约、已完成和已过号',
'registration_rule' => '按支付时间统计已支付且实收金额大于 0、低于 10 元的订单,每笔计 1 个挂号并按订单创建人归属',
'performance_rule' => '按业务订单创建时间和创建人统计,排除取消、拒收、退款及已发生退款的订单',
'open_count_source' => '个人业绩录入',
],
'filters' => [
'departments' => DeptLogic::getAllDataScoped($adminId, $adminInfo),
'assistants' => self::assistantOptions($baseVisibleAdminIds, $selectedDeptIds, $selectedDeptId),
'media_channels' => is_array($conversionFilters['media_channels'] ?? null)
? $conversionFilters['media_channels']
: [],
],
'summary' => $summary,
'rankings' => [
@@ -296,10 +258,6 @@ class FirstVisitConversionLogic
if (!is_array($row)) {
continue;
}
if (in_array((string) ($row['type'] ?? ''), ['member', 'unbound'], true)) {
$out[] = $row;
continue;
}
$children = self::filterDeptRows(is_array($row['children'] ?? null) ? $row['children'] : [], $allowedSet);
$id = (int) ($row['id'] ?? 0);
if (isset($allowedSet[$id])) {
@@ -330,37 +288,23 @@ class FirstVisitConversionLogic
}
}
/**
* @param int[]|null $effectiveAdminIds
* @param array<int,true> $rowDeptSet
* @param string[]|null $mediaSources null=全部渠道;空数组=所选渠道没有可匹配的手工来源
* @return array{dept:array<int,int>,admin:array<int,int>}
*/
private static function loadOpenCounts(
string $startDate,
string $endDate,
?array $effectiveAdminIds,
array $rowDeptSet,
?array $mediaSources = null
): array
/** @param int[]|null $effectiveAdminIds @param array<int,true> $rowDeptSet @return array<int,int> */
private static function loadOpenCountByDept(string $startDate, string $endDate, ?array $effectiveAdminIds, array $rowDeptSet): array
{
if ($effectiveAdminIds === [] || $rowDeptSet === [] || $mediaSources === []) {
return ['dept' => [], 'admin' => []];
if ($effectiveAdminIds === [] || $rowDeptSet === []) {
return [];
}
$query = PersonalYeji::whereBetween('yeji_date', [$startDate, $endDate]);
if ($effectiveAdminIds !== null) {
$query->whereIn('creator_id', $effectiveAdminIds);
}
if ($mediaSources !== null) {
$query->whereIn('media_source', $mediaSources);
}
$rows = $query
->fieldRaw('creator_id, SUM(total_open_count) AS open_count')
->group('creator_id')
->select()
->toArray();
if ($rows === []) {
return ['dept' => [], 'admin' => []];
return [];
}
$creatorIds = self::normalizeIds(array_column($rows, 'creator_id'));
@@ -374,54 +318,8 @@ class FirstVisitConversionLogic
foreach ($deptRows as $deptRow) {
$adminDeptMap[(int) $deptRow['admin_id']][] = (int) $deptRow['dept_id'];
}
$deptMetaRows = Db::name('dept')
->whereNull('delete_time')
->field('id, pid, sort')
->select()
->toArray();
$deptMeta = [];
foreach ($deptMetaRows as $deptMetaRow) {
$deptId = (int) ($deptMetaRow['id'] ?? 0);
if ($deptId > 0) {
$deptMeta[$deptId] = [
'pid' => (int) ($deptMetaRow['pid'] ?? 0),
'sort' => (int) ($deptMetaRow['sort'] ?? 0),
];
}
}
$depthCache = [];
$depthOf = static function (int $deptId) use (&$depthOf, &$depthCache, $deptMeta): int {
if ($deptId <= 0 || !isset($deptMeta[$deptId])) {
return 0;
}
if (isset($depthCache[$deptId])) {
return $depthCache[$deptId];
}
$parentId = (int) ($deptMeta[$deptId]['pid'] ?? 0);
if ($parentId <= 0 || $parentId === $deptId || !isset($deptMeta[$parentId])) {
return $depthCache[$deptId] = 0;
}
return $depthCache[$deptId] = $depthOf($parentId) + 1;
};
foreach ($adminDeptMap as &$deptIds) {
usort($deptIds, static function (int $left, int $right) use ($depthOf, $deptMeta): int {
$depthCompare = $depthOf($right) <=> $depthOf($left);
if ($depthCompare !== 0) {
return $depthCompare;
}
$sortCompare = (int) ($deptMeta[$right]['sort'] ?? 0) <=> (int) ($deptMeta[$left]['sort'] ?? 0);
if ($sortCompare !== 0) {
return $sortCompare;
}
return $left <=> $right;
});
}
unset($deptIds);
$direct = [];
$adminDirect = [];
foreach ($rows as $row) {
$adminId = (int) ($row['creator_id'] ?? 0);
$targetDeptId = 0;
@@ -435,117 +333,41 @@ class FirstVisitConversionLogic
$targetDeptId = -2;
}
if ($targetDeptId !== 0) {
$openCount = (int) ($row['open_count'] ?? 0);
$direct[$targetDeptId] = ($direct[$targetDeptId] ?? 0) + $openCount;
$adminDirect[$adminId] = ($adminDirect[$adminId] ?? 0) + $openCount;
$direct[$targetDeptId] = ($direct[$targetDeptId] ?? 0) + (int) ($row['open_count'] ?? 0);
}
}
return ['dept' => $direct, 'admin' => $adminDirect];
return $direct;
}
/**
* @param array<int,array<string,mixed>> $rows
* @param array<int,int> $deptDirect
* @param array<int,int> $adminDirect
*/
private static function applyOpenCounts(array &$rows, array $deptDirect, array $adminDirect): int
/** @param array<int,array<string,mixed>> $rows @param array<int,int> $direct */
private static function applyOpenCounts(array &$rows, array $direct): int
{
$sum = 0;
foreach ($rows as &$row) {
$rowType = (string) ($row['type'] ?? '');
if (in_array($rowType, ['member', 'unbound'], true)) {
$count = $rowType === 'member'
? (int) ($adminDirect[(int) ($row['admin_id'] ?? 0)] ?? 0)
: 0;
$row['total_open_count'] = $count;
$row['total_open_rate'] = self::percent($count, (int) ($row['add_fans_count'] ?? 0));
$row['open_appointment_rate'] = self::percent(
(int) ($row['paid_appointment_count'] ?? 0),
$count
);
$row['open_receive_rate'] = self::percent(
(int) ($row['completed_order_count'] ?? 0),
$count
);
continue;
}
$children = is_array($row['children'] ?? null) ? $row['children'] : [];
$childTotal = self::applyOpenCounts($children, $deptDirect, $adminDirect);
$childTotal = self::applyOpenCounts($children, $direct);
if ($children !== []) {
$row['children'] = $children;
}
$directCount = (int) ($deptDirect[(int) ($row['id'] ?? 0)] ?? 0);
$count = $directCount + $childTotal;
$count = (int) ($direct[(int) ($row['id'] ?? 0)] ?? 0) + $childTotal;
$row['total_open_count'] = $count;
$row['total_open_rate'] = self::percent($count, (int) ($row['add_fans_count'] ?? 0));
$row['open_appointment_rate'] = self::percent(
(int) ($row['paid_appointment_count'] ?? 0),
$count
);
$row['open_receive_rate'] = self::percent((int) ($row['completed_order_count'] ?? 0), $count);
$sum += $directCount + $childTotal;
$sum += (int) ($direct[(int) ($row['id'] ?? 0)] ?? 0) + $childTotal;
}
unset($row);
return $sum;
}
/**
* 手工开口按 personal_yeji.media_source 保存;渠道筛选时仅匹配该渠道自身的稳定标识和名称。
* 不使用 source_group_name,避免同组多个渠道的开口数被重复计入每个渠道。
*
* @param array<string,mixed>|null $channel
* @return string[]|null
*/
private static function personalYejiMediaSources(string $channelCode, ?array $channel): ?array
{
if ($channelCode === '') {
return null;
}
if ($channel === null) {
return [];
}
return array_values(array_unique(array_filter(array_map(
static fn ($value): string => trim((string) $value),
[
$channelCode,
$channel['channel_name'] ?? '',
$channel['source_tag_name'] ?? '',
]
), static fn (string $value): bool => $value !== '')));
}
/** @param array<int,array<string,mixed>> $rows @return array<int,array<string,mixed>> */
private static function rankingRows(array $rows): array
{
// lists 里可能同时存在“未绑定/未分配部门”等虚拟根节点。它们会让顶层节点数量
// 大于 1,导致原逻辑无法展开唯一的真实组织根节点,图表最终只显示医院汇总行。
$visibleRows = array_values(array_filter($rows, static function (array $row): bool {
return (int) ($row['id'] ?? 0) > 0 && !((bool) ($row['_virtual_bucket'] ?? false));
}));
// 每个可见顶层分支只展示同一层级:有权限看到下级时展示直属子部门;没有可见
// 下级时保留当前部门。这样既能按角色/DataScope 展示子部门,也不会把父子汇总
// 同时放进占比图造成重复计算。
$chartRows = [];
foreach ($visibleRows as $row) {
$children = array_values(array_filter(
is_array($row['children'] ?? null) ? $row['children'] : [],
static fn (array $child): bool => (int) ($child['id'] ?? 0) > 0
&& !((bool) ($child['_virtual_bucket'] ?? false))
));
if ($children !== []) {
foreach ($children as $child) {
$chartRows[] = $child;
}
continue;
}
$chartRows[] = $row;
if (count($rows) === 1 && is_array($rows[0]['children'] ?? null) && $rows[0]['children'] !== []) {
return $rows[0]['children'];
}
return $chartRows;
return $rows;
}
/** @param array<int,array<string,mixed>> $rows @return array<int,array<string,mixed>> */
@@ -627,14 +449,17 @@ class FirstVisitConversionLogic
if ($effectiveAdminIds !== []) {
$actualQuery = Db::name('tcm_prescription_order')
->alias('po')
->leftJoin('tcm_prescription rx', 'rx.id = po.prescription_id')
->whereNull('po.delete_time')
->where('po.prescription_audit_status', 1)
->where('po.payment_slip_audit_status', 1)
->where('po.create_time', 'between', [
strtotime($year . '-01-01 00:00:00'),
strtotime($year . '-12-31 23:59:59'),
]);
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($actualQuery, 'po');
if ($effectiveAdminIds !== null) {
$actualQuery->whereIn('po.creator_id', $effectiveAdminIds);
$actualQuery->whereIn('rx.assistant_id', $effectiveAdminIds);
}
$actualRows = $actualQuery
->fieldRaw("DATE_FORMAT(FROM_UNIXTIME(po.create_time), '%m') AS month_no, SUM(po.amount) AS actual_amount")
@@ -14,7 +14,7 @@ use think\facade\Db;
/**
* 一诊「医生看板」。
*
* 医生是最终展示维度;部门权限通过实际经手医助下推到预约、诊单与业绩:
* 医生是最终展示维度;部门权限通过实际经手医助下推到挂号、诊单与业绩:
* - 医生 SELF:只看本人医生数据,不限制经手医助;
* - 医助 SELF:只看本人经手患者关联的医生数据;
* - 组长/经理:只看数据范围内医助经手患者关联的医生数据;
@@ -79,15 +79,7 @@ class FirstVisitDoctorDashboardLogic
$doctorDeptNames,
$doctorStatus
);
// 支付单没有医生字段,当前数据中的低额支付单也未关联患者;挂号只能按创建人及权限范围汇总,
// 不能为了医生排行而将医助创建的支付单虚构分摊给某位医生。
$registrationCreatorIds = $doctorSelf ? [$adminId] : $assistantIds;
$registrationTotal = self::loadRegistrationTotal(
$range['start'],
$range['end'],
$registrationCreatorIds
);
$summary = self::buildSummary($rows, $registrationTotal);
$summary = self::buildSummary($rows);
$trend = self::buildAmountTrend($doctorIds, $assistantIds);
$selectedDeptName = $selectedDeptId > 0
@@ -116,8 +108,7 @@ class FirstVisitDoctorDashboardLogic
'selected_dept_name' => $selectedDeptName,
'selected_doctor_name' => $selectedDoctorName,
'doctor_count' => count($rows),
'registration_rule' => '总挂号按支付时间统计已支付且实收金额大于 0、低于 10 元的订单,每笔计 1 个,并按订单创建人及当前权限范围归属',
'appointment_rule' => '总预约包含已预约、已取消、已完成和已过号;面诊取状态为已完成的预约',
'appointment_rule' => '总挂号包含已预约、已取消、已完成和已过号;面诊取状态为已完成的挂号',
'performance_rule' => '诊单按订单创建时间统计,排除已取消、拒收、全额退款及部分退款,金额归属处方开方医生',
],
'filters' => [
@@ -131,8 +122,7 @@ class FirstVisitDoctorDashboardLogic
'conversion' => self::ranking($rows, 'receive_conversion_rate', 8),
],
'funnel' => [
['key' => 'registration', 'label' => '挂号', 'value' => (int) $summary['registration_total']],
['key' => 'appointment', 'label' => '预约', 'value' => (int) $summary['appointment_total']],
['key' => 'appointment', 'label' => '挂号', 'value' => (int) $summary['appointment_total']],
['key' => 'interview', 'label' => '面诊', 'value' => (int) $summary['interview_count']],
['key' => 'receive', 'label' => '接诊', 'value' => (int) $summary['order_count']],
['key' => 'deal', 'label' => '成交', 'value' => (int) $summary['order_count']],
@@ -337,7 +327,7 @@ class FirstVisitDoctorDashboardLogic
}
/** @param array<int,array<string,mixed>> $rows @return array<string,mixed> */
private static function buildSummary(array $rows, int $registrationTotal): array
private static function buildSummary(array $rows): array
{
$appointmentTotal = 0;
$interviewCount = 0;
@@ -355,7 +345,6 @@ class FirstVisitDoctorDashboardLogic
}
return [
'registration_total' => $registrationTotal,
'appointment_total' => $appointmentTotal,
'interview_count' => $interviewCount,
'order_count' => $orderCount,
@@ -372,40 +361,6 @@ class FirstVisitDoctorDashboardLogic
];
}
/**
* 新挂号口径:支付时间位于筛选区间、状态为已支付、0 < 实收金额 < 10 元。
* null 表示全部创建人,空数组表示当前权限范围没有可统计创建人。
*
* @param int[]|null $creatorIds
*/
private static function loadRegistrationTotal(
string $startDate,
string $endDate,
?array $creatorIds
): int {
if ($creatorIds === []) {
return 0;
}
$query = Db::name('order')
->whereNull('delete_time')
->where('status', 2)
->where('amount', '>', 0)
->where('amount', '<', 10)
->whereNotNull('payment_time')
->where('payment_time', '<>', '')
->whereBetweenTime(
'payment_time',
$startDate . ' 00:00:00',
$endDate . ' 23:59:59'
);
if ($creatorIds !== null) {
$query->whereIn('creator_id', $creatorIds);
}
return (int) $query->count();
}
/** @param array<int,array<string,mixed>> $rows @return array<int,array<string,mixed>> */
private static function ranking(array $rows, string $field, int $limit): array
{
@@ -14,9 +14,7 @@ use think\facade\Db;
* 一诊「挂号统计」。
*
* 统计口径:
* - 挂号:order.payment_time,已支付且 0 < amount < 10,每笔支付订单计 1 个
* 按支付订单 creator_id 归属员工。
* - 预约:doctor_appointment.appointment_date,状态 1/3/4,排除已取消 2;
* - 挂号:doctor_appointment.appointment_date,状态 1/3/4,排除已取消 2
* 归属优先挂号医助 assistant_id,再回退诊单医助 assistant_id。
* - 诊单:tcm_prescription_order.create_time,归属订单 creator_id,排除履约 4/9/10。
* - 所有部门和员工筛选都只能收窄 DataScope,不允许 HTTP 参数扩大当前账号范围。
@@ -64,11 +62,6 @@ class FirstVisitRegistrationStatsLogic
$range['day_after_tomorrow'],
$assistantIds
);
$registrationDaily = self::loadRegistrationDaily(
min($range['compare_start'], $range['start']),
$range['end'],
$assistantIds
);
$orderDaily = self::loadOrderDaily(
min($range['compare_start'], $range['start']),
$range['end'],
@@ -80,7 +73,6 @@ class FirstVisitRegistrationStatsLogic
$assistantIds,
$assignment,
$appointmentDaily,
$registrationDaily,
$orderDaily,
$range
);
@@ -120,7 +112,6 @@ class FirstVisitRegistrationStatsLogic
'selected_dept_name' => $selectedDeptName,
'selected_assistant_name' => $selectedAssistantName,
'member_count' => count($assistantIds),
'registration_rule' => '支付时间在统计区间,状态为已支付且实收金额低于 10 元(大于 0 元),每笔支付订单计 1 个挂号',
'appointment_rule' => '预约日期在统计区间,状态为已预约、已完成或已过号,排除已取消',
'performance_rule' => '按订单创建时间和创建人统计,排除已取消、拒收和退款',
],
@@ -132,7 +123,6 @@ class FirstVisitRegistrationStatsLogic
'employee_rows' => $groups,
'rankings' => [
'performance' => self::rankMembers($members, 'order_amount', 10),
'registrations' => self::rankMembers($members, 'registration_count', 10),
'appointments' => self::rankMembers($members, 'appointment_count', 10),
],
'departments' => self::departmentSummaryRows($groups),
@@ -315,39 +305,6 @@ class FirstVisitRegistrationStatsLogic
return $out;
}
/** @param int[] $assistantIds @return array<int,array<string,array{count:int}>> */
private static function loadRegistrationDaily(string $startDate, string $endDate, array $assistantIds): array
{
if ($assistantIds === []) {
return [];
}
$rows = Db::name('order')->alias('o')
->whereNull('o.delete_time')
->where('o.status', 2)
->where('o.amount', '>', 0)
->where('o.amount', '<', 10)
->whereBetweenTime(
'o.payment_time',
$startDate . ' 00:00:00',
$endDate . ' 23:59:59'
)
->whereIn('o.creator_id', $assistantIds)
->fieldRaw('o.creator_id AS assistant_id, DATE(o.payment_time) AS date_label, COUNT(*) AS item_count')
->group(['o.creator_id', 'date_label'])
->select()
->toArray();
$out = [];
foreach ($rows as $row) {
$aid = (int) ($row['assistant_id'] ?? 0);
$date = (string) ($row['date_label'] ?? '');
if ($aid > 0 && $date !== '') {
$out[$aid][$date] = ['count' => (int) ($row['item_count'] ?? 0)];
}
}
return $out;
}
/** @param int[] $assistantIds @return array<int,array<string,array{count:int,amount:float}>> */
private static function loadOrderDaily(string $startDate, string $endDate, array $assistantIds): array
{
@@ -388,7 +345,6 @@ class FirstVisitRegistrationStatsLogic
array $assistantIds,
array $assignment,
array $appointmentDaily,
array $registrationDaily,
array $orderDaily,
array $range
): array {
@@ -400,8 +356,6 @@ class FirstVisitRegistrationStatsLogic
foreach ($assistantIds as $aid) {
$appointmentCount = self::sumDaily($appointmentDaily[$aid] ?? [], $range['start'], $range['end'], 'count');
$compareAppointmentCount = self::sumDaily($appointmentDaily[$aid] ?? [], $range['compare_start'], $range['compare_end'], 'count');
$registrationCount = self::sumDaily($registrationDaily[$aid] ?? [], $range['start'], $range['end'], 'count');
$compareRegistrationCount = self::sumDaily($registrationDaily[$aid] ?? [], $range['compare_start'], $range['compare_end'], 'count');
$orderCount = self::sumDaily($orderDaily[$aid] ?? [], $range['start'], $range['end'], 'count');
$orderAmount = self::sumDaily($orderDaily[$aid] ?? [], $range['start'], $range['end'], 'amount');
$rows[] = [
@@ -410,9 +364,6 @@ class FirstVisitRegistrationStatsLogic
'dept_id' => (int) ($assignment[$aid] ?? 0),
'name' => (string) ($assistantIndex[$aid] ?? '未命名员工'),
'row_type' => 'employee',
'registration_count' => (int) $registrationCount,
'compare_registration_count' => (int) $compareRegistrationCount,
'registration_compare_rate' => self::relativeChange($registrationCount, $compareRegistrationCount),
'appointment_count' => (int) $appointmentCount,
'compare_appointment_count' => (int) $compareAppointmentCount,
'appointment_compare_rate' => self::relativeChange($appointmentCount, $compareAppointmentCount),
@@ -423,7 +374,7 @@ class FirstVisitRegistrationStatsLogic
'status' => 'normal',
];
}
usort($rows, static fn (array $a, array $b): int => ($b['registration_count'] <=> $a['registration_count']) ?: ($b['appointment_count'] <=> $a['appointment_count']) ?: ($b['order_amount'] <=> $a['order_amount']));
usort($rows, static fn (array $a, array $b): int => ($b['appointment_count'] <=> $a['appointment_count']) ?: ($b['order_amount'] <=> $a['order_amount']));
return $rows;
}
@@ -442,8 +393,6 @@ class FirstVisitRegistrationStatsLogic
'name' => $key > 0 ? (string) ($deptIndex[$key]['name'] ?? '未命名部门') : '未分配部门',
'row_type' => 'department',
'member_count' => 0,
'registration_count' => 0,
'compare_registration_count' => 0,
'appointment_count' => 0,
'compare_appointment_count' => 0,
'tomorrow_count' => 0,
@@ -456,17 +405,13 @@ class FirstVisitRegistrationStatsLogic
}
$groups[$key]['children'][] = $member;
$groups[$key]['member_count']++;
foreach (['registration_count', 'compare_registration_count', 'appointment_count', 'compare_appointment_count', 'tomorrow_count', 'day_after_count', 'order_count'] as $field) {
foreach (['appointment_count', 'compare_appointment_count', 'tomorrow_count', 'day_after_count', 'order_count'] as $field) {
$groups[$key][$field] += (int) ($member[$field] ?? 0);
}
$groups[$key]['order_amount'] += (float) ($member['order_amount'] ?? 0);
}
foreach ($groups as &$group) {
$group['order_amount'] = round((float) $group['order_amount'], 2);
$group['registration_compare_rate'] = self::relativeChange(
(float) $group['registration_count'],
(float) $group['compare_registration_count']
);
$group['appointment_compare_rate'] = self::relativeChange(
(float) $group['appointment_count'],
(float) $group['compare_appointment_count']
@@ -487,15 +432,11 @@ class FirstVisitRegistrationStatsLogic
/** @param array<int,array<string,mixed>> $members @return array<string,mixed> */
private static function buildSummary(array $members, array $range): array
{
$registrationCount = 0;
$compareRegistrationCount = 0;
$appointmentCount = 0;
$compareAppointmentCount = 0;
$orderCount = 0;
$orderAmount = 0.0;
foreach ($members as $member) {
$registrationCount += (int) ($member['registration_count'] ?? 0);
$compareRegistrationCount += (int) ($member['compare_registration_count'] ?? 0);
$appointmentCount += (int) ($member['appointment_count'] ?? 0);
$compareAppointmentCount += (int) ($member['compare_appointment_count'] ?? 0);
$orderCount += (int) ($member['order_count'] ?? 0);
@@ -503,9 +444,6 @@ class FirstVisitRegistrationStatsLogic
}
return [
'registration_count' => $registrationCount,
'registration_compare_count' => $compareRegistrationCount,
'registration_compare_rate' => self::relativeChange($registrationCount, $compareRegistrationCount),
'appointment_count' => $appointmentCount,
'appointment_compare_count' => $compareAppointmentCount,
'appointment_compare_rate' => self::relativeChange($appointmentCount, $compareAppointmentCount),
@@ -522,18 +460,13 @@ class FirstVisitRegistrationStatsLogic
usort($rows, static fn (array $a, array $b): int => (($b[$field] ?? 0) <=> ($a[$field] ?? 0)) ?: strcmp((string) $a['name'], (string) $b['name']));
$out = [];
foreach (array_slice($rows, 0, $limit) as $row) {
$countField = match ($field) {
'order_amount' => 'order_count',
'registration_count' => 'registration_count',
default => 'appointment_count',
};
$out[] = [
'admin_id' => (int) ($row['admin_id'] ?? 0),
'name' => (string) ($row['name'] ?? ''),
'value' => $field === 'order_amount'
? round((float) ($row[$field] ?? 0), 2)
: (int) ($row[$field] ?? 0),
'count' => (int) ($row[$countField] ?? 0),
'count' => $field === 'order_amount' ? (int) ($row['order_count'] ?? 0) : (int) ($row['appointment_count'] ?? 0),
];
}
@@ -42,8 +42,6 @@ class ConversionLogic
$includeFilters = (int)($params['include_filters'] ?? 0) === 1;
// 仅供需要“有效挂号”口径的内部看板调用;默认保持转换统计历史口径不变。
$excludeCancelledAppointments = (int)($params['exclude_cancelled_appointments'] ?? 0) === 1;
// 一诊综合转化复用处方订单页的业绩口径;其它调用方继续保留历史“双审完成单”口径。
$usePerformanceOrderMetrics = strtolower(trim((string)($params['order_metric_mode'] ?? ''))) === 'performance';
$dimension = self::normalizeDimension((string)($params['dimension'] ?? 'dept'));
$mediaChannelCode = MediaChannelService::normalizeStatsCode((string) ($params['media_channel_code'] ?? ''));
$mediaChannel = $mediaChannelCode !== '' ? MediaChannelService::getChannelByCode($mediaChannelCode) : null;
@@ -156,20 +154,9 @@ class ConversionLogic
$endDate,
$mediaChannel,
$visibleAdminIds,
$excludeCancelledAppointments,
$usePerformanceOrderMetrics
);
self::hydrateOrderAndAmountStats(
$entities,
$dimension,
$entityIds,
$adminToDeptIds,
$startTimestamp,
$endTimestamp,
$mediaChannel,
$visibleAdminIds,
$usePerformanceOrderMetrics
$excludeCancelledAppointments
);
self::hydrateOrderAndAmountStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
// 数据隔离:可见部门 = 可见 admin 所属部门并集;用于 account_cost 与下游 cost 分摊。
$visibleDeptIds = self::resolveVisibleDeptIds($visibleAdminIds);
[$globalAccountCost, $accountCostDeptIds] = self::hydrateAccountCostStats($entities, $startDate, $endDate, $mediaChannelCode, $visibleDeptIds);
@@ -252,9 +239,7 @@ class ConversionLogic
$adminToDeptIds,
$validDeptIds,
$globalAccountCost,
$visibleAdminIds,
$excludeCancelledAppointments,
$usePerformanceOrderMetrics
$visibleAdminIds
);
$pagedRows = self::attachDeptMembers($pagedRows, $memberRowsByDeptId);
}
@@ -728,8 +713,8 @@ class ConversionLogic
/**
* @return array<int, int[]>
*
* 返回 admin_id => [dept_id, ...]。跨部门时优先最深层、再按部门排序,
* 与一诊挂号统计和业绩看板的人员归属规则保持一致
* 返回 admin_id => [dept_id, ...],每个 admin 的 dept_id 列表按 dept_id 升序排列
* zyt_admin_dept 联合主键 admin_id+dept_id,无独立 id 列)
* 当 admin 跨部门时,下游 mapEntityIds 只取列表中第一个落在当前 entityIds 内的部门,
* 避免同一笔加粉/挂号/接诊被多次累加到不同部门。
*/
@@ -737,38 +722,10 @@ class ConversionLogic
{
$rows = Db::name('admin_dept')
->field('admin_id, dept_id')
->order('admin_id', 'asc')
->order('dept_id', 'asc')
->select()
->toArray();
$deptRows = Db::name('dept')
->whereNull('delete_time')
->field('id, pid, sort')
->select()
->toArray();
$deptById = [];
foreach ($deptRows as $deptRow) {
$deptId = (int)($deptRow['id'] ?? 0);
if ($deptId > 0) {
$deptById[$deptId] = [
'pid' => (int)($deptRow['pid'] ?? 0),
'sort' => (int)($deptRow['sort'] ?? 0),
];
}
}
$depthCache = [];
$depthOf = static function (int $deptId) use (&$depthOf, &$depthCache, $deptById): int {
if ($deptId <= 0 || !isset($deptById[$deptId])) {
return 0;
}
if (isset($depthCache[$deptId])) {
return $depthCache[$deptId];
}
$parentId = (int)($deptById[$deptId]['pid'] ?? 0);
if ($parentId <= 0 || $parentId === $deptId || !isset($deptById[$parentId])) {
return $depthCache[$deptId] = 0;
}
return $depthCache[$deptId] = $depthOf($parentId) + 1;
};
$map = [];
foreach ($rows as $row) {
$adminId = (int)($row['admin_id'] ?? 0);
@@ -779,21 +736,6 @@ class ConversionLogic
$map[$adminId] ??= [];
$map[$adminId][] = $deptId;
}
foreach ($map as &$deptIds) {
usort($deptIds, static function (int $left, int $right) use ($depthOf, $deptById): int {
$depthCompare = $depthOf($right) <=> $depthOf($left);
if ($depthCompare !== 0) {
return $depthCompare;
}
$sortCompare = (int)($deptById[$right]['sort'] ?? 0) <=> (int)($deptById[$left]['sort'] ?? 0);
if ($sortCompare !== 0) {
return $sortCompare;
}
return $left <=> $right;
});
}
unset($deptIds);
return $map;
}
@@ -986,12 +928,9 @@ class ConversionLogic
string $endDate,
?array $mediaChannel,
?array $visibleAdminIds = null,
bool $excludeCancelledAppointments = false,
bool $useRegistrationMetric = false
bool $excludeCancelledAppointments = false
): void {
$sourceExpr = $dimension === 'doctor'
? 'a.doctor_id'
: 'COALESCE(NULLIF(a.assistant_id, 0), NULLIF(u.assistant_id, 0))';
$sourceExpr = $dimension === 'doctor' ? 'a.doctor_id' : 'u.assistant_id';
$query = Db::name('doctor_appointment')
->alias('a')
->leftJoin('tcm_diagnosis u', 'a.patient_id = u.id')
@@ -1001,7 +940,7 @@ class ConversionLogic
->group("{$sourceExpr}, a.patient_id");
if ($excludeCancelledAppointments) {
$query->whereIn('a.status', [1, 3, 4]);
$query->where('a.status', '<>', 2);
}
$query->where(static function (Query $subQuery): void {
@@ -1044,17 +983,7 @@ class ConversionLogic
}
}
self::hydratePaidAppointmentStats(
$entities,
$dimension,
$entityIds,
$adminToDeptIds,
$startTimestamp,
$endTimestamp,
$mediaChannel,
$visibleAdminIds,
$useRegistrationMetric
);
self::hydratePaidAppointmentStats($entities, $dimension, $entityIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
foreach ($entities as &$entity) {
$appointmentTotalCount = (int)($entity['appointment_total_count'] ?? 0);
@@ -1079,8 +1008,7 @@ class ConversionLogic
int $startTimestamp,
int $endTimestamp,
?array $mediaChannel,
?array $visibleAdminIds = null,
bool $useRegistrationMetric = false
?array $visibleAdminIds = null
): void {
$startDateTime = date('Y-m-d H:i:s', $startTimestamp);
$endDateTime = date('Y-m-d H:i:s', $endTimestamp);
@@ -1088,20 +1016,14 @@ class ConversionLogic
->alias('o')
->whereNull('o.delete_time')
->where('o.status', 2)
->where('o.order_type', 1)
->where('o.amount', 5)
->whereNotNull('o.payment_time')
->where('o.payment_time', '<>', '')
->whereBetweenTime('o.payment_time', $startDateTime, $endDateTime)
->fieldRaw('o.creator_id AS source_admin_id, COUNT(*) AS paid_appointment_count')
->group('o.creator_id');
if ($useRegistrationMetric) {
// 一诊页面的新“挂号”:已支付且 0 < 实收金额 < 10 元,每笔支付订单计 1 个。
$query->where('o.amount', '>', 0)->where('o.amount', '<', 10);
} else {
// 保留旧统计页面的历史“付费挂号”字段,避免本次一诊改造改变其它模块口径。
$query->where('o.order_type', 1)->where('o.amount', 5);
}
if ($mediaChannel !== null) {
$query->leftJoin('qywx_external_contact q', 'q.external_userid = o.payer_external_userid');
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
@@ -1137,59 +1059,8 @@ class ConversionLogic
int $startTimestamp,
int $endTimestamp,
?array $mediaChannel,
?array $visibleAdminIds = null,
bool $usePerformanceOrderMetrics = false
?array $visibleAdminIds = null
): void {
if ($usePerformanceOrderMetrics) {
$sourceExpr = $dimension === 'doctor' ? 'rx.creator_id' : 'po.creator_id';
$query = Db::name('tcm_prescription_order')
->alias('po')
->leftJoin('tcm_prescription rx', 'rx.id = po.prescription_id AND rx.delete_time IS NULL')
->leftJoin('tcm_diagnosis dg', 'dg.id = po.diagnosis_id AND dg.delete_time IS NULL')
->whereNull('po.delete_time')
->where('po.create_time', 'between', [$startTimestamp, $endTimestamp]);
YejiStatsLogic::applyPrescriptionOrderEffectiveAmountQuery($query, 'po');
$query
->fieldRaw("{$sourceExpr} AS source_admin_id, COUNT(*) AS order_count, SUM(po.amount) AS total_amount")
->group($sourceExpr);
if ($mediaChannel !== null) {
$query
->leftJoin('order o', 'o.id = po.linked_pay_order_id')
->leftJoin('qywx_external_contact q', 'q.external_userid = o.payer_external_userid');
MediaChannelService::applyFollowUsersChannelFilter($query, 'q.follow_users', $mediaChannel);
}
foreach ($query->select()->toArray() as $row) {
$sourceAdminId = (int)($row['source_admin_id'] ?? 0);
$mappedEntityIds = self::mapEntityIds(
$dimension,
$sourceAdminId,
$entityIds,
$adminToDeptIds,
$visibleAdminIds
);
if ($mappedEntityIds === []) {
continue;
}
$orderCount = (int)($row['order_count'] ?? 0);
$amount = round((float)($row['total_amount'] ?? 0), 2);
foreach ($mappedEntityIds as $entityId) {
$entities[$entityId]['completed_order_count'] += $orderCount;
$entities[$entityId]['completed_order_amount'] = round(
(float)$entities[$entityId]['completed_order_amount'] + $amount,
2
);
$entities[$entityId]['business_order_amount'] = round(
(float)$entities[$entityId]['business_order_amount'] + $amount,
2
);
}
}
return;
}
$completedSourceExpr = $dimension === 'doctor' ? 'rx.creator_id' : 'rx.assistant_id';
$completedQuery = Db::name('tcm_prescription_order')
->alias('po')
@@ -1704,8 +1575,6 @@ class ConversionLogic
* @param float $globalAccountCost 由调用方提前计算好的本期总账户消耗(zyt_account_cost SUM)。
* -1 表示让本函数自行 hydrate;>= 0 时直接复用,避免重复 SQL。
* @param int[]|null $visibleAdminIds 可见 admin 集合(null = SCOPE_ALL);用于成员明细的隔离
* @param bool $excludeCancelledAppointments 是否排除已取消挂号,须与部门汇总口径一致
* @param bool $usePerformanceOrderMetrics 是否使用有效业绩订单口径,须与部门汇总口径一致
* @return array<int, array<int, array<string, mixed>>> dept_id => [member_row, ...]
*/
private static function buildMemberRowsByDept(
@@ -1720,9 +1589,7 @@ class ConversionLogic
array $adminToDeptIds,
array $validDeptIds = [],
float $globalAccountCost = -1.0,
?array $visibleAdminIds = null,
bool $excludeCancelledAppointments = false,
bool $usePerformanceOrderMetrics = false
?array $visibleAdminIds = null
): array
{
$assistantEntities = self::loadAdminEntities(2, 0, $visibleAdminIds);
@@ -1731,15 +1598,15 @@ class ConversionLogic
$assistantIds = array_keys($assistantEntities);
$doctorIds = array_keys($doctorEntities);
if ($assistantIds !== [] && !$usePerformanceOrderMetrics) {
if ($assistantIds !== []) {
self::hydrateFanStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
self::hydrateAppointmentStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds, $excludeCancelledAppointments);
self::hydrateOrderAndAmountStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds, $usePerformanceOrderMetrics);
self::hydrateAppointmentStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds);
self::hydrateOrderAndAmountStats($assistantEntities, 'assistant', $assistantIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
}
if ($doctorIds !== [] && !$usePerformanceOrderMetrics) {
if ($doctorIds !== []) {
self::hydrateFanStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
self::hydrateAppointmentStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds, $excludeCancelledAppointments);
self::hydrateOrderAndAmountStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds, $usePerformanceOrderMetrics);
self::hydrateAppointmentStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds);
self::hydrateOrderAndAmountStats($doctorEntities, 'doctor', $doctorIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
}
// 复用调用方传入的 global account cost;只有兜底未传时才回查一次(保留向后兼容)。
@@ -1774,15 +1641,6 @@ class ConversionLogic
}
}
if ($usePerformanceOrderMetrics && $combined !== []) {
// 一诊综合转化的部门指标均按业务归属人统计。成员明细也必须沿用同一归属,
// 不能再分别按“医助/医生”统计后相加,否则双角色员工会重复、人员合计也无法与部门汇总对齐。
$combinedIds = array_keys($combined);
self::hydrateFanStats($combined, 'assistant', $combinedIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds);
self::hydrateAppointmentStats($combined, 'assistant', $combinedIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $startDate, $endDate, $mediaChannel, $visibleAdminIds, $excludeCancelledAppointments, true);
self::hydrateOrderAndAmountStats($combined, 'assistant', $combinedIds, $adminToDeptIds, $startTimestamp, $endTimestamp, $mediaChannel, $visibleAdminIds, true);
}
if ($combined === []) {
return [];
}
@@ -18,7 +18,7 @@ use think\facade\Db;
* 数据口径:
* - 所有“业绩/接诊诊单”与业绩统计、业务订单列表保持一致:按业务订单创建时间,
* 排除履约已取消/拒收/退款(4/9/10),金额取业务订单 amount,归属人取订单 creator_id。
* - 今日预约、面诊沿用 ConversionLogic挂号按已支付且实收低于 10 元的订单统计
* - 今日加粉、挂号、面诊和转化率继续沿用 ConversionLogic它们不是业绩指标
* - 趋势使用同一业绩条件的轻量按日 SQL,固定补齐最近 7 个自然日。
* - 所有查询都使用 DataScopeService 返回的可见管理员集合收窄。
*/
@@ -29,7 +29,7 @@ class PerformanceDashboardLogic
/**
* @return array<string, mixed>
*/
public static function overview(int $adminId, array $adminInfo, array $params = []): array
public static function overview(int $adminId, array $adminInfo): array
{
$today = date('Y-m-d');
$yesterday = date('Y-m-d', strtotime('-1 day'));
@@ -48,15 +48,9 @@ class PerformanceDashboardLogic
/** @var array<int>|null $visibleAdminIds */
$visibleAdminIds = $scope['_visible_admin_ids'];
unset($scope['_visible_admin_ids']);
$rankingDeptId = self::resolveRankingDeptId(
max(0, (int) ($params['ranking_dept_id'] ?? 0)),
$adminId,
$adminInfo
);
$orderDaily = self::loadPerformanceOrderDaily($previousMonthStart, $today, $visibleAdminIds);
$personalOrderDaily = self::loadPerformanceOrderDaily($monthStart, $today, [$adminId]);
$registrationDaily = self::loadRegistrationDaily($trendStart, $today, $visibleAdminIds);
$monthAmount = self::sumDailyMetric($orderDaily, $monthStart, $today, 'amount');
$previousMonthAmount = self::sumDailyMetric(
@@ -74,7 +68,6 @@ class PerformanceDashboardLogic
'time_type' => 'today',
'include_members' => 0,
'include_filters' => 0,
'exclude_cancelled_appointments' => 1,
'page_no' => 1,
'page_size' => 100,
], $adminId, $adminInfo);
@@ -84,7 +77,6 @@ class PerformanceDashboardLogic
'time_type' => 'yesterday',
'include_members' => 0,
'include_filters' => 0,
'exclude_cancelled_appointments' => 1,
'page_no' => 1,
'page_size' => 100,
], $adminId, $adminInfo);
@@ -97,13 +89,7 @@ class PerformanceDashboardLogic
'start_date' => $today,
'end_date' => $today,
], $adminId, $adminInfo);
$appointmentRanking = self::buildRegistrationRanking(
$adminId,
$adminInfo,
$scope,
$visibleAdminIds,
$rankingDeptId
);
$appointmentRanking = self::buildAppointmentRanking($adminId, $adminInfo, $scope);
$performanceRanking = self::buildPerformanceRanking(
is_array($todayPerformanceOverview['rows'] ?? null) ? $todayPerformanceOverview['rows'] : []
);
@@ -111,14 +97,7 @@ class PerformanceDashboardLogic
'start_date' => $trendStart,
'end_date' => $today,
], $adminId, $adminInfo);
$trend = self::buildTrend(
$trendStart,
$today,
$visibleAdminIds,
$orderDaily,
$registrationDaily,
$trendContext
);
$trend = self::buildTrend($trendStart, $today, $visibleAdminIds, $orderDaily, $trendContext);
$todayTrendIndex = max(0, count($trend['dates'] ?? []) - 1);
$yesterdayTrendIndex = max(0, $todayTrendIndex - 1);
@@ -132,15 +111,10 @@ class PerformanceDashboardLogic
$yesterdayOrderCount = (int) self::dailyMetric($orderDaily, $yesterday, 'count');
$todayOrderAmount = self::dailyMetric($orderDaily, $today, 'amount');
$yesterdayOrderAmount = self::dailyMetric($orderDaily, $yesterday, 'amount');
$todayLowAmountPaymentCount = (int) self::dailyMetric($registrationDaily, $today, 'count');
$yesterdayLowAmountPaymentCount = (int) self::dailyMetric($registrationDaily, $yesterday, 'count');
$todayPaidAppointmentCount = $todayLowAmountPaymentCount;
$yesterdayPaidAppointmentCount = $yesterdayLowAmountPaymentCount;
$todayPaidAppointmentRate = self::percent($todayPaidAppointmentCount, $todayAddFansCount);
$yesterdayPaidAppointmentRate = self::percent($yesterdayPaidAppointmentCount, $yesterdayAddFansCount);
// 接诊卡片使用的是有效业务订单,接诊率必须使用同一订单口径,不能继续读取旧的双审完成单。
$todayInterviewReceiveRate = self::percent($todayOrderCount, $todayInterviewCount);
$yesterdayInterviewReceiveRate = self::percent($yesterdayOrderCount, $yesterdayInterviewCount);
$todayPaidAppointmentRate = round((float) ($todaySummary['paid_appointment_rate'] ?? 0), 2);
$yesterdayPaidAppointmentRate = round((float) ($yesterdaySummary['paid_appointment_rate'] ?? 0), 2);
$todayInterviewReceiveRate = round((float) ($todaySummary['interview_receive_rate'] ?? 0), 2);
$yesterdayInterviewReceiveRate = round((float) ($yesterdaySummary['interview_receive_rate'] ?? 0), 2);
$target = self::buildTargetProgress(
$adminId,
@@ -156,9 +130,6 @@ class PerformanceDashboardLogic
'month_amount' => round($monthAmount, 2),
'month_compare_rate' => self::relativeChange($monthAmount, $previousMonthAmount),
'month_compare_label' => '较上月同期',
'today_amount' => round($todayOrderAmount, 2),
'today_compare_rate' => self::relativeChange($todayOrderAmount, $yesterdayOrderAmount),
'today_compare_label' => '较昨日',
'yesterday_amount' => round($yesterdayAmount, 2),
'yesterday_compare_rate' => self::relativeChange($yesterdayAmount, $dayBeforeAmount),
'yesterday_compare_label' => '较前一日',
@@ -167,12 +138,10 @@ class PerformanceDashboardLogic
'today' => [
'add_fans_count' => $todayAddFansCount,
'appointment_total_count' => $todayAppointmentCount,
'low_amount_payment_count' => $todayLowAmountPaymentCount,
'interview_count' => $todayInterviewCount,
// 保留原响应字段名以兼容已发布前端,数值含义已统一为“计入业绩的业务订单”。
'completed_order_count' => $todayOrderCount,
'completed_order_amount' => $todayOrderAmount,
'paid_appointment_count' => $todayPaidAppointmentCount,
'paid_appointment_rate' => $todayPaidAppointmentRate,
'interview_receive_rate' => $todayInterviewReceiveRate,
'comparisons' => [
@@ -181,10 +150,6 @@ class PerformanceDashboardLogic
$todayAppointmentCount,
$yesterdayAppointmentCount
),
'low_amount_payment_count' => self::buildComparison(
$todayLowAmountPaymentCount,
$yesterdayLowAmountPaymentCount
),
'interview_count' => self::buildComparison($todayInterviewCount, $yesterdayInterviewCount),
'completed_order_count' => self::buildComparison($todayOrderCount, $yesterdayOrderCount),
'completed_order_amount' => self::buildComparison($todayOrderAmount, $yesterdayOrderAmount),
@@ -206,17 +171,13 @@ class PerformanceDashboardLogic
'items' => $performanceRanking,
],
],
'filters' => [
'ranking_departments' => self::rankingDepartmentOptions($adminId, $adminInfo),
'ranking_dept_id' => $rankingDeptId,
],
'trend' => $trend,
'target' => $target,
'meta' => [
'generated_at' => date('Y-m-d H:i:s'),
'timezone' => date_default_timezone_get(),
'commission_note' => '本人业绩按当前账号创建的业务订单统计,排除已取消、拒收和退款订单。',
'rate_note' => '挂号及挂号率:按支付时间统计已支付且 0<实收金额<10 元的订单,每笔订单计 1 个挂号,并按订单创建人归属;预约按预约日期统计有效预约记录。接诊率:有效业务诊单数 / 已完成面诊数。',
'rate_note' => '近 7 天趋势与业绩统计一致:挂号排除已取消记录,进线仅统计当前范围内可归属业绩中心的新增客户事件,诊单按订单创建时间统计并排除履约 4/9/10。',
],
];
}
@@ -280,30 +241,6 @@ class PerformanceDashboardLogic
];
}
private static function resolveRankingDeptId(int $requestedDeptId, int $adminId, array $adminInfo): int
{
if ($requestedDeptId <= 0) {
return 0;
}
$exists = Dept::where('id', $requestedDeptId)->whereNull('delete_time')->count() > 0;
if (!$exists) {
return 0;
}
$allowedDeptSet = DataScopeService::getAllowedDeptIdSet($adminId, $adminInfo);
if ($allowedDeptSet !== null && !isset($allowedDeptSet[$requestedDeptId])) {
return 0;
}
return $requestedDeptId;
}
/** @return array<int, array<string, mixed>> */
private static function rankingDepartmentOptions(int $adminId, array $adminInfo): array
{
return DeptLogic::getAllDataScoped($adminId, $adminInfo);
}
/**
* @param array<int>|null $visibleAdminIds
* @return array<string, array{amount: float, count: int}>
@@ -349,53 +286,6 @@ class PerformanceDashboardLogic
return $out;
}
/**
* 0 < 实收金额 < 10 元的已支付订单笔数;退款订单状态为 4,不会进入统计。
*
* @param array<int>|null $visibleAdminIds
* @return array<string, array{count:int}>
*/
private static function loadRegistrationDaily(
string $startDate,
string $endDate,
?array $visibleAdminIds
): array {
if ($visibleAdminIds === []) {
return [];
}
$query = Db::name('order')
->whereNull('delete_time')
->where('status', 2)
->where('amount', '>', 0)
->where('amount', '<', 10)
->whereNotNull('payment_time')
->where('payment_time', '<>', '')
->whereBetweenTime(
'payment_time',
$startDate . ' 00:00:00',
$endDate . ' 23:59:59'
);
if ($visibleAdminIds !== null) {
$query->whereIn('creator_id', $visibleAdminIds);
}
$rows = $query
->fieldRaw("DATE(payment_time) AS date_label, COUNT(*) AS item_count")
->group('date_label')
->select()
->toArray();
$out = [];
foreach ($rows as $row) {
$date = (string) ($row['date_label'] ?? '');
if ($date !== '') {
$out[$date] = ['count' => (int) ($row['item_count'] ?? 0)];
}
}
return $out;
}
/**
* @param array<string, array{amount: float, count: int}> $daily
*/
@@ -420,15 +310,6 @@ class PerformanceDashboardLogic
return round((float) ($daily[$date][$metric] ?? 0), 2);
}
private static function percent(float $numerator, int $denominator): float
{
if ($denominator <= 0) {
return 0.0;
}
return round(($numerator / $denominator) * 100, 2);
}
private static function relativeChange(float $current, float $previous): ?float
{
if (abs($previous) < 0.00001) {
@@ -459,20 +340,41 @@ class PerformanceDashboardLogic
* @param array<string, mixed> $scope
* @return array<string, mixed>
*/
private static function buildRegistrationRanking(
int $adminId,
array $adminInfo,
array $scope,
?array $baseVisibleAdminIds,
int $rankingDeptId
): array
private static function buildAppointmentRanking(int $adminId, array $adminInfo, array $scope): array
{
$roleIds = array_map('intval', $scope['role_ids'] ?? []);
$isDoctorSelf = ($scope['key'] ?? '') === 'self'
&& in_array(1, $roleIds, true)
&& !in_array(2, $roleIds, true);
$rankingVisibleAdminIds = $baseVisibleAdminIds;
if ($isDoctorSelf) {
$doctorStats = DoctorDailyStatsLogic::overview([
'start_date' => date('Y-m-d'),
'end_date' => date('Y-m-d'),
], $adminId, $adminInfo);
$items = [];
foreach (array_slice($doctorStats['rows'] ?? [], 0, 5) as $row) {
$items[] = [
'id' => (int) ($row['admin_id'] ?? 0),
'name' => (string) ($row['doctor_name'] ?? ''),
// DoctorDailyStats 的 total 含已取消;驾驶舱实时排行只统计有效挂号。
'count' => max(
0,
(int) ($row['appointment_total'] ?? 0) - (int) ($row['appointment_cancelled'] ?? 0)
),
'amount' => round((float) ($row['deal_amount'] ?? 0), 2),
];
}
return [
'title' => '实时挂号排行',
'kind' => 'doctor',
'scope_label' => (string) ($scope['label'] ?? ''),
'items' => $items,
];
}
$rankingVisibleAdminIds = null;
$rankingScopeLabel = (string) ($scope['label'] ?? '');
if (
(int) ($scope['scope_value'] ?? DataScopeService::SCOPE_SELF) === DataScopeService::SCOPE_SELF
@@ -484,72 +386,48 @@ class PerformanceDashboardLogic
$rankingScopeLabel = '本人所属部门';
}
}
if ($rankingDeptId > 0) {
$deptAdminIds = self::departmentAdminIds($rankingDeptId);
$rankingVisibleAdminIds = $rankingVisibleAdminIds === null
? $deptAdminIds
: array_values(array_intersect($rankingVisibleAdminIds, $deptAdminIds));
$rankingScopeLabel = (string) (Dept::where('id', $rankingDeptId)->value('name') ?? $rankingScopeLabel);
}
$assistantStats = ConversionLogic::overview([
'dimension' => 'assistant',
'time_type' => 'today',
'include_filters' => 0,
'exclude_cancelled_appointments' => 1,
'page_no' => 1,
'page_size' => $rankingVisibleAdminIds !== null ? max(1, count($rankingVisibleAdminIds)) : 100,
], $adminId, $adminInfo, $rankingVisibleAdminIds);
$rows = is_array($assistantStats['lists'] ?? null) ? $assistantStats['lists'] : [];
usort($rows, static function (array $a, array $b): int {
$byAppointment = (int) ($b['appointment_total_count'] ?? 0) <=> (int) ($a['appointment_total_count'] ?? 0);
if ($byAppointment !== 0) {
return $byAppointment;
}
$byAmount = (float) ($b['completed_order_amount'] ?? 0) <=> (float) ($a['completed_order_amount'] ?? 0);
if ($byAmount !== 0) {
return $byAmount;
}
return (int) ($a['id'] ?? 0) <=> (int) ($b['id'] ?? 0);
});
$items = [];
if ($rankingVisibleAdminIds !== []) {
$query = Db::name('order')
->alias('o')
->join('admin a', 'a.id = o.creator_id AND a.delete_time IS NULL', 'INNER')
->whereNull('o.delete_time')
->where('o.status', 2)
->where('o.amount', '>', 0)
->where('o.amount', '<', 10)
->whereNotNull('o.payment_time')
->where('o.payment_time', '<>', '')
->whereBetweenTime('o.payment_time', date('Y-m-d 00:00:00'), date('Y-m-d 23:59:59'));
if ($rankingVisibleAdminIds !== null) {
$query->whereIn('o.creator_id', $rankingVisibleAdminIds);
}
$rows = $query
->fieldRaw('o.creator_id AS id, a.name, COUNT(*) AS item_count, SUM(o.amount) AS amount_sum')
->group(['o.creator_id', 'a.name'])
->orderRaw('item_count DESC, amount_sum DESC, o.creator_id ASC')
->limit(5)
->select()
->toArray();
foreach ($rows as $row) {
$items[] = [
'id' => (int) ($row['id'] ?? 0),
'name' => (string) ($row['name'] ?? ''),
'count' => (int) ($row['item_count'] ?? 0),
'amount' => round((float) ($row['amount_sum'] ?? 0), 2),
];
}
foreach (array_slice($rows, 0, 5) as $row) {
$items[] = [
'id' => (int) ($row['id'] ?? 0),
'name' => (string) ($row['name'] ?? ''),
'count' => (int) ($row['appointment_total_count'] ?? 0),
'amount' => round((float) ($row['completed_order_amount'] ?? 0), 2),
];
}
return [
'title' => '实时挂号排行',
'kind' => $isDoctorSelf ? 'doctor' : 'member',
'kind' => 'assistant',
'scope_label' => $rankingScopeLabel,
'items' => $items,
];
}
/** @return int[] */
private static function departmentAdminIds(int $deptId): array
{
$deptIds = array_values(array_unique(array_filter(
array_map('intval', DeptLogic::getSelfAndDescendantIds($deptId)),
static fn (int $id): bool => $id > 0
)));
if ($deptIds === []) {
return [];
}
return array_values(array_unique(array_filter(
array_map('intval', AdminDept::whereIn('dept_id', $deptIds)->column('admin_id')),
static fn (int $id): bool => $id > 0
)));
}
/**
* SELF 医助排行的卡片级例外:只扩展到当前账号所有有效直接部门内的有效医助。
* 不展开子部门,也不改变驾驶舱其它指标的数据范围。
@@ -634,7 +512,6 @@ class PerformanceDashboardLogic
/**
* @param array<int>|null $visibleAdminIds
* @param array<string, array{amount: float, count: int}> $orderDaily
* @param array<string, array{count: int}> $registrationDaily
* @param array<string, mixed> $trendContext
* @return array<string, mixed>
*/
@@ -643,7 +520,6 @@ class PerformanceDashboardLogic
string $endDate,
?array $visibleAdminIds,
array $orderDaily,
array $registrationDaily,
array $trendContext
): array {
$adminToPrimary = is_array($trendContext['adminToPrimary'] ?? null)
@@ -661,7 +537,6 @@ class PerformanceDashboardLogic
$tableRowDeptIds
);
$dates = [];
$registrations = [];
$appointments = [];
$leads = [];
$orders = [];
@@ -671,7 +546,6 @@ class PerformanceDashboardLogic
while ($cursor <= $end) {
$date = date('Y-m-d', $cursor);
$dates[] = date('m-d', $cursor);
$registrations[] = (int) ($registrationDaily[$date]['count'] ?? 0);
$appointments[] = (int) ($appointmentDaily[$date] ?? 0);
$leads[] = (int) ($leadDaily[$date] ?? 0);
$orders[] = (int) ($orderDaily[$date]['count'] ?? 0);
@@ -681,7 +555,6 @@ class PerformanceDashboardLogic
return [
'date_range' => [$startDate, $endDate],
'dates' => $dates,
'registrations' => $registrations,
'appointments' => $appointments,
'leads' => $leads,
'orders' => $orders,
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import t from"./error-WAKv02FK.js";import{o,q as r,r as a,v as n,D as c,s}from"./.pnpm-CLkClvFH.js";import"./index-DmTxYTP_.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 +0,0 @@
import e from"./error-WAKv02FK.js";import{o,q as r,r as t,v as s}from"./.pnpm-CLkClvFH.js";import"./index-DmTxYTP_.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 +0,0 @@
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-CLkClvFH.js";import{a as V}from"./doctor-D1AXimsd.js";import{m as A,_ as M}from"./index-DmTxYTP_.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 +0,0 @@
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-CLkClvFH.js";import{aa as V}from"./tcm-BVCJBN1B.js";import{_ as q}from"./index-DmTxYTP_.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 +0,0 @@
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-CLkClvFH.js";import{ab as Y}from"./tcm-BVCJBN1B.js";import{_ as q}from"./index-DmTxYTP_.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};
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
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-CLkClvFH.js";import j from"./RecordingPlaybackBlock-BfAX4_lk.js";import{U as x}from"./index-CSsUnauB.js";import{i as c,_ as q}from"./index-DmTxYTP_.js";import{af as K,ag as k,ah as A}from"./tcm-BVCJBN1B.js";import"./RecordingVideoPlayer-yypL1bOe.js";import"./file-B5w338Ax.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 +0,0 @@
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-CLkClvFH.js";import{ai as q}from"./tcm-BVCJBN1B.js";import{_ as H}from"./index-DmTxYTP_.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};
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
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{_ as o}from"./GancaoSubmissionReconcileButton.vue_vue_type_script_setup_true_lang-3g5h4UAQ.js";import"./.pnpm-CLkClvFH.js";import"./tcm-BVCJBN1B.js";import"./index-DmTxYTP_.js";export{o as default};
@@ -1 +0,0 @@
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-CLkClvFH.js";import{p as j}from"./tcm-BVCJBN1B.js";import{i as C}from"./index-DmTxYTP_.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 +0,0 @@
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-CLkClvFH.js";import{d as te}from"./dayjs-B1oVjCBe.js";import{an as ne,ao as oe}from"./tcm-BVCJBN1B.js";import{p as re}from"./im-business-message-parse-CBCIOBjN.js";import{_ as le}from"./index-DmTxYTP_.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 @@
import{_ as m}from"./MediaSourceSelect.vue_vue_type_script_setup_true_lang-CGoTjXza.js";import"./.pnpm-CLkClvFH.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 @@
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-CLkClvFH.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 +0,0 @@
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-CLkClvFH.js";import{t as j,_ as J}from"./index-DmTxYTP_.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,2 +0,0 @@
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-CLkClvFH.js";import{_ as fe}from"./picker-w-v2x1vC.js";import{e as ve,c as _e,i as f,_ as ye}from"./index-DmTxYTP_.js";import{a as T,d as he}from"./patient-BBfhTSSv.js";import{h as ke}from"./perm-ae2VAcFY.js";import"./index-bg1q5oVL.js";import"./index-Ca40CkGy.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./index-kJqjciNr.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";import"./usePaging-22V4hi3A.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};
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
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
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-CLkClvFH.js";import{_ as V}from"./index-DmTxYTP_.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};
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 @@
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-CLkClvFH.js";import H from"./RecordingVideoPlayer-yypL1bOe.js";import{e as I,_ as P}from"./index-DmTxYTP_.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};
@@ -1,2 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/.pnpm-CLkClvFH.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-CLkClvFH.js";import{e as ae,_ as ne}from"./index-DmTxYTP_.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-CLkClvFH.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};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
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-CLkClvFH.js";import{a0 as L}from"./tcm-BVCJBN1B.js";import{i as M,_ as S}from"./index-DmTxYTP_.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)])}}}),Q=S(O,[["__scopeId","data-v-82b635bd"]]);export{Q as default};
@@ -1 +0,0 @@
import{_ as o}from"./account-adjust.vue_vue_type_script_setup_true_lang-CTQv-CwI.js";import"./.pnpm-CLkClvFH.js";import"./index-bg1q5oVL.js";import"./index-DmTxYTP_.js";export{o as default};
@@ -1 +0,0 @@
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-CLkClvFH.js";import{_ as L}from"./index-bg1q5oVL.js";import{i as V}from"./index-DmTxYTP_.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 _};
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 @@
import{_ as o}from"./add-nav.vue_vue_type_script_setup_true_lang-C_TJu0Lx.js";import"./.pnpm-CLkClvFH.js";import"./index-kJqjciNr.js";import"./index-DmTxYTP_.js";import"./picker-D_EZ5kJQ.js";import"./index-bg1q5oVL.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./article-mIdeSctd.js";import"./usePaging-22V4hi3A.js";import"./picker-w-v2x1vC.js";import"./index-Ca40CkGy.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";export{o as default};
@@ -1 +0,0 @@
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-CLkClvFH.js";import{_ as q}from"./index-kJqjciNr.js";import{_ as F}from"./picker-D_EZ5kJQ.js";import{_ as K}from"./picker-w-v2x1vC.js";import{c as O,i as r}from"./index-DmTxYTP_.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 +0,0 @@
import{r as n}from"./index-DmTxYTP_.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};
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{r as e}from"./index-DmTxYTP_.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 +0,0 @@
import{r as e}from"./index-DmTxYTP_.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 m}from"./attr.vue_vue_type_script_setup_true_lang-BO6dS5u1.js";import"./.pnpm-CLkClvFH.js";export{m 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};
@@ -1 +0,0 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-sNjZKNJk.js";import"./.pnpm-CLkClvFH.js";import"./add-nav.vue_vue_type_script_setup_true_lang-C_TJu0Lx.js";import"./index-kJqjciNr.js";import"./index-DmTxYTP_.js";import"./picker-D_EZ5kJQ.js";import"./index-bg1q5oVL.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./article-mIdeSctd.js";import"./usePaging-22V4hi3A.js";import"./picker-w-v2x1vC.js";import"./index-Ca40CkGy.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";export{o as default};
@@ -1 +0,0 @@
import{_ as m}from"./attr.vue_vue_type_script_setup_true_lang-sI_zd9pI.js";import"./.pnpm-CLkClvFH.js";export{m as default};
@@ -1 +0,0 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-D3eqTS0W.js";import"./.pnpm-CLkClvFH.js";import"./add-nav.vue_vue_type_script_setup_true_lang-C_TJu0Lx.js";import"./index-kJqjciNr.js";import"./index-DmTxYTP_.js";import"./picker-D_EZ5kJQ.js";import"./index-bg1q5oVL.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./article-mIdeSctd.js";import"./usePaging-22V4hi3A.js";import"./picker-w-v2x1vC.js";import"./index-Ca40CkGy.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";export{o as default};
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-Dz8jLbwU.js";import"./.pnpm-CLkClvFH.js";import"./index-kJqjciNr.js";import"./index-DmTxYTP_.js";import"./picker-D_EZ5kJQ.js";import"./index-bg1q5oVL.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./article-mIdeSctd.js";import"./usePaging-22V4hi3A.js";import"./picker-w-v2x1vC.js";import"./index-Ca40CkGy.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";export{o as default};
@@ -1 +0,0 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-DHmH4Ys4.js";import"./.pnpm-CLkClvFH.js";import"./index-kJqjciNr.js";import"./index-DmTxYTP_.js";import"./picker-D_EZ5kJQ.js";import"./index-bg1q5oVL.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./article-mIdeSctd.js";import"./usePaging-22V4hi3A.js";import"./picker-w-v2x1vC.js";import"./index-Ca40CkGy.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";export{o as default};
@@ -1 +0,0 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-SDo-A_BV.js";import"./.pnpm-CLkClvFH.js";import"./picker-w-v2x1vC.js";import"./index-bg1q5oVL.js";import"./index-DmTxYTP_.js";import"./index-Ca40CkGy.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./index-kJqjciNr.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";import"./usePaging-22V4hi3A.js";export{o as default};
@@ -1 +0,0 @@
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-CLkClvFH.js";import{_ as p}from"./menu-set.vue_vue_type_script_setup_true_lang-CVd0yse_.js";import"./index-kJqjciNr.js";import"./index-DmTxYTP_.js";import"./picker-D_EZ5kJQ.js";import"./index-bg1q5oVL.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./article-mIdeSctd.js";import"./usePaging-22V4hi3A.js";import"./picker-w-v2x1vC.js";import"./index-Ca40CkGy.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.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 +0,0 @@
import{_ as m}from"./attr.vue_vue_type_script_setup_true_lang-BvRCPK8R.js";import"./.pnpm-CLkClvFH.js";export{m as default};
@@ -1 +0,0 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-C_RLVms0.js";import"./.pnpm-CLkClvFH.js";import"./index.vue_vue_type_script_setup_true_lang-Dc-97eB3.js";import"./picker-w-v2x1vC.js";import"./index-bg1q5oVL.js";import"./index-DmTxYTP_.js";import"./index-Ca40CkGy.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./index-kJqjciNr.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";import"./usePaging-22V4hi3A.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};
@@ -1 +0,0 @@
import{_ as o}from"./attr.vue_vue_type_script_setup_true_lang-CY_UI04O.js";import"./.pnpm-CLkClvFH.js";import"./index-kJqjciNr.js";import"./index-DmTxYTP_.js";import"./picker-D_EZ5kJQ.js";import"./index-bg1q5oVL.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./article-mIdeSctd.js";import"./usePaging-22V4hi3A.js";import"./picker-w-v2x1vC.js";import"./index-Ca40CkGy.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";import"./index.vue_vue_type_script_setup_true_lang-Dc-97eB3.js";export{o 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};
@@ -1 +0,0 @@
import{_ as o}from"./attr-setting.vue_vue_type_script_setup_true_lang-BZTk2WrL.js";import"./.pnpm-CLkClvFH.js";import"./index-1T1XNa0L.js";import"./attr-CUCnbx7X.js";import"./index-kJqjciNr.js";import"./index-DmTxYTP_.js";import"./picker-D_EZ5kJQ.js";import"./index-bg1q5oVL.js";import"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import"./article-mIdeSctd.js";import"./usePaging-22V4hi3A.js";import"./picker-w-v2x1vC.js";import"./index-Ca40CkGy.js";import"./index-CSsUnauB.js";import"./file-B5w338Ax.js";import"./index.vue_vue_type_script_setup_true_lang-BqzZSXmO.js";import"./content.vue_vue_type_script_setup_true_lang-B5fRd4t1.js";import"./decoration-img-BgerRSRz.js";import"./attr.vue_vue_type_script_setup_true_lang-SDo-A_BV.js";import"./content-DicodFyP.js";import"./attr.vue_vue_type_script_setup_true_lang-Dz8jLbwU.js";import"./content.vue_vue_type_script_setup_true_lang-CDjoy9GR.js";import"./attr.vue_vue_type_script_setup_true_lang-sNjZKNJk.js";import"./add-nav.vue_vue_type_script_setup_true_lang-C_TJu0Lx.js";import"./content-CCx6zhs5.js";import"./attr.vue_vue_type_script_setup_true_lang-D3eqTS0W.js";import"./content.vue_vue_type_script_setup_true_lang-CC05PMJH.js";import"./attr.vue_vue_type_script_setup_true_lang-sI_zd9pI.js";import"./content-B5RJ0Ef5.js";import"./decoration-BR9foBj9.js";import"./attr.vue_vue_type_script_setup_true_lang-C_RLVms0.js";import"./index.vue_vue_type_script_setup_true_lang-Dc-97eB3.js";import"./content-DG4t_H8J.js";import"./content.vue_vue_type_script_setup_true_lang-D74KJmhK.js";import"./attr.vue_vue_type_script_setup_true_lang-BvRCPK8R.js";import"./content-BzuBN8-y.js";import"./attr.vue_vue_type_script_setup_true_lang-DHmH4Ys4.js";import"./content.vue_vue_type_script_setup_true_lang-C-773KKA.js";import"./attr.vue_vue_type_script_setup_true_lang-BO6dS5u1.js";import"./content-PGCIisrb.js";export{o as default};
@@ -1 +0,0 @@
import{o as g,q as a,r as b,v as c,bg as y,D as r,s as x,T as _,a2 as h,O as i,a3 as w,a4 as v,u as C}from"./.pnpm-CLkClvFH.js";import{e as k}from"./index-1T1XNa0L.js";const B={class:"pages-setting"},D={class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"},O=g({__name:"attr-setting",props:{widget:{type:Object,default:()=>({})},type:{type:String,default:"mobile"}},emits:["update:content"],setup(e,{emit:d}){const m=d,p=n=>{m("update:content",n)};return(n,E)=>{const f=y,u=h;return a(),b("div",B,[c(f,{shadow:"never",class:"!border-none flex"},{default:r(()=>{var t;return[x("div",D,_((t=e.widget)==null?void 0:t.title),1)]}),_:1}),c(u,{class:"w-full",style:{height:"calc(100% - 60px)"}},{default:r(()=>{var t,s,o,l;return[(a(),i(w,null,[(a(),i(v((s=C(k)[(t=e.widget)==null?void 0:t.name])==null?void 0:s.attr),{content:(o=e.widget)==null?void 0:o.content,styles:(l=e.widget)==null?void 0:l.styles,type:e.type,"onUpdate:content":p},null,40,["content","styles","type"]))],1024))]}),_:1})])}}});export{O as _};
@@ -1 +0,0 @@
import{o as j,q as V,r as v,v as e,D as t,s,L as h,bg as q,b6 as A,u as m,bQ as K,ae as M,b7 as O,I as Q,K as R,T as w,P as G,b9 as H,F as J,p as y}from"./.pnpm-CLkClvFH.js";import{_ as W}from"./index-kJqjciNr.js";import{_ as X}from"./picker-D_EZ5kJQ.js";import{_ as Y}from"./picker-w-v2x1vC.js";import{c as Z,i as b}from"./index-DmTxYTP_.js";import{_ as ee}from"./index.vue_vue_type_script_setup_true_lang-Dc-97eB3.js";const le={class:"mb-[18px] max-w-[400px]"},oe={class:"bg-fill-light w-full p-4 mt-4"},te={class:"upload-btn w-[60px] h-[60px]"},se={class:"upload-btn w-[60px] h-[60px]"},ae={class:"flex-1 flex items-center"},ne={class:"drag-move cursor-move ml-auto"},de={key:0,class:"mt-4"},c=5,p=2,_e=j({__name:"attr",props:{modelValue:{type:Object,default:()=>({list:[],style:{}})}},emits:["update:modelValue"],setup(k,{emit:U}){const C=k,E=U,n=y({get(){return C.modelValue},set(a){E("update:modelValue",a)}}),$=y(()=>{var a;return((a=n.value.list)==null?void 0:a.filter(l=>l.is_show=="1"))||[]}),z=()=>{var a;((a=n.value.list)==null?void 0:a.length)<c?n.value.list.push({name:"",selected:"",unselected:"",is_show:1,link:{}}):b.msgError(`最多添加${c}`)},B=a=>{var l;if(((l=n.value.list)==null?void 0:l.length)<=p)return b.msgError(`最少保留${p}`);n.value.list.splice(a,1)},D=a=>a.relatedContext.index!=0,F=a=>{if($.value.length<p)return a.is_show=1,b.msgError(`最少显示${p}`)};return(a,l)=>{const _=q,x=ee,i=A,f=Z,g=Y,N=O,I=X,P=Q,S=W,T=R,L=H;return V(),v(J,null,[e(_,{shadow:"never",class:"!border-none flex"},{default:t(()=>[...l[3]||(l[3]=[s("div",{class:"title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"},[h(" 底部导航设置 "),s("span",{class:"form-tips ml-[10px] !mt-0"}," 至少添加2个导航,最多添加5个导航 ")],-1)])]),_:1}),e(L,{"label-width":"70px"},{default:t(()=>[e(_,{shadow:"never",class:"!border-none flex mt-2"},{default:t(()=>[l[4]||(l[4]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"展示样式")],-1)),e(i,{label:"默认颜色"},{default:t(()=>[e(x,{class:"max-w-[400px]",modelValue:m(n).style.default_color,"onUpdate:modelValue":l[0]||(l[0]=u=>m(n).style.default_color=u),"default-color":"#999999"},null,8,["modelValue"])]),_:1}),e(i,{label:"选中颜色",style:{"margin-bottom":"0"}},{default:t(()=>[e(x,{class:"max-w-[400px]",modelValue:m(n).style.selected_color,"onUpdate:modelValue":l[1]||(l[1]=u=>m(n).style.selected_color=u),"default-color":"#4173ff"},null,8,["modelValue"])]),_:1})]),_:1}),e(_,{shadow:"never",class:"!border-none flex mt-2"},{default:t(()=>{var u;return[l[7]||(l[7]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"菜单设置"),s("div",{class:"text-xs text-tx-secondary ml-2"},"建议图片尺寸:100px*100px")],-1)),s("div",le,[e(m(K),{class:"draggable",modelValue:m(n).list,"onUpdate:modelValue":l[2]||(l[2]=o=>m(n).list=o),animation:"300",draggable:".draggable",handle:".drag-move",move:D,"item-key":"index"},{item:t(({element:o,index:r})=>[e(S,{onClose:d=>B(r),class:M(["max-w-[400px]",{draggable:r!=0}]),"show-close":r!==0},{default:t(()=>[s("div",oe,[e(i,{label:"导航图标"},{default:t(()=>[e(g,{modelValue:o.unselected,"onUpdate:modelValue":d=>o.unselected=d,"upload-class":"bg-body","exclude-domain":"",size:"60px"},{upload:t(()=>[s("div",te,[e(f,{name:"el-icon-Plus",size:16}),l[5]||(l[5]=s("span",{class:"text-xs leading-5"}," 未选中 ",-1))])]),_:1},8,["modelValue","onUpdate:modelValue"]),e(g,{modelValue:o.selected,"onUpdate:modelValue":d=>o.selected=d,"exclude-domain":"","upload-class":"bg-body",size:"60px"},{upload:t(()=>[s("div",se,[e(f,{name:"el-icon-Plus",size:16}),l[6]||(l[6]=s("span",{class:"text-xs leading-5"}," 选中 ",-1))])]),_:1},8,["modelValue","onUpdate:modelValue"])]),_:2},1024),e(i,{label:"导航名称"},{default:t(()=>[e(N,{modelValue:o.name,"onUpdate:modelValue":d=>o.name=d,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),_:2},1024),e(i,{label:"链接地址"},{default:t(()=>[e(I,{"is-tab":!0,disabled:r===0,modelValue:o.link,"onUpdate:modelValue":d=>o.link=d},null,8,["disabled","modelValue","onUpdate:modelValue"])]),_:2},1024),e(i,{label:"是否显示"},{default:t(()=>[s("div",ae,[e(P,{disabled:r==0,modelValue:o.is_show,"onUpdate:modelValue":d=>o.is_show=d,"active-value":1,"inactive-value":0,onChange:d=>F(o)},null,8,["disabled","modelValue","onUpdate:modelValue","onChange"]),s("div",ne,[e(f,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])]),_:2},1032,["onClose","show-close","class"])]),_:1},8,["modelValue"])]),((u=m(n).list)==null?void 0:u.length)<c?(V(),v("div",de,[e(T,{class:"w-full",type:"primary",onClick:z},{default:t(()=>{var o;return[h(" 添加导航 "+w((o=m(n).list)==null?void 0:o.length)+" / "+w(c),1)]}),_:1})])):G("",!0)]}),_:1})]),_:1})],64)}}});export{_e as _};
@@ -1 +0,0 @@
import{o as E,q as s,O as r,D as t,v as l,bg as F,b6 as C,bc as N,u as a,bf as z,L as p,b7 as B,P as i,s as b,b9 as O,p as j}from"./.pnpm-CLkClvFH.js";import{_ as D}from"./index.vue_vue_type_script_setup_true_lang-Dc-97eB3.js";import{_ as I}from"./picker-w-v2x1vC.js";const G=E({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(d,{emit:g}){const y=g,x=d,o=j({get:()=>x.content,set:f=>{y("update:content",f)}});return(f,e)=>{const m=z,_=N,u=C,v=B,V=I,k=D,U=F,w=O;return s(),r(w,{ref:"form","label-width":"80px",size:"large"},{default:t(()=>[l(U,{shadow:"never",class:"!border-none flex mt-2"},{default:t(()=>[l(u,{label:"页面标题"},{default:t(()=>[l(_,{modelValue:a(o).title_type,"onUpdate:modelValue":e[0]||(e[0]=n=>a(o).title_type=n)},{default:t(()=>[l(m,{value:"1"},{default:t(()=>[...e[7]||(e[7]=[p("文字",-1)])]),_:1}),l(m,{value:"2"},{default:t(()=>[...e[8]||(e[8]=[p("图片",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),d.content.title_type==1?(s(),r(u,{key:0},{default:t(()=>[l(v,{modelValue:a(o).title,"onUpdate:modelValue":e[1]||(e[1]=n=>a(o).title=n),maxlength:"8","show-word-limit":"",class:"w-[300px]",placeholder:"请输入页面标题"},null,8,["modelValue"])]),_:1})):i("",!0),d.content.title_type==2?(s(),r(u,{key:1},{default:t(()=>[l(V,{modelValue:a(o).title_img,"onUpdate:modelValue":e[2]||(e[2]=n=>a(o).title_img=n),limit:1,size:"100px"},null,8,["modelValue"]),e[9]||(e[9]=b("div",{class:"form-tips"},"建议图片尺寸:300px*40px",-1))]),_:1})):i("",!0),d.content.title_type==1?(s(),r(u,{key:2,label:"文字颜色"},{default:t(()=>[l(_,{modelValue:a(o).text_color,"onUpdate:modelValue":e[3]||(e[3]=n=>a(o).text_color=n)},{default:t(()=>[l(m,{value:"1"},{default:t(()=>[...e[10]||(e[10]=[p("白色",-1)])]),_:1}),l(m,{value:"2"},{default:t(()=>[...e[11]||(e[11]=[p("黑色",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1})):i("",!0),l(u,{label:"页面背景"},{default:t(()=>[l(_,{modelValue:a(o).bg_type,"onUpdate:modelValue":e[4]||(e[4]=n=>a(o).bg_type=n)},{default:t(()=>[l(m,{value:"1"},{default:t(()=>[...e[12]||(e[12]=[p("背景颜色",-1)])]),_:1}),l(m,{value:"2"},{default:t(()=>[...e[13]||(e[13]=[p("背景图片",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),d.content.bg_type==1?(s(),r(u,{key:3},{default:t(()=>[l(k,{modelValue:a(o).bg_color,"onUpdate:modelValue":e[5]||(e[5]=n=>a(o).bg_color=n),"reset-color":"#F5F5F5"},null,8,["modelValue"])]),_:1})):i("",!0),d.content.bg_type==2?(s(),r(u,{key:4},{default:t(()=>[l(V,{modelValue:a(o).bg_image,"onUpdate:modelValue":e[6]||(e[6]=n=>a(o).bg_image=n),limit:1,size:"100px"},null,8,["modelValue"]),e[14]||(e[14]=b("div",{class:"form-tips"},"建议图片尺寸:750px*高度不限",-1))]),_:1})):i("",!0)]),_:1})]),_:1},512)}}});export{G as _};
@@ -1 +1 @@
import{o as e,q as t,r as o}from"./.pnpm-CLkClvFH.js";const p=e({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},setup(n){return(r,a)=>(t(),o("div"))}});export{p as _};
import{o as e,q as t,r as o}from"./.pnpm-BadRMC3e.js";const p=e({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},setup(n){return(r,a)=>(t(),o("div"))}});export{p as _};
@@ -1 +0,0 @@
import{o as k,q as d,r as u,v as l,D as o,bg as F,s,bc as U,u as n,bf as B,L as x,b6 as C,bm as N,F as b,G as c,bn as O,b9 as j,p as D}from"./.pnpm-CLkClvFH.js";import{_ as G}from"./add-nav.vue_vue_type_script_setup_true_lang-C_TJu0Lx.js";const L={class:"flex-1 mt-4"},I=k({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(v,{emit:V}){const y=V,w=v,a=D({get:()=>w.content,set:m=>{y("update:content",m)}});return(m,e)=>{const r=B,E=U,p=O,_=N,i=C,f=F,g=j;return d(),u("div",null,[l(g,{"label-width":"70px"},{default:o(()=>[l(f,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[6]||(e[6]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"展示样式")],-1)),l(E,{modelValue:n(a).style,"onUpdate:modelValue":e[0]||(e[0]=t=>n(a).style=t)},{default:o(()=>[l(r,{value:1},{default:o(()=>[...e[4]||(e[4]=[x("固定显示",-1)])]),_:1}),l(r,{value:2},{default:o(()=>[...e[5]||(e[5]=[x("分页滑动",-1)])]),_:1})]),_:1},8,["modelValue"]),l(i,{label:"每行数量",class:"mt-4"},{default:o(()=>[l(_,{modelValue:n(a).per_line,"onUpdate:modelValue":e[1]||(e[1]=t=>n(a).per_line=t),style:{width:"300px"}},{default:o(()=>[(d(),u(b,null,c(5,t=>l(p,{key:t,label:t+"个",value:t},null,8,["label","value"])),64))]),_:1},8,["modelValue"])]),_:1}),l(i,{label:"显示行数"},{default:o(()=>[l(_,{modelValue:n(a).show_line,"onUpdate:modelValue":e[2]||(e[2]=t=>n(a).show_line=t),style:{width:"300px"}},{default:o(()=>[(d(),u(b,null,c(2,t=>l(p,{key:t,label:t+"行",value:t},null,8,["label","value"])),64))]),_:1},8,["modelValue"])]),_:1})]),_:1}),l(f,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[7]||(e[7]=s("div",{class:"flex items-end"},[s("div",{class:"text-base text-[#101010] font-medium"},"菜单设置"),s("div",{class:"text-xs text-tx-secondary ml-2"},"建议图片尺寸:100px*100px")],-1)),s("div",L,[l(G,{modelValue:n(a).data,"onUpdate:modelValue":e[3]||(e[3]=t=>n(a).data=t)},null,8,["modelValue"])])]),_:1})]),_:1})])}}});export{I as _};
@@ -1 +0,0 @@
import{o as I,q as i,r as g,v as t,D as l,bg as O,s,u,bQ as j,O as F,b6 as q,b7 as z,I as A,K,L,P,b9 as Q,p as R,cm as b}from"./.pnpm-CLkClvFH.js";import{_ as S}from"./index-kJqjciNr.js";import{c as T,i as v}from"./index-DmTxYTP_.js";import{_ as G}from"./picker-D_EZ5kJQ.js";import{_ as H}from"./picker-w-v2x1vC.js";const J={class:"bg-fill-light flex items-center w-full p-4 mt-4"},M={class:"ml-3 flex-1"},W={class:"flex-1 flex items-center"},X={class:"drag-move cursor-move ml-auto"},Y={key:0,class:"mt-4"},p=5,le=I({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(r,{emit:h}){const m=h,c=r,f=R({get:()=>c.content,set:a=>{m("update:content",a)}}),k=()=>{var a;if(((a=c.content.data)==null?void 0:a.length)<p){const e=b(c.content);e.data.push({is_show:"1",image:"",name:"",link:{}}),m("update:content",e)}else v.msgError(`最多添加${p}张图片`)},w=a=>{var d;if(((d=c.content.data)==null?void 0:d.length)<=1)return v.msgError("最少保留一张图片");const e=b(c.content);e.data.splice(a,1),m("update:content",e)};return(a,e)=>{const d=H,y=z,_=q,E=G,U=A,C=T,B=S,D=K,N=O,$=Q;return i(),g("div",null,[t($,{"label-width":"70px"},{default:l(()=>[t(N,{shadow:"never",class:"!border-none flex mt-2"},{default:l(()=>{var x;return[e[2]||(e[2]=s("div",{class:"flex items-end mb-4"},[s("div",{class:"text-base text-[#101010] font-medium"},"菜单"),s("div",{class:"text-xs text-tx-secondary ml-2"}," 最多添加5张,建议图片尺寸:750px*200px ")],-1)),t(u(j),{class:"draggable",modelValue:u(f).data,"onUpdate:modelValue":e[0]||(e[0]=o=>u(f).data=o),animation:"300",handle:".drag-move","item-key":"index"},{item:l(({element:o,index:V})=>[(i(),F(B,{key:V,onClose:n=>w(V),class:"w-[467px]"},{default:l(()=>[s("div",J,[t(d,{modelValue:o.image,"onUpdate:modelValue":n=>o.image=n,"upload-class":"bg-body","exclude-domain":""},null,8,["modelValue","onUpdate:modelValue"]),s("div",M,[t(_,{label:"图片名称"},{default:l(()=>[t(y,{modelValue:o.name,"onUpdate:modelValue":n=>o.name=n,placeholder:"请输入名称"},null,8,["modelValue","onUpdate:modelValue"])]),_:2},1024),t(_,{class:"mt-[18px]",label:"图片链接"},{default:l(()=>[t(E,{modelValue:o.link,"onUpdate:modelValue":n=>o.link=n},null,8,["modelValue","onUpdate:modelValue"])]),_:2},1024),t(_,{label:"是否显示",class:"mt-[18px]"},{default:l(()=>[s("div",W,[t(U,{modelValue:o.is_show,"onUpdate:modelValue":n=>o.is_show=n,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),s("div",X,[t(C,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"]),((x=r.content.data)==null?void 0:x.length)<p?(i(),g("div",Y,[t(D,{class:"w-full",type:"primary",onClick:k},{default:l(()=>[...e[1]||(e[1]=[L("添加图片",-1)])]),_:1})])):P("",!0)]}),_:1})]),_:1})])}}});export{le as _};
@@ -1 +1 @@
import{o as e,q as t,r as o}from"./.pnpm-CLkClvFH.js";const p=e({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},setup(n){return(r,a)=>(t(),o("div"))}});export{p as _};
import{o as e,q as t,r as o}from"./.pnpm-BadRMC3e.js";const p=e({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},setup(n){return(r,a)=>(t(),o("div"))}});export{p as _};
@@ -1 +0,0 @@
import{o as j,q as d,r as b,v as o,D as s,bg as F,s as n,u as p,bQ as S,O as _,b6 as q,P as r,b7 as z,I as A,K,L,b9 as P,p as Q,cm as v}from"./.pnpm-CLkClvFH.js";import{_ as R}from"./index-kJqjciNr.js";import{c as T,i as k}from"./index-DmTxYTP_.js";import{_ as G}from"./picker-D_EZ5kJQ.js";import{_ as H}from"./picker-w-v2x1vC.js";const J={class:"flex-1"},M={class:"bg-fill-light w-full p-4 mt-4"},W={class:"flex-1"},X={class:"flex-1 flex items-center"},Y={class:"drag-move cursor-move ml-auto"},Z={key:0,class:"mt-4"},f=5,ae=j({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})},type:{type:String,default:"mobile"}},emits:["update:content"],setup(m,{emit:y}){const u=y,c=m,g=Q({get:()=>c.content,set:a=>{u("update:content",a)}}),w=()=>{var a;if(((a=c.content.data)==null?void 0:a.length)<f){const e=v(c.content);e.data.push({is_show:"1",image:"",name:"",link:{}}),u("update:content",e)}else k.msgError(`最多添加${f}张图片`)},E=a=>{var i;if(((i=c.content.data)==null?void 0:i.length)<=1)return k.msgError("最少保留一张图片");const e=v(c.content);e.data.splice(a,1),u("update:content",e)};return(a,e)=>{const i=H,U=G,C=z,h=q,B=A,D=T,N=R,$=K,I=F,O=P;return d(),b("div",null,[o(O,{"label-width":"70px"},{default:s(()=>[o(I,{shadow:"never",class:"!border-none flex mt-2"},{default:s(()=>{var x;return[e[2]||(e[2]=n("div",{class:"flex items-end"},[n("div",{class:"text-base text-[#101010] font-medium"},"图片设置"),n("div",{class:"text-xs text-tx-secondary ml-2"}," 最多添加5张,建议图片尺寸:750px*200px ")],-1)),n("div",J,[o(p(S),{class:"draggable",modelValue:p(g).data,"onUpdate:modelValue":e[0]||(e[0]=t=>p(g).data=t),animation:"300",handle:".drag-move"},{item:s(({element:t,index:V})=>[(d(),_(N,{key:V,onClose:l=>E(V),class:"w-full"},{default:s(()=>[n("div",M,[o(i,{width:"396px",height:"196px",modelValue:t.image,"onUpdate:modelValue":l=>t.image=l,"upload-class":"bg-body","exclude-domain":""},null,8,["modelValue","onUpdate:modelValue"]),n("div",W,[o(h,{class:"mt-[18px]",label:"图片链接"},{default:s(()=>[m.type=="mobile"?(d(),_(U,{key:0,modelValue:t.link,"onUpdate:modelValue":l=>t.link=l},null,8,["modelValue","onUpdate:modelValue"])):r("",!0),m.type=="pc"?(d(),_(C,{key:1,placeholder:"请输入链接",modelValue:t.link.path,"onUpdate:modelValue":l=>t.link.path=l},null,8,["modelValue","onUpdate:modelValue"])):r("",!0)]),_:2},1024),o(h,{label:"是否显示",class:"mt-[18px] !mb-0"},{default:s(()=>[n("div",X,[o(B,{modelValue:t.is_show,"onUpdate:modelValue":l=>t.is_show=l,"active-value":"1","inactive-value":"0"},null,8,["modelValue","onUpdate:modelValue"]),n("div",Y,[o(D,{name:"el-icon-Rank",size:"18"})])])]),_:2},1024)])])]),_:2},1032,["onClose"]))]),_:1},8,["modelValue"])]),((x=m.content.data)==null?void 0:x.length)<f?(d(),b("div",Z,[o($,{class:"w-full",type:"primary",onClick:w},{default:s(()=>[...e[1]||(e[1]=[L("添加图片",-1)])]),_:1})])):r("",!0)]}),_:1})]),_:1})])}}});export{ae as _};
@@ -1 +0,0 @@
import{o as V,q as b,r as w,v as e,D as n,bg as x,b6 as g,b7 as k,u as o,s as E,b9 as U,p as v}from"./.pnpm-CLkClvFH.js";import{_ as q}from"./picker-w-v2x1vC.js";const C=V({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(u,{emit:p}){const r=p,i=u,l=v({get:()=>i.content,set:d=>{r("update:content",d)}});return(d,t)=>{const s=k,m=g,_=q,c=x,f=U;return b(),w("div",null,[e(f,{"label-width":"90px",size:"large","label-position":"top"},{default:n(()=>[e(c,{shadow:"never",class:"!border-none flex mt-2"},{default:n(()=>[e(m,{label:"平台名称"},{default:n(()=>[e(s,{class:"w-[400px]","show-word-limit":"",maxlength:"20",modelValue:o(l).title,"onUpdate:modelValue":t[0]||(t[0]=a=>o(l).title=a)},null,8,["modelValue"])]),_:1}),e(m,{label:"客服二维码"},{default:n(()=>[E("div",null,[e(_,{modelValue:o(l).qrcode,"onUpdate:modelValue":t[1]||(t[1]=a=>o(l).qrcode=a),"exclude-domain":""},null,8,["modelValue"])])]),_:1}),e(m,{label:"备注"},{default:n(()=>[e(s,{class:"w-[400px]","show-word-limit":"",maxlength:"20",modelValue:o(l).remark,"onUpdate:modelValue":t[2]||(t[2]=a=>o(l).remark=a)},null,8,["modelValue"])]),_:1}),e(m,{label:"联系电话"},{default:n(()=>[e(s,{class:"w-[400px]",modelValue:o(l).mobile,"onUpdate:modelValue":t[3]||(t[3]=a=>o(l).mobile=a)},null,8,["modelValue"])]),_:1}),e(m,{label:"服务时间"},{default:n(()=>[e(s,{class:"w-[400px]","show-word-limit":"",maxlength:"20",modelValue:o(l).time,"onUpdate:modelValue":t[4]||(t[4]=a=>o(l).time=a)},null,8,["modelValue"])]),_:1})]),_:1})]),_:1})])}}});export{C as _};
@@ -1 +1 @@
import{o as e,q as t,r as o}from"./.pnpm-CLkClvFH.js";const p=e({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},setup(n){return(r,a)=>(t(),o("div"))}});export{p as _};
import{o as e,q as t,r as o}from"./.pnpm-BadRMC3e.js";const p=e({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},setup(n){return(r,a)=>(t(),o("div"))}});export{p as _};
@@ -1 +0,0 @@
import{o as V,q as E,r as w,v as t,D as o,bg as y,b6 as g,b7 as B,u as s,s as a,bc as C,bf as N,L as u,b9 as U,p as j}from"./.pnpm-CLkClvFH.js";import{_ as k}from"./add-nav.vue_vue_type_script_setup_true_lang-C_TJu0Lx.js";const D={class:"flex-1"},O=V({__name:"attr",props:{content:{type:Object,default:()=>({})},styles:{type:Object,default:()=>({})}},emits:["update:content"],setup(p,{emit:i}){const f=i,_=p,l=j({get:()=>_.content,set:m=>{f("update:content",m)}});return(m,e)=>{const x=B,c=g,d=y,r=N,b=C,v=U;return E(),w("div",null,[t(v,{"label-width":"70px"},{default:o(()=>[t(d,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[t(c,{label:"标题"},{default:o(()=>[t(x,{class:"w-[396px]",modelValue:s(l).title,"onUpdate:modelValue":e[0]||(e[0]=n=>s(l).title=n)},null,8,["modelValue"])]),_:1})]),_:1}),t(d,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[5]||(e[5]=a("div",{class:"flex items-end mb-4"},[a("div",{class:"text-base text-[#101010] font-medium"},"展示样式")],-1)),t(b,{modelValue:s(l).style,"onUpdate:modelValue":e[1]||(e[1]=n=>s(l).style=n)},{default:o(()=>[t(r,{value:1},{default:o(()=>[...e[3]||(e[3]=[u("横排",-1)])]),_:1}),t(r,{value:2},{default:o(()=>[...e[4]||(e[4]=[u("竖排",-1)])]),_:1})]),_:1},8,["modelValue"])]),_:1}),t(d,{shadow:"never",class:"!border-none flex mt-2"},{default:o(()=>[e[6]||(e[6]=a("div",{class:"flex items-end mb-4"},[a("div",{class:"text-base text-[#101010] font-medium"},"菜单"),a("div",{class:"text-xs text-tx-secondary ml-2"},"建议图片尺寸:100px*100px")],-1)),a("div",D,[t(k,{modelValue:s(l).data,"onUpdate:modelValue":e[2]||(e[2]=n=>s(l).data=n)},null,8,["modelValue"])])]),_:1})]),_:1})])}}});export{O as _};
@@ -1 +0,0 @@
import{_ as o}from"./auth.vue_vue_type_script_setup_true_lang-BMViaqZY.js";import"./.pnpm-CLkClvFH.js";import"./menu-BLc1mXjX.js";import"./index-DmTxYTP_.js";import"./role-D_erpi4-.js";import"./index-bg1q5oVL.js";export{o as default};
@@ -1 +0,0 @@
import{o as O,q as k,r as U,v as s,D as d,ac as H,O as I,b9 as j,u as c,a2 as z,b6 as G,s as y,bd as J,C as P,bU as Q,bi as W,a8 as f,M as r,ba as X,n as b}from"./.pnpm-CLkClvFH.js";import{m as Y}from"./menu-BLc1mXjX.js";import{a as Z}from"./role-D_erpi4-.js";import{_ as $}from"./index-bg1q5oVL.js";import{x as ee}from"./index-DmTxYTP_.js";const te={class:"edit-popup"},re=O({__name:"auth",emits:["success","close"],setup(ae,{expose:x,emit:C}){const _=C,l=f(),h=f(),u=f(),g=r(!1),i=r(!0),m=r(!1),v=r([]),p=r([]),o=X({id:"",name:"",desc:"",sort:0,data_scope:1,menu_id:[]}),E={name:[{required:!0,message:"请输入名称",trigger:["blur"]}]},w=()=>{m.value=!0,Y().then(e=>{p.value=e,v.value=ee(e),b(()=>{A()}),m.value=!1})},R=()=>{var a,n;const e=(a=l.value)==null?void 0:a.getCheckedKeys(),t=(n=l.value)==null?void 0:n.getHalfCheckedKeys();return e==null||e.unshift.apply(e,t),e},A=()=>{o.menu_id.forEach(e=>{b(()=>{var t;(t=l.value)==null||t.setChecked(e,!0,!1)})})},D=e=>{const t=p.value;for(let a=0;a<t.length;a++)l.value.store.nodesMap[t[a].id].expanded=e},K=e=>{var t,a;e?(t=l.value)==null||t.setCheckedKeys(v.value.map(n=>n.id)):(a=l.value)==null||a.setCheckedKeys([])},V=async()=>{var e,t;await((e=h.value)==null?void 0:e.validate()),o.menu_id=R(),await Z(o),(t=u.value)==null||t.close(),_("success")},B=()=>{_("close")},S=()=>{var e;(e=u.value)==null||e.open()},T=async e=>{for(const t in o)e[t]!=null&&e[t]!=null&&(o[t]=e[t])};return w(),x({open:S,setFormData:T}),(e,t)=>{const a=J,n=Q,F=G,q=z,L=j,M=W;return k(),U("div",te,[s($,{ref_key:"popupRef",ref:u,title:"分配权限",async:!0,width:"550px",onConfirm:V,onClose:B},{default:d(()=>[H((k(),I(L,{class:"ls-form",ref_key:"formRef",ref:h,rules:E,model:c(o),"label-width":"60px"},{default:d(()=>[s(q,{class:"h-[400px] sm:h-[600px]"},{default:d(()=>[s(F,{label:"权限",prop:"menu_id"},{default:d(()=>[y("div",null,[s(a,{label:"展开/折叠",onChange:D}),s(a,{label:"全选/不全选",onChange:K}),s(a,{modelValue:c(i),"onUpdate:modelValue":t[0]||(t[0]=N=>P(i)?i.value=N:null),label:"父子联动"},null,8,["modelValue"]),y("div",null,[s(n,{ref_key:"treeRef",ref:l,data:c(p),props:{label:"name",children:"children"},"check-strictly":!c(i),"node-key":"id","default-expand-all":c(g),"show-checkbox":""},null,8,["data","check-strictly","default-expand-all"])])])]),_:1})]),_:1})]),_:1},8,["model"])),[[M,c(m)]])]),_:1},512)])}}});export{re as _};
@@ -1 +0,0 @@
import{o as y,q as m,r as h,v as e,D as l,bh as K,b9 as P,u as a,b6 as N,b7 as O,b8 as j,bm as q,bn as z,F as I,G as R,O as w,K as S,L as _,bg as $,ac as A,bj as G,bk as H,s as p,T as v,ae as J,bi as M,C as Q,ba as W}from"./.pnpm-CLkClvFH.js";import{_ as X}from"./index.vue_vue_type_script_setup_true_lang-CkTCzcR4.js";import{h as Y}from"./index-DmTxYTP_.js";import{_ as Z}from"./index.vue_vue_type_script_setup_true_lang-AkFBBq5G.js";import{g as ee,h as te}from"./finance-BhM-JZZM.js";import{u as ae}from"./useDictOptions-luad54lw.js";import{u as ne}from"./usePaging-22V4hi3A.js";const le={class:"flex items-center"},oe={class:"flex justify-end mt-4"},se=y({name:"balanceDetail"}),be=y({...se,setup(ie){const o=W({user_info:"",change_type:"",start_time:"",end_time:""}),{pager:i,getLists:d,resetPage:c,resetParams:C}=ne({fetchFun:ee,params:o}),{optionsData:x}=ae({change_type:{api:te}});return d(),(re,n)=>{const V=K,E=O,r=N,u=z,T=q,k=Z,f=S,D=P,b=$,s=H,U=Y,B=G,F=X,L=M;return m(),h("div",null,[e(b,{class:"!border-none",shadow:"never"},{default:l(()=>[e(V,{type:"warning",title:"温馨提示:用户账户变动记录",closable:!1,"show-icon":""}),e(D,{ref:"formRef",class:"mb-[-16px] mt-[16px]",model:a(o),inline:!0},{default:l(()=>[e(r,{class:"w-[280px]",label:"用户信息"},{default:l(()=>[e(E,{modelValue:a(o).user_info,"onUpdate:modelValue":n[0]||(n[0]=t=>a(o).user_info=t),placeholder:"请输入用户账号/昵称/手机号",clearable:"",onKeyup:j(a(c),["enter"])},null,8,["modelValue","onKeyup"])]),_:1}),e(r,{class:"w-[280px]",label:"变动类型"},{default:l(()=>[e(T,{modelValue:a(o).change_type,"onUpdate:modelValue":n[1]||(n[1]=t=>a(o).change_type=t)},{default:l(()=>[e(u,{label:"全部",value:""}),(m(!0),h(I,null,R(a(x).change_type,(t,g)=>(m(),w(u,{key:g,label:t,value:g},null,8,["label","value"]))),128))]),_:1},8,["modelValue"])]),_:1}),e(r,{label:"记录时间"},{default:l(()=>[e(k,{startTime:a(o).start_time,"onUpdate:startTime":n[2]||(n[2]=t=>a(o).start_time=t),endTime:a(o).end_time,"onUpdate:endTime":n[3]||(n[3]=t=>a(o).end_time=t)},null,8,["startTime","endTime"])]),_:1}),e(r,null,{default:l(()=>[e(f,{type:"primary",onClick:a(c)},{default:l(()=>[...n[5]||(n[5]=[_("查询",-1)])]),_:1},8,["onClick"]),e(f,{onClick:a(C)},{default:l(()=>[...n[6]||(n[6]=[_("重置",-1)])]),_:1},8,["onClick"])]),_:1})]),_:1},8,["model"])]),_:1}),e(b,{class:"!border-none mt-4",shadow:"never"},{default:l(()=>[A((m(),w(B,{size:"large",data:a(i).lists},{default:l(()=>[e(s,{label:"用户账号",prop:"account","min-width":"100"}),e(s,{label:"用户昵称","min-width":"160"},{default:l(({row:t})=>[p("div",le,[e(U,{class:"flex-none mr-2",src:t.avatar,width:40,height:40,"preview-teleported":"",fit:"contain"},null,8,["src"]),_(" "+v(t.nickname),1)])]),_:1}),e(s,{label:"手机号码",prop:"mobile","min-width":"100"}),e(s,{label:"变动金额",prop:"change_amount","min-width":"100"},{default:l(({row:t})=>[p("span",{class:J({"text-error":t.action==2})},v(t.change_amount),3)]),_:1}),e(s,{label:"剩余金额",prop:"left_amount","min-width":"100"}),e(s,{label:"变动类型",prop:"change_type_desc","min-width":"120"}),e(s,{label:"来源单号",prop:"source_sn","min-width":"100"}),e(s,{label:"记录时间",prop:"create_time","min-width":"120"})]),_:1},8,["data"])),[[L,a(i).loading]]),p("div",oe,[e(F,{modelValue:a(i),"onUpdate:modelValue":n[4]||(n[4]=t=>Q(i)?i.value=t:null),onChange:a(d)},null,8,["modelValue","onChange"])])]),_:1})])}}});export{be as default};
@@ -1 +0,0 @@
import{o as T,W as $,ap as j,E as p,aq as z,q as d,r as l,s as r,v as c,D as y,u as S,b5 as I,w as A,F as D,L as M,K as F,M as k,n as C}from"./.pnpm-CLkClvFH.js";import{_ as K}from"./footer.vue_vue_type_script_setup_true_lang-q2axEfrC.js";import{u as Q,g as V,P as L,a as X,b as G,_ as H}from"./index-DmTxYTP_.js";import{a as J}from"./wecomOauthPostMessage-C_e6VGrO.js";const Y={class:"bind-wx flex flex-col"},Z={class:"flex-1 flex items-center justify-center"},ee={class:"bind-wx-card bg-body rounded-md px-10 py-10 w-[480px]"},te={key:0,class:"text-center py-10"},oe={key:0,class:"text-center py-10"},ne={key:1,id:"wxwork_bind_qrcode_container",class:"wxwork-qrcode mx-auto"},ae={class:"mt-8 flex justify-center gap-4"},w="bind_wxwork",g="like_admin_wx_bind_oauth",se=T({__name:"bind-work-wechat",setup(re){const u=$(),f=Q(),x=k(!1),_=k(!1),m=k({corp_id:"",agent_id:""}),U=()=>/wxwork/i.test(navigator.userAgent),v=()=>window.location.origin+window.location.pathname+"?bind_wxwork=1";function W(){const o=new URLSearchParams(window.location.search),e=window.location.hash||"",t=e.includes("?")?e.split("?").slice(1).join("?"):"";return t&&new URLSearchParams(t).forEach((n,i)=>{o.has(i)||o.set(i,n)}),o}const E=()=>{f.logout()},q=async()=>{try{await f.getUserInfo()}catch{}if(f.isPaw===0){u.replace("/change-password");return}u.replace(L.INDEX)},b=async o=>{_.value=!0;try{await G({code:o}),window.history.replaceState({},"",window.location.pathname),await q()}catch(e){console.error(e),p.error((e==null?void 0:e.msg)||(e==null?void 0:e.message)||"绑定失败"),_.value=!1}};let s=null;const B=async()=>{if(!m.value.corp_id)return;x.value=!0,window.WwLogin||await new Promise((e,t)=>{const n=document.createElement("script");n.src="https://wwcdn.weixin.qq.com/node/wework/wwopen/js/wwLogin-1.2.7.js",n.onload=()=>e(),n.onerror=()=>t(new Error("加载企业微信 JS 失败")),document.head.appendChild(n)}),await C(),x.value=!1,await C();const o=encodeURIComponent(v());new window.WwLogin({id:"wxwork_bind_qrcode_container",appid:m.value.corp_id,agentid:m.value.agent_id,redirect_uri:o,lang:"zh",state:w,href:"",self_redirect:!1})};return j(async()=>{let o="";try{o=(new URLSearchParams(window.location.search).get("code")||"").trim()}catch{o=""}if(!V()){p.error("请先登录"),u.push(L.LOGIN);return}const e=W(),t=o||(e.get("code")||"").trim(),n=e.get("state"),i=n!=null?String(n).trim():"",P=i===w||i==="admin_bind_wx"||i.toLowerCase()===w.toLowerCase(),R=e.get("bind_wxwork")==="1",N=!!t&&i===""&&u.currentRoute.value.path==="/bind-work-wechat";if(t&&(P||R||N)){if(sessionStorage.getItem(g)===t)return;sessionStorage.setItem(g,t);try{await b(t)}finally{sessionStorage.removeItem(g)}return}try{const a=await X();if(!(a!=null&&a.enabled)||!a.corp_id){p.error("企业微信未配置,请联系管理员");return}if(m.value={corp_id:a.corp_id,agent_id:a.agent_id},U()){_.value=!0;const h=encodeURIComponent(v()),O=`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${a.corp_id}&redirect_uri=${h}&response_type=code&scope=snsapi_privateinfo&agentid=${a.agent_id}&state=${w}#wechat_redirect`;window.location.href=O;return}s==null||s(),s=J({pathIncludes:"bind-work-wechat",lockKey:g,wxBindState:w,onCode:h=>b(h)}),await B()}catch(a){console.error(a),p.error("获取企业微信配置失败")}}),z(()=>{s==null||s(),s=null}),(o,e)=>{const t=A,n=F;return d(),l("div",Y,[r("div",Z,[r("div",ee,[e[4]||(e[4]=r("div",{class:"text-center text-2xl font-medium mb-2"},"绑定企业微信",-1)),e[5]||(e[5]=r("div",{class:"text-center text-gray-500 text-sm mb-8"}," 根据安全策略,需绑定企业微信账号后方可使用管理后台 ",-1)),_.value?(d(),l("div",te,[c(t,{class:"is-loading mb-4",size:40,color:"var(--el-color-primary)"},{default:y(()=>[c(S(I))]),_:1}),e[0]||(e[0]=r("div",{class:"text-gray-500"},"企业微信授权中...",-1))])):(d(),l(D,{key:1},[x.value?(d(),l("div",oe,[c(t,{class:"is-loading",size:32,color:"var(--el-color-primary)"},{default:y(()=>[c(S(I))]),_:1}),e[1]||(e[1]=r("div",{class:"mt-2 text-gray-400 text-sm"},"加载扫码...",-1))])):(d(),l("div",ne)),e[2]||(e[2]=r("div",{class:"text-center text-sm text-gray-400 mt-4"}," 请使用企业微信扫描二维码完成绑定 ",-1))],64)),r("div",ae,[c(n,{onClick:E},{default:y(()=>[...e[3]||(e[3]=[M("退出登录",-1)])]),_:1})])])]),c(K)])}}}),ue=H(se,[["__scopeId","data-v-d4a61522"]]);export{ue as default};
@@ -1 +0,0 @@
import{o as l,q as i,r as m,v as e,D as a,bh as p,bg as f,bj as b,u,bk as h,K as w,L as C,M as k}from"./.pnpm-CLkClvFH.js";import{i as E,B as x}from"./index-DmTxYTP_.js";const y={class:"cache"},g=l({name:"cache"}),N=l({...g,setup(v){const s=k([{content:"系统缓存",desc:"系统运行过程中产生的各类缓存数据"}]),c=async()=>{await E.confirm("确认清除系统缓存?"),await x(),window.location.reload()};return(B,o)=>{const r=p,n=f,t=h,_=w,d=b;return i(),m("div",y,[e(n,{class:"!border-none",shadow:"never"},{default:a(()=>[e(r,{type:"warning",title:"温馨提示:管理系统运行过程中产生的缓存",closable:!1,"show-icon":""})]),_:1}),e(n,{class:"!border-none mt-4",shadow:"never"},{default:a(()=>[e(d,{data:u(s),size:"large"},{default:a(()=>[e(t,{label:"管理内容",prop:"content","min-width":"130"}),e(t,{label:"内容说明",prop:"desc","min-width":"180"}),e(t,{label:"操作",width:"130",fixed:"right"},{default:a(()=>[e(_,{type:"primary",link:"",onClick:c},{default:a(()=>[...o[0]||(o[0]=[C("清除系统缓存",-1)])]),_:1})]),_:1})]),_:1},8,["data"])]),_:1})])}}});export{N as default};
@@ -1 +0,0 @@
import{o as k,W as v,ap as E,E as l,q as h,r as V,s as n,v as e,D as r,b6 as P,b7 as C,b8 as L,b9 as F,L as z,u as f,K as B,a8 as I,ba as N}from"./.pnpm-CLkClvFH.js";import{u as R,g as S,P as q,c as K,d as U,_ as D}from"./index-DmTxYTP_.js";import{u as M}from"./useLockFn-BSuuebQ0.js";import{_ as T}from"./footer.vue_vue_type_script_setup_true_lang-q2axEfrC.js";const $={class:"change-password flex flex-col"},j={class:"flex-1 flex items-center justify-center"},G={class:"change-password-card bg-body rounded-md px-10 py-10 w-[480px]"},O=k({__name:"change-password",setup(W){const i=I(),_=v(),m=R();E(()=>{S()||(l.error("请先登录"),_.push(q.LOGIN))});const o=N({password:"",password_confirm:""}),w={password:[{required:!0,message:"请输入新密码",trigger:"blur"},{min:6,message:"密码长度不能少于6位",trigger:"blur"}],password_confirm:[{required:!0,validator:(t,s,a)=>{s===""?a(new Error("请再次输入密码")):s!==o.password?a(new Error("两次输入的密码不一致")):a()},trigger:"blur"}]},p=async()=>{var t;await((t=i.value)==null?void 0:t.validate());try{await U({password:o.password,password_confirm:o.password_confirm}),l.success("密码修改成功,请重新登录"),m.isPaw=1,await m.logout()}catch(s){l.error((s==null?void 0:s.msg)||(s==null?void 0:s.message)||"密码修改失败")}},{isLock:g,lockFn:x}=M(p);return(t,s)=>{const a=K,c=C,u=P,b=F,y=B;return h(),V("div",$,[n("div",j,[n("div",G,[s[3]||(s[3]=n("div",{class:"text-center text-2xl font-medium mb-2"},"首次登录",-1)),s[4]||(s[4]=n("div",{class:"text-center text-gray-500 text-sm mb-8"},"为了您的账号安全,请修改初始密码",-1)),e(b,{ref_key:"formRef",ref:i,model:o,size:"large",rules:w},{default:r(()=>[e(u,{prop:"password"},{default:r(()=>[e(c,{modelValue:o.password,"onUpdate:modelValue":s[0]||(s[0]=d=>o.password=d),type:"password","show-password":"",placeholder:"请输入新密码"},{prepend:r(()=>[e(a,{name:"el-icon-Lock",size:"16"})]),_:1},8,["modelValue"])]),_:1}),e(u,{prop:"password_confirm"},{default:r(()=>[e(c,{modelValue:o.password_confirm,"onUpdate:modelValue":s[1]||(s[1]=d=>o.password_confirm=d),type:"password","show-password":"",placeholder:"请再次输入新密码",onKeyup:L(p,["enter"])},{prepend:r(()=>[e(a,{name:"el-icon-Lock",size:"16"})]),_:1},8,["modelValue"])]),_:1})]),_:1},8,["model"]),e(y,{type:"primary",size:"large",loading:f(g),onClick:f(x),class:"w-full"},{default:r(()=>[...s[2]||(s[2]=[z(" 确认修改 ",-1)])]),_:1},8,["loading","onClick"])])]),e(T)])}}}),Y=D(O,[["__scopeId","data-v-bbd4ab41"]]);export{Y as default};
@@ -1 +0,0 @@
import{r as t}from"./index-DmTxYTP_.js";function o(e){return t.get({url:"/tools.generator/generateTable",params:e})}function n(e){return t.get({url:"/tools.generator/dataTable",params:e})}function a(e){return t.post({url:"/tools.generator/selectTable",params:e})}function l(e){return t.get({url:"/tools.generator/detail",params:e})}function s(e){return t.post({url:"/tools.generator/syncColumn",params:e})}function u(e){return t.post({url:"/tools.generator/delete",params:e})}function g(e){return t.post({url:"/tools.generator/edit",params:e})}function i(e){return t.post({url:"/tools.generator/preview",params:e})}function c(e){return t.post({url:"/tools.generator/generate",params:e})}function f(){return t.get({url:"/tools.generator/getModels"})}export{f as a,o as b,u as c,i as d,c as e,n as f,g,a as h,s,l as t};
@@ -1 +0,0 @@
import{_ as o}from"./code-preview.vue_vue_type_script_setup_true_lang-DkKtX2m8.js";import"./.pnpm-CLkClvFH.js";import"./index-DmTxYTP_.js";export{o as default};
@@ -1 +0,0 @@
import{o as B,x as T,q as c,r as u,v as t,D as a,Y as D,C as i,u as m,F as N,G as $,O as j,X as F,s as p,a2 as L,K as S,L as U,bt as q,M as G,p as K,aC as M}from"./.pnpm-CLkClvFH.js";import{c as O,i as _}from"./index-DmTxYTP_.js";const P={class:"code-preview"},R={class:"flex",style:{height:"50vh"}},A=B({__name:"code-preview",props:{modelValue:{type:Boolean},code:{}},emits:["update:modelValue"],setup(d,{emit:f}){const b=d,V=f,{toClipboard:v}=M(),n=G("index0"),g=async l=>{try{await v(l),_.msgSuccess("复制成功")}catch{_.msgError("复制失败")}},s=K({get(){return b.modelValue},set(l){V("update:modelValue",l)}});return(l,e)=>{const h=T("highlightjs"),C=L,x=O,y=S,k=F,E=D,w=q;return c(),u("div",P,[t(w,{modelValue:m(s),"onUpdate:modelValue":e[1]||(e[1]=o=>i(s)?s.value=o:null),width:"900px",title:"代码预览"},{default:a(()=>[t(E,{modelValue:m(n),"onUpdate:modelValue":e[0]||(e[0]=o=>i(n)?n.value=o:null)},{default:a(()=>[(c(!0),u(N,null,$(d.code,(o,r)=>(c(),j(k,{label:o.name,name:`index${r}`,key:r},{default:a(()=>[p("div",R,[t(C,{class:"flex-1"},{default:a(()=>[t(h,{autodetect:"",code:o.content},null,8,["code"])]),_:2},1024),p("div",null,[t(y,{onClick:X=>g(o.content),type:"primary",link:""},{icon:a(()=>[t(x,{name:"el-icon-CopyDocument"})]),default:a(()=>[e[2]||(e[2]=U(" 复制 ",-1))]),_:1},8,["onClick"])])])]),_:2},1032,["label","name"]))),128))]),_:1},8,["modelValue"])]),_:1},8,["modelValue"])])}}});export{A as _};

Some files were not shown because too many files have changed in this diff Show More