This commit is contained in:
Your Name
2026-05-05 14:51:35 +08:00
parent 40eec808ef
commit 3ef10846b4
10 changed files with 492 additions and 57 deletions
+3 -3
View File
@@ -25,7 +25,7 @@ export function deptDetail(params: any) {
return request.get({ url: '/dept.dept/detail', params })
}
// 部门列表全部
export function deptAll() {
return request.get({ url: '/dept.dept/all' })
// 部门列表全部apply_data_scope=1 时按当前账号角色数据权限收窄树(与 DataScopeService 一致)
export function deptAll(params?: { apply_data_scope?: number }) {
return request.get({ url: '/dept.dept/all', params })
}
+11 -1
View File
@@ -43,7 +43,17 @@ export function yejiStatsLeaderboard(params: {
return request.get({ url: '/stats.yejiStats/leaderboard', params })
}
/** 医生统计(start/end、channel_code;含数据范围;不按展示部门 dept_ids 筛选 */
/** 「未归属中心」行:按订单创建人拆解合计业绩(与看板「合计业绩」未归属部分同口径 */
export function yejiStatsUnassignedBreakdown(params: {
start_date: string
end_date: string
dept_ids?: string
channel_code?: string
tag_id?: string
}) {
return request.get({ url: '/stats.yejiStats/unassignedBreakdown', params })
}
export function doctorDailyStatsOverview(params: {
start_date?: string
end_date?: string
+28 -19
View File
@@ -60,25 +60,26 @@
<el-divider content-position="left">组织信息</el-divider>
<!-- 归属部门 -->
<!-- 归属部门数据权限内仅末级可选父级仅用于展开 -->
<el-form-item label="归属部门" prop="dept_id">
<el-tree-select
class="flex-1"
v-model="formData.dept_id"
:data="optionsData.dept"
clearable
multiple
node-key="id"
:props="{
label: 'name',
disabled(data: any) {
return data.status !== 1
}
}"
check-strictly
:default-expand-all="true"
placeholder="请选择归属部门"
/>
<div class="w-full">
<el-tree-select
class="flex-1 w-full"
v-model="formData.dept_id"
:data="optionsData.dept"
clearable
multiple
node-key="id"
:props="{
label: 'name',
disabled: deptTreeNodeDisabled
}"
check-strictly
:default-expand-all="true"
placeholder="请选择末级归属部门"
/>
<div class="form-tips">仅可选末级部门含下级的父部门不可选与数据权限范围一致</div>
</div>
</el-form-item>
<!-- 岗位 -->
@@ -215,6 +216,13 @@ import { getDictData } from '@/api/app'
import { useDictOptions } from '@/hooks/useDictOptions'
import feedback from '@/utils/feedback'
/** 归属部门:禁止选父级,仅末级可选(status=1 且无子节点) */
function deptTreeNodeDisabled(data: { status?: number; children?: unknown[] }) {
if (data.status !== 1) return true
const ch = data.children
return Array.isArray(ch) && ch.length > 0
}
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const visible = ref(false)
@@ -264,7 +272,8 @@ const { optionsData } = useDictOptions<{
api: jobsAll
},
dept: {
api: deptAll
api: deptAll,
params: { apply_data_scope: 1 }
}
})
+28 -19
View File
@@ -54,25 +54,26 @@
<el-divider content-position="left">组织信息</el-divider>
<!-- 归属部门 -->
<!-- 归属部门数据权限内仅末级可选父级仅用于展开 -->
<el-form-item label="归属部门" prop="dept_id">
<el-tree-select
class="flex-1"
v-model="formData.dept_id"
:data="optionsData.dept"
clearable
multiple
node-key="id"
:props="{
label: 'name',
disabled(data: any) {
return data.status !== 1
}
}"
check-strictly
:default-expand-all="true"
placeholder="请选择归属部门"
/>
<div class="w-full">
<el-tree-select
class="flex-1 w-full"
v-model="formData.dept_id"
:data="optionsData.dept"
clearable
multiple
node-key="id"
:props="{
label: 'name',
disabled: deptTreeNodeDisabled
}"
check-strictly
:default-expand-all="true"
placeholder="请选择末级归属部门"
/>
<div class="form-tips">仅可选末级部门含下级的父部门不可选与数据权限范围一致</div>
</div>
</el-form-item>
<!-- 岗位 -->
@@ -254,6 +255,13 @@ import { getDictData } from '@/api/app'
import { useDictOptions } from '@/hooks/useDictOptions'
import feedback from '@/utils/feedback'
/** 归属部门:禁止选父级,仅末级可选(status=1 且无子节点) */
function deptTreeNodeDisabled(data: { status?: number; children?: unknown[] }) {
if (data.status !== 1) return true
const ch = data.children
return Array.isArray(ch) && ch.length > 0
}
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const visible = ref(false)
@@ -307,7 +315,8 @@ const { optionsData } = useDictOptions<{
api: jobsAll
},
dept: {
api: deptAll
api: deptAll,
params: { apply_data_scope: 1 }
}
})
+3 -2
View File
@@ -99,11 +99,12 @@ import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
// 表单数据 - 固定role_id=1查询医生
// 表单数据 - 固定 role_id=1 查询医生;数据范围与医助列表一致,仅看自己权限内成员
const formData = reactive({
account: '',
name: '',
role_id: 1 // 固定为医生角色
role_id: 1,
apply_data_scope: 1
})
const showEdit = ref(false)
+197 -12
View File
@@ -594,9 +594,12 @@
<tbody>
<tr
v-for="r in tb.rows"
:key="r.dept_id"
:key="`${idx}-${r.dept_id}-${r.dept_name}`"
class="yeji-row--action"
:class="{ 'yeji-row--idle': r.dept_id <= 0 }"
:class="{
'yeji-row--unassigned': isUnassignedYejiRow(r),
'yeji-row--idle': r.dept_id <= 0 && !isUnassignedYejiRow(r),
}"
@click="onYejiDeptRowClick(tb, r)"
>
<td class="col-dept">{{ r.dept_name }}</td>
@@ -811,6 +814,50 @@
</div>
</div>
</el-drawer>
<el-dialog
v-model="unassignedDialogVisible"
width="min(680px, 94vw)"
destroy-on-close
class="yeji-unassigned-dialog"
>
<template #header>
<div class="yeji-unassigned-dialog__head">
<span class="yeji-unassigned-dialog__title">「未归属中心」业绩拆解</span>
<p class="yeji-unassigned-dialog__sub">{{ unassignedDialogSubtitle }}</p>
</div>
</template>
<p v-if="unassignedDialogNote" class="yeji-unassigned-dialog__note">{{ unassignedDialogNote }}</p>
<div v-loading="unassignedDialogLoading" class="yeji-unassigned-dialog__body">
<el-table
v-if="unassignedDialogRows.length > 0 || unassignedDialogLoading"
:data="unassignedDialogRows"
size="small"
stripe
border
max-height="440"
class="yeji-unassigned-dialog__table"
:empty-text="unassignedDialogLoading ? '加载中…' : '暂无数据'"
>
<el-table-column prop="name" label="创建人" min-width="120" show-overflow-tooltip />
<el-table-column label="账号" width="88" align="right">
<template #default="{ row }">
{{ row.admin_id > 0 ? row.admin_id : '—' }}
</template>
</el-table-column>
<el-table-column label="订单数" width="88" align="right">
<template #default="{ row }">{{ formatInt(row.order_count) }}</template>
</el-table-column>
<el-table-column label="业绩金额" min-width="112" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
</el-table>
<el-empty
v-if="!unassignedDialogLoading && unassignedDialogRows.length === 0"
description="暂无拆解数据"
/>
</div>
</el-dialog>
</div>
</template>
@@ -820,12 +867,13 @@ import { Search, RefreshRight, InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import vCharts from 'vue-echarts'
import {
doctorDailyStatsOverview,
yejiStatsChannelOptions,
yejiStatsDeptOptions,
yejiStatsLeaderboard,
yejiStatsMulti,
yejiStatsOverview,
doctorDailyStatsOverview,
yejiStatsUnassignedBreakdown,
} from '@/api/stats'
import { deptPerformanceTargetMonthMatrix } from '@/api/finance'
import { prescriptionOrderLists } from '@/api/tcm'
@@ -907,6 +955,13 @@ interface ChannelGroup {
channels: ChannelOption[]
}
interface YejiUnassignedBreakdownRow {
admin_id: number
name: string
order_count: number
amount: number
}
interface YejiRow {
dept_id: number
dept_name: string
@@ -984,6 +1039,12 @@ const tables = ref<YejiTable[]>([])
const leaderboardBlock = ref<LeaderboardPack | null>(null)
const leaderboardsLoading = ref(false)
const unassignedDialogVisible = ref(false)
const unassignedDialogLoading = ref(false)
const unassignedDialogSubtitle = ref('')
const unassignedDialogNote = ref('')
const unassignedDialogRows = ref<YejiUnassignedBreakdownRow[]>([])
/** 图表 / 数据表切换 */
const yejiDisplayTab = ref<'charts' | 'table'>('table')
@@ -1934,10 +1995,58 @@ function onOrderDrawerPageSizeChange(size: number) {
void fetchOrderDrawerPage()
}
function isUnassignedYejiRow(row: YejiRow): boolean {
return row.dept_id <= 0 && row.dept_name === '未归属中心'
}
function buildUnassignedBreakdownSubtitle(tb: YejiTable): string {
const parts = [`区间:${tb.start_date} ${tb.end_date}`]
if (selectedChannel.value) {
parts.push(`渠道:${tb.channel_name || selectedChannel.value}`)
}
if (selectedDeptIds.value.length > 0) {
parts.push(`已选展示部门 ${selectedDeptIds.value.length} 个`)
}
return parts.join(' · ')
}
async function openUnassignedBreakdown(tb: YejiTable) {
unassignedDialogSubtitle.value = buildUnassignedBreakdownSubtitle(tb)
unassignedDialogNote.value = ''
unassignedDialogRows.value = []
unassignedDialogVisible.value = true
unassignedDialogLoading.value = true
try {
const p: Record<string, string> = {
start_date: tb.start_date,
end_date: tb.end_date,
}
if (selectedDeptIds.value.length > 0) {
p.dept_ids = selectedDeptIds.value.join(',')
}
if (selectedChannel.value) {
p.channel_code = selectedChannel.value
}
const res: any = await yejiStatsUnassignedBreakdown(p as any)
unassignedDialogRows.value = Array.isArray(res?.rows) ? res.rows : []
unassignedDialogNote.value = typeof res?.note === 'string' ? res.note : ''
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载未归属拆解失败')
unassignedDialogRows.value = []
unassignedDialogNote.value = ''
} finally {
unassignedDialogLoading.value = false
}
}
/**
* 部门业绩行:有子部门 → 仅筛选该节点(后端展开子行);末级 → 侧栏业务订单
* 部门业绩行:有子部门 → 仅筛选该节点(后端展开子行);末级 → 侧栏业务订单;「未归属中心」→ 按创建人拆解弹窗
*/
function onYejiDeptRowClick(tb: YejiTable, row: YejiRow) {
if (isUnassignedYejiRow(row)) {
void openUnassignedBreakdown(tb)
return
}
if (row.dept_id <= 0) return
if (deptHasChildren(row.dept_id)) {
selectedDeptIds.value = [row.dept_id]
@@ -2312,6 +2421,13 @@ onMounted(async () => {
--yj-radius: 16px;
--yj-radius-sm: 10px;
--yj-font: 'Inter', var(--el-font-family), system-ui, sans-serif;
/* 部门列竖条(表头略深、与指标列区分) */
--yj-dept-col-bg: #eef2ff;
--yj-dept-col-bg-hover: #e0e7ff;
--yj-dept-col-head-start: #e0e7ff;
--yj-dept-col-head-end: #e8ecff;
--yj-dept-col-border: #c7d2fe;
--yj-dept-col-head-ink: #3730a3;
min-height: 100%;
padding: 24px;
@@ -2911,7 +3027,7 @@ onMounted(async () => {
}
.col-abbr {
width: 48px;
min-width: 56px;
}
.col-name {
@@ -2972,6 +3088,45 @@ onMounted(async () => {
font-size: 13px;
color: var(--yj-ink);
thead th.col-dept,
thead th.col-abbr {
min-width: 88px;
background: linear-gradient(
180deg,
var(--yj-dept-col-head-start) 0%,
var(--yj-dept-col-head-end) 100%
);
color: var(--yj-dept-col-head-ink);
font-weight: 700;
border-right: 2px solid var(--yj-dept-col-border);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
thead th.col-abbr {
min-width: 56px;
}
tbody td.col-dept,
tbody td.col-abbr {
background: var(--yj-dept-col-bg);
color: var(--yj-ink);
font-weight: 600;
border-right: 2px solid var(--yj-dept-col-border);
text-align: center;
}
tbody tr.yeji-row--action:not(.yeji-row--idle):hover td.col-dept,
tbody tr.yeji-row--action:not(.yeji-row--idle):hover td.col-abbr {
background: var(--yj-dept-col-bg-hover);
}
.total-row td.col-dept {
background: color-mix(in srgb, var(--yj-accent-soft) 55%, var(--yj-dept-col-bg) 45%);
color: var(--yj-ink);
font-weight: 700;
border-right: 2px solid color-mix(in srgb, var(--yj-dept-col-border) 55%, var(--yj-line) 45%);
}
thead th {
background: #f1f5f9;
color: var(--yj-ink-secondary);
@@ -2998,13 +3153,6 @@ onMounted(async () => {
font-variant-numeric: tabular-nums;
}
.col-dept {
width: 80px;
font-weight: 600;
background: var(--yj-surface-muted);
color: var(--yj-ink-secondary);
}
tbody tr.yeji-row--action {
cursor: pointer;
transition: background 0.18s ease;
@@ -3013,6 +3161,10 @@ onMounted(async () => {
background: var(--yj-brand-soft);
}
&.yeji-row--unassigned {
cursor: pointer;
}
&.yeji-row--idle {
cursor: default;
@@ -3194,6 +3346,39 @@ onMounted(async () => {
justify-content: flex-end;
}
.yeji-unassigned-dialog__head {
line-height: 1.35;
}
.yeji-unassigned-dialog__title {
display: block;
font-size: 16px;
font-weight: 700;
color: var(--yj-ink, #0f172a);
}
.yeji-unassigned-dialog__sub {
margin: 6px 0 0;
font-size: 12px;
color: var(--yj-ink-secondary, #64748b);
font-weight: 400;
}
.yeji-unassigned-dialog__note {
margin: 0 0 12px;
padding: 10px 12px;
font-size: 12px;
line-height: 1.65;
color: var(--yj-ink-secondary, #64748b);
background: var(--yj-accent-soft, #fff7ed);
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--yj-line, #e2e8f0) 80%, transparent);
}
.yeji-unassigned-dialog__body {
min-height: 120px;
}
@media (prefers-reduced-motion: reduce) {
.date-pill,
.filter-btn,