更新
This commit is contained in:
+112
-30
@@ -127,10 +127,10 @@
|
||||
<span class="yeji-table-tabs-head__title">表格统计</span>
|
||||
<span class="yeji-table-tabs-head__hint">列较多时可横向滚动浏览;「部门 / 排名」列在滚动时保持固定。</span>
|
||||
</div>
|
||||
<el-tabs v-model="yejiDisplayTab" class="yeji-view-tabs">
|
||||
<el-tab-pane label="医生统计" name="doctor" lazy>
|
||||
<el-tabs v-if="hasAnyYejiTableTab" v-model="yejiDisplayTab" class="yeji-view-tabs">
|
||||
<el-tab-pane v-if="canViewDoctorTab" label="医生统计" name="doctor" lazy>
|
||||
<el-card
|
||||
v-if="canViewDoctorDailyStats"
|
||||
v-if="canViewDoctorTab"
|
||||
class="yeji-panel doctor-daily-card doctor-daily-card--nested"
|
||||
shadow="never"
|
||||
>
|
||||
@@ -189,7 +189,7 @@
|
||||
<el-empty v-else description="当前账号无医生统计权限" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="医助排行榜" name="leaderboard" lazy>
|
||||
<el-tab-pane v-if="canViewLeaderboardTab" label="医助排行榜" name="leaderboard" lazy>
|
||||
<div
|
||||
v-if="leaderboardBlock || leaderboardsLoading"
|
||||
class="leaderboards-wrap"
|
||||
@@ -316,7 +316,7 @@
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="甄养堂互联网医院诊金" name="zyyt" lazy>
|
||||
<el-tab-pane v-if="canViewZyytTab" label="甄养堂互联网医院诊金" name="zyyt" lazy>
|
||||
<!-- ── 业绩表(4 张/N 张);目标看板见「目标看板(卡片)」Tab ── -->
|
||||
<div v-loading="loading" class="tables-wrap">
|
||||
<div v-if="!loading && tables.length === 0" class="empty-tip">
|
||||
@@ -445,12 +445,8 @@
|
||||
<el-tooltip placement="top" effect="dark" :show-after="200">
|
||||
<template #content>
|
||||
<div style="max-width: 320px; line-height: 1.7; font-size: 12px">
|
||||
<<<<<<< HEAD
|
||||
<b>业务订单条数</b>(计业绩):订单 <b>create_time</b> 落入区间、<b>fulfillment_status ∉ {4,9,10}</b>(<b>NULL 计入</b>);按<b>订单创建人</b>的人事部门落在该部门子树即计入(表格多行命中时取最深的展示部门)。与<b>合计业绩 / 医助排行榜接诊诊单</b>同口径。选定渠道时本列仍为全量。
|
||||
=======
|
||||
<b>业务订单条数</b>(计业绩):订单 <b>create_time</b> 落入区间、<b>fulfillment_status ∉ {4,9,10}</b>;与列表筛选
|
||||
<b>assistant_dept_id</b> 时一致——<b>创建人</b>人事部门优先,无创建人则诊单 <b>医助</b>;落在该部门子树即计入(表格多行命中时取最深的展示部门)。与侧栏勾选「与表格业绩对齐」时的集合可能略有差异。选定渠道时本列仍为全量。
|
||||
>>>>>>> master
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="col-info"><InfoFilled /></el-icon>
|
||||
@@ -636,7 +632,7 @@
|
||||
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="目标看板(卡片)" name="zyyt_matrix" lazy>
|
||||
<el-tab-pane v-if="canViewTargetMatrixTab" label="目标看板(卡片)" name="zyyt_matrix" lazy>
|
||||
<div v-loading="targetProgressLoading" class="tp-matrix-wrap">
|
||||
<template v-if="targetProgressCard">
|
||||
<h2 class="tp-matrix__title">{{ targetProgressCard.title }}</h2>
|
||||
@@ -702,18 +698,19 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-empty v-else description="当前账号无表格统计 Tab 权限" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="yeji-panel yeji-charts-only-card" shadow="never">
|
||||
<div class="yeji-charts-only-head">统计图</div>
|
||||
<div
|
||||
v-loading="loading || (canViewDoctorDailyStats && doctorDailyLoading) || leaderboardsLoading"
|
||||
v-loading="loading || (canViewDoctorTab && doctorDailyLoading) || (canViewLeaderboardTab && leaderboardsLoading)"
|
||||
class="yeji-charts-stack"
|
||||
>
|
||||
<template v-if="chartsHasAnyData">
|
||||
<section
|
||||
v-if="
|
||||
canViewDoctorDailyStats &&
|
||||
canViewDoctorTab &&
|
||||
(doctorDealBarHasData ||
|
||||
doctorRxStackHasData ||
|
||||
doctorAppointmentPieHasData ||
|
||||
@@ -855,7 +852,7 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-if="leaderboardBlock">
|
||||
<template v-if="canViewLeaderboardTab && leaderboardBlock">
|
||||
<section
|
||||
v-for="lb in leaderboardChartBlocks"
|
||||
:key="'lb-chart-' + lb.dept_id"
|
||||
@@ -898,8 +895,8 @@
|
||||
<el-empty
|
||||
v-else-if="
|
||||
!loading &&
|
||||
(!canViewDoctorDailyStats || !doctorDailyLoading) &&
|
||||
!leaderboardsLoading
|
||||
(!canViewDoctorTab || !doctorDailyLoading) &&
|
||||
(!canViewLeaderboardTab || !leaderboardsLoading)
|
||||
"
|
||||
class="yeji-charts-empty"
|
||||
description="当前筛选下暂无图表数据,请调整条件后查询"
|
||||
@@ -1318,7 +1315,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { Search, RefreshRight, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -1337,7 +1334,14 @@ import {
|
||||
} from '@/api/stats'
|
||||
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
|
||||
import { prescriptionOrderLists } from '@/api/tcm'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { hasPermission } from '@/utils/perm'
|
||||
|
||||
type YejiDisplayTab = 'doctor' | 'leaderboard' | 'zyyt' | 'zyyt_matrix'
|
||||
|
||||
/** 任一权限命中即可(兼容原有接口按钮权限 + 新增 Tab 独立权限) */
|
||||
function hasAnyPermission(perms: string[]): boolean {
|
||||
return perms.some(p => hasPermission([p]))
|
||||
}
|
||||
|
||||
interface DeptOption {
|
||||
id: number
|
||||
@@ -1664,14 +1668,60 @@ const appointmentLinesShowChannelColumn = computed(() =>
|
||||
)
|
||||
|
||||
/** 图表 / 数据表切换 */
|
||||
const yejiDisplayTab = ref<'doctor' | 'leaderboard' | 'zyyt' | 'zyyt_matrix'>('zyyt')
|
||||
const yejiDisplayTab = ref<YejiDisplayTab>('zyyt')
|
||||
|
||||
/** 与菜单 stats.doctorDailyStats/overview 一致;无权限则不展示医生统计表/图、不请求接口 */
|
||||
const userStore = useUserStore()
|
||||
const DOCTOR_DAILY_STATS_PERM = 'stats.doctorDailyStats/overview'
|
||||
const canViewDoctorDailyStats = computed(() => {
|
||||
const perms = userStore.perms || []
|
||||
return perms.some(p => p === '*' || p === DOCTOR_DAILY_STATS_PERM)
|
||||
/** Tab 可见性:优先认 Tab 独立权限;未配置时兼容原有接口按钮权限,避免破坏已有角色 */
|
||||
const YEJI_TAB_DOCTOR_PERM = 'stats.yejiStats/tabDoctor'
|
||||
const YEJI_TAB_LEADERBOARD_PERM = 'stats.yejiStats/tabLeaderboard'
|
||||
const YEJI_TAB_ZYYT_PERM = 'stats.yejiStats/tabZyyt'
|
||||
const YEJI_TAB_TARGET_MATRIX_PERM = 'stats.yejiStats/tabTargetMatrix'
|
||||
|
||||
const canViewDoctorTab = computed(() =>
|
||||
hasAnyPermission([YEJI_TAB_DOCTOR_PERM, 'stats.doctorDailyStats/overview'])
|
||||
)
|
||||
const canViewLeaderboardTab = computed(() =>
|
||||
hasAnyPermission([YEJI_TAB_LEADERBOARD_PERM, 'stats.yejiStats/leaderboard'])
|
||||
)
|
||||
const canViewZyytTab = computed(() =>
|
||||
hasAnyPermission([
|
||||
YEJI_TAB_ZYYT_PERM,
|
||||
'stats.yejiStats/multi',
|
||||
'stats.yejiStats/overview',
|
||||
])
|
||||
)
|
||||
const canViewTargetMatrixTab = computed(() =>
|
||||
hasAnyPermission([
|
||||
YEJI_TAB_TARGET_MATRIX_PERM,
|
||||
'stats.yejiStats/targetMatrix',
|
||||
'stats.yejiStats/multi',
|
||||
])
|
||||
)
|
||||
const hasAnyYejiTableTab = computed(
|
||||
() =>
|
||||
canViewDoctorTab.value ||
|
||||
canViewLeaderboardTab.value ||
|
||||
canViewZyytTab.value ||
|
||||
canViewTargetMatrixTab.value
|
||||
)
|
||||
|
||||
function pickInitialYejiTab(): YejiDisplayTab {
|
||||
if (canViewZyytTab.value) return 'zyyt'
|
||||
if (canViewDoctorTab.value) return 'doctor'
|
||||
if (canViewLeaderboardTab.value) return 'leaderboard'
|
||||
if (canViewTargetMatrixTab.value) return 'zyyt_matrix'
|
||||
return 'zyyt'
|
||||
}
|
||||
|
||||
watch(yejiDisplayTab, tab => {
|
||||
if (tab === 'doctor' && !canViewDoctorTab.value) {
|
||||
yejiDisplayTab.value = pickInitialYejiTab()
|
||||
} else if (tab === 'leaderboard' && !canViewLeaderboardTab.value) {
|
||||
yejiDisplayTab.value = pickInitialYejiTab()
|
||||
} else if (tab === 'zyyt' && !canViewZyytTab.value) {
|
||||
yejiDisplayTab.value = pickInitialYejiTab()
|
||||
} else if (tab === 'zyyt_matrix' && !canViewTargetMatrixTab.value) {
|
||||
yejiDisplayTab.value = pickInitialYejiTab()
|
||||
}
|
||||
})
|
||||
|
||||
const leaderboardChartBlocks = computed(() =>
|
||||
@@ -1715,7 +1765,7 @@ const doctorRxStackHasData = computed(() =>
|
||||
)
|
||||
|
||||
const doctorChartsHasData = computed(() => {
|
||||
if (!canViewDoctorDailyStats.value) {
|
||||
if (!canViewDoctorTab.value) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
@@ -1731,12 +1781,14 @@ const chartsHasAnyData = computed(() => {
|
||||
if (doctorChartsHasData.value) {
|
||||
return true
|
||||
}
|
||||
for (const tb of tables.value) {
|
||||
if (yejiDeptChartHasData(tb)) {
|
||||
return true
|
||||
if (canViewZyytTab.value) {
|
||||
for (const tb of tables.value) {
|
||||
if (yejiDeptChartHasData(tb)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (leaderboardChartBlocks.value.length > 0) {
|
||||
if (canViewLeaderboardTab.value && leaderboardChartBlocks.value.length > 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -2073,6 +2125,12 @@ function getDoctorDailySummaries(param: { columns: any[] }) {
|
||||
}
|
||||
|
||||
async function loadDoctorDailyStats() {
|
||||
if (!canViewDoctorTab.value) {
|
||||
doctorDailyRows.value = []
|
||||
doctorDailyTotal.value = {}
|
||||
doctorDailyRange.value = null
|
||||
return
|
||||
}
|
||||
doctorDailyLoading.value = true
|
||||
try {
|
||||
const res: any = await doctorDailyStatsOverview(buildDoctorDailyRequestParams())
|
||||
@@ -2211,6 +2269,10 @@ function daysInMonth(ym: string): number {
|
||||
}
|
||||
|
||||
async function loadTargetProgress() {
|
||||
if (!canViewTargetMatrixTab.value) {
|
||||
targetProgressCard.value = null
|
||||
return
|
||||
}
|
||||
targetProgressLoading.value = true
|
||||
targetProgressCard.value = null
|
||||
try {
|
||||
@@ -2458,6 +2520,10 @@ async function loadChannelOptions() {
|
||||
}
|
||||
|
||||
async function loadLeaderboard(range: { start: string; end: string }) {
|
||||
if (!canViewLeaderboardTab.value) {
|
||||
leaderboardBlock.value = null
|
||||
return
|
||||
}
|
||||
leaderboardsLoading.value = true
|
||||
try {
|
||||
const p: Record<string, any> = {
|
||||
@@ -2491,6 +2557,18 @@ async function loadLeaderboard(range: { start: string; end: string }) {
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
leaderboardBlock.value = null
|
||||
if (!canViewZyytTab.value) {
|
||||
tables.value = []
|
||||
loading.value = false
|
||||
if (canViewDoctorTab.value) {
|
||||
void loadDoctorDailyStats()
|
||||
} else {
|
||||
doctorDailyRows.value = []
|
||||
doctorDailyTotal.value = {}
|
||||
doctorDailyRange.value = null
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const baseParams: Record<string, any> = {}
|
||||
if (selectedDeptIds.value.length > 0) {
|
||||
@@ -2542,7 +2620,7 @@ async function loadData() {
|
||||
ElMessage.error(any?.msg || any?.message || '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (canViewDoctorDailyStats.value) {
|
||||
if (canViewDoctorTab.value) {
|
||||
void loadDoctorDailyStats()
|
||||
} else {
|
||||
doctorDailyRows.value = []
|
||||
@@ -2661,6 +2739,9 @@ function buildOrderDrawerListParams(): Record<string, any> | null {
|
||||
if (selectedDeptIds.value.length > 0) {
|
||||
params.dept_ids = selectedDeptIds.value.join(',')
|
||||
}
|
||||
if (f.yejiTableRowDeptIds && f.yejiTableRowDeptIds.length > 0) {
|
||||
params.yeji_table_row_dept_ids = f.yejiTableRowDeptIds.join(',')
|
||||
}
|
||||
} else {
|
||||
params.assistant_id = f.assistantId
|
||||
if (f.erCenterRevisitSlot !== undefined) {
|
||||
@@ -3561,6 +3642,7 @@ function formatLocalYmd(d: Date): string {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
yejiDisplayTab.value = pickInitialYejiTab()
|
||||
await Promise.all([loadDeptOptions(), loadChannelOptions()])
|
||||
loadData()
|
||||
loadTargetProgress()
|
||||
|
||||
Reference in New Issue
Block a user