This commit is contained in:
Your Name
2026-05-11 16:10:06 +08:00
parent 62d15efbaf
commit 15cf6324f6
8 changed files with 2349 additions and 19 deletions
+57
View File
@@ -103,3 +103,60 @@ export function doctorDailyStatsOverview(params: {
}) {
return request.get({ url: '/stats.doctorDailyStats/overview', params })
}
/** 提成结算接口计算较重,单独放宽超时(默认多为 60s) */
const COMMISSION_SETTLEMENT_TIMEOUT_MS = 120000
/** 提成结算业绩(独立于业绩看板 yejiStats) */
export function commissionSettlementOverview(params: {
settlement_month: string
dept_ids?: number[] | string
channel_code?: string
}) {
return request.get(
{ url: '/stats.commissionSettlement/overview', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
{ ignoreCancelToken: true }
)
}
export function commissionSettlementDeptOptions() {
return request.get({ url: '/stats.commissionSettlement/deptOptions' })
}
export function commissionSettlementChannelOptions() {
return request.get({ url: '/stats.commissionSettlement/channelOptions' })
}
/** 提成核对:订单明细分页 bucket: 空|current|deferred */
export function commissionSettlementOrderLines(params: {
settlement_month: string
dept_ids?: number[] | string
channel_code?: string
page?: number
page_size?: number
bucket?: string
}) {
return request.get(
{ url: '/stats.commissionSettlement/orderLines', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
{ ignoreCancelToken: true }
)
}
export function commissionSettlementConfirmStatus(params: {
settlement_month: string
dept_ids?: number[] | string
channel_code?: string
}) {
return request.get({ url: '/stats.commissionSettlement/confirmStatus', params })
}
export function commissionSettlementSaveReconcile(params: Record<string, any>) {
return request.post({ url: '/stats.commissionSettlement/saveReconcile', params })
}
export function commissionSettlementConfirmFinalize(params: Record<string, any>) {
return request.post(
{ url: '/stats.commissionSettlement/confirmFinalize', params, timeout: COMMISSION_SETTLEMENT_TIMEOUT_MS },
{ ignoreCancelToken: true }
)
}
@@ -0,0 +1,988 @@
<template>
<div class="cs-page">
<header class="cs-masthead">
<div class="cs-masthead__inner">
<div>
<h1 class="cs-masthead__title">提成结算业绩</h1>
<p class="cs-masthead__desc">
系统代开处方对应的已完成业务订单按结算月 7 日截止拆分本期提成与顺延下期可按部门汇总或下发至业绩归属医助与处方开方医生
</p>
</div>
</div>
</header>
<el-card class="cs-panel filter-card" shadow="never">
<div class="filter-surface">
<div class="filter-row filter-row--top">
<div class="filter-field">
<span class="lbl">结算月</span>
<el-date-picker
v-model="settlementMonth"
type="month"
value-format="YYYY-MM"
placeholder="选择结算月"
class="filter-month"
/>
</div>
<div class="filter-field">
<span class="lbl">渠道来源</span>
<el-select
v-model="selectedChannel"
filterable
clearable
placeholder="全部"
class="filter-select filter-select--channel"
:loading="channelOptionsLoading"
@change="loadOverview"
>
<el-option label="全部" value="" />
<el-option-group
v-for="g in channelGroups"
:key="g.group_name"
:label="g.group_name"
>
<el-option
v-for="ch in g.channels"
:key="ch.channel_code"
:label="ch.channel_name"
:value="ch.channel_code"
/>
</el-option-group>
</el-select>
</div>
<div class="filter-field filter-field--wide">
<span class="lbl">展示部门</span>
<div
class="filter-select filter-select--dept filter-dept-tree-wrap"
v-loading="deptOptionsLoading"
>
<el-tree-select
v-model="selectedDeptIds"
:data="deptTreeOptions"
multiple
collapse-tags
collapse-tags-tooltip
filterable
check-strictly
:render-after-expand="false"
:default-expand-all="true"
placeholder="默认:所有「中心」部门"
class="filter-dept-tree-select"
node-key="id"
:props="deptTreeSelectProps"
/>
</div>
</div>
<div class="filter-actions">
<el-button
type="primary"
class="filter-btn filter-btn--query"
:loading="loading"
@click="loadOverview"
>
<el-icon class="filter-btn__icon"><Search /></el-icon>
查询
</el-button>
<el-button class="filter-btn filter-btn--reset" :disabled="loading" @click="handleReset">
<el-icon class="filter-btn__icon"><RefreshRight /></el-icon>
重置
</el-button>
</div>
</div>
</div>
</el-card>
<el-alert type="info" :closable="false" show-icon class="cs-alert">
<template #title>
<span class="cs-alert__title">统计口径</span>
</template>
<div class="cs-alert__body">
<p v-if="meta.rule_note">{{ meta.rule_note }}</p>
<p v-else>
订单创建月为<strong>结算月的上一自然月</strong><strong>系统代开处方</strong>完成挂号等自动生成对应的<strong>履约已完成</strong>业务订单
<strong>签收</strong>时间取物流签收时间<strong>尾款支付</strong>取关联支付单已支付时间优先尾款/全部费用类型
签收与尾款均在<strong>结算月 7 24:00 </strong>完成的计入本期提成其余计入顺延下期
</p>
<p v-if="meta.cutoff_end" class="cs-alert__meta">
当前结算月<strong>{{ meta.settlement_month }}</strong>
· 订单创建月<strong>{{ meta.order_month }}</strong>
· 截止时刻<strong>{{ meta.cutoff_end }}</strong>
<template v-if="meta.channel_name">
· 渠道<strong>{{ meta.channel_name }}</strong>
</template>
</p>
</div>
</el-alert>
<el-card class="cs-panel cs-reconcile-card" shadow="never">
<div class="cs-reconcile">
<div class="cs-reconcile__head">
<div class="cs-reconcile__tags">
<el-tag v-if="confirmInfo?.status === 1" type="success" effect="dark">已确定业绩</el-tag>
<el-tag v-else type="warning" effect="plain">待确定业绩</el-tag>
<span
v-if="confirmInfo?.status === 1 && confirmInfo.confirmed_at_text"
class="cs-reconcile__meta"
>
{{ confirmInfo.confirmed_admin_name || '—' }} · {{ confirmInfo.confirmed_at_text }}
</span>
</div>
<div class="cs-reconcile__toolbar">
<el-button type="primary" plain @click="openReconcileDialog">业绩明细核对</el-button>
<el-button :disabled="confirmInfo?.status === 1 || reconcileSaving" @click="handleSaveReconcile">
保存核对备注
</el-button>
<el-button
type="warning"
:disabled="confirmInfo?.status === 1 || confirmSubmitting"
@click="handleConfirmFinalize"
>
确定本期业绩
</el-button>
</div>
</div>
<el-input
v-model="reconcileNote"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="核对备注(可与财务线下对齐后填写;确定业绩前可反复保存)"
:disabled="confirmInfo?.status === 1"
class="cs-reconcile__note"
/>
<p v-if="Number(totalAgg.carry_in_order_count) > 0" class="cs-reconcile__carry">
上期顺延汇入本期<strong>{{ totalAgg.carry_in_order_count }}</strong> 已并入当前汇总
</p>
</div>
</el-card>
<el-card class="cs-panel cs-summary-card" shadow="never">
<el-tabs v-model="summaryTab" class="cs-summary-tabs">
<el-tab-pane label="部门汇总" name="dept">
<div class="cs-table-head">
<span class="cs-table-head__title">部门汇总</span>
<span class="cs-table-head__hint">
金额单位与业绩看板相同的部门摊列规则业绩归属医助 展示中心
</span>
</div>
<el-table
v-loading="loading"
:data="rows"
border
stripe
size="small"
class="cs-table"
show-summary
:summary-method="getSummaries"
>
<el-table-column prop="dept_name" label="部门" min-width="120" fixed />
<el-table-column prop="completed_order_count" label="已完成单数" min-width="104" align="right" />
<el-table-column prop="completed_order_amount" label="已完成金额" min-width="112" align="right">
<template #default="{ row }">{{ formatMoney(row.completed_order_amount) }}</template>
</el-table-column>
<el-table-column prop="current_period_count" label="本期提成单数" min-width="118" align="right" />
<el-table-column prop="current_period_amount" label="本期提成金额" min-width="118" align="right">
<template #default="{ row }">{{ formatMoney(row.current_period_amount) }}</template>
</el-table-column>
<el-table-column prop="deferred_period_count" label="顺延下期单数" min-width="118" align="right" />
<el-table-column prop="deferred_period_amount" label="顺延下期金额" min-width="118" align="right">
<template #default="{ row }">{{ formatMoney(row.deferred_period_amount) }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="按医助下发" name="assistant">
<div class="cs-table-head">
<span class="cs-table-head__title">业绩归属医助</span>
<span class="cs-table-head__hint">
按诊单业绩归属规则聚合到具体医助顺延汇入为上期结转进入本期订单池的单数
</span>
</div>
<el-table
v-loading="loading"
:data="assistantRows"
border
stripe
size="small"
class="cs-table"
show-summary
:summary-method="getPersonSummaries"
>
<el-table-column prop="assistant_name" label="医助" min-width="120" fixed show-overflow-tooltip />
<el-table-column prop="primary_dept_name" label="归属中心" min-width="120" show-overflow-tooltip />
<el-table-column prop="carry_in_order_count" label="顺延汇入单数" min-width="112" align="right" />
<el-table-column prop="completed_order_count" label="已完成单数" min-width="104" align="right" />
<el-table-column prop="completed_order_amount" label="已完成金额" min-width="112" align="right">
<template #default="{ row }">{{ formatMoney(row.completed_order_amount) }}</template>
</el-table-column>
<el-table-column prop="current_period_count" label="本期提成单数" min-width="118" align="right" />
<el-table-column prop="current_period_amount" label="本期提成金额" min-width="118" align="right">
<template #default="{ row }">{{ formatMoney(row.current_period_amount) }}</template>
</el-table-column>
<el-table-column prop="deferred_period_count" label="顺延下期单数" min-width="118" align="right" />
<el-table-column prop="deferred_period_amount" label="顺延下期金额" min-width="118" align="right">
<template #default="{ row }">{{ formatMoney(row.deferred_period_amount) }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="按医生下发" name="doctor">
<div class="cs-table-head">
<span class="cs-table-head__title">处方开方医生</span>
<span class="cs-table-head__hint">
按处方创建账号开方人聚合可与医助维度交叉核对
</span>
</div>
<el-table
v-loading="loading"
:data="doctorRows"
border
stripe
size="small"
class="cs-table"
show-summary
:summary-method="getPersonSummaries"
>
<el-table-column prop="doctor_name" label="医生" min-width="120" fixed show-overflow-tooltip />
<el-table-column prop="primary_dept_name" label="归属中心" min-width="120" show-overflow-tooltip />
<el-table-column prop="carry_in_order_count" label="顺延汇入单数" min-width="112" align="right" />
<el-table-column prop="completed_order_count" label="已完成单数" min-width="104" align="right" />
<el-table-column prop="completed_order_amount" label="已完成金额" min-width="112" align="right">
<template #default="{ row }">{{ formatMoney(row.completed_order_amount) }}</template>
</el-table-column>
<el-table-column prop="current_period_count" label="本期提成单数" min-width="118" align="right" />
<el-table-column prop="current_period_amount" label="本期提成金额" min-width="118" align="right">
<template #default="{ row }">{{ formatMoney(row.current_period_amount) }}</template>
</el-table-column>
<el-table-column prop="deferred_period_count" label="顺延下期单数" min-width="118" align="right" />
<el-table-column prop="deferred_period_amount" label="顺延下期金额" min-width="118" align="right">
<template #default="{ row }">{{ formatMoney(row.deferred_period_amount) }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</el-card>
<el-dialog
v-model="reconcileDialogVisible"
title="业绩明细核对"
width="1040px"
destroy-on-close
class="cs-dialog"
@open="onReconcileDialogOpen"
>
<div class="cs-dialog__bar">
<el-radio-group v-model="linesBucket" size="small" @change="loadOrderLines(1)">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="current">本期提成</el-radio-button>
<el-radio-button label="deferred">顺延下期</el-radio-button>
</el-radio-group>
</div>
<el-table v-loading="linesLoading" :data="linesRows" border stripe size="small" max-height="440">
<el-table-column prop="order_no" label="业务订单号" min-width="116" show-overflow-tooltip />
<el-table-column prop="assistant_name" label="业绩医助" min-width="100" show-overflow-tooltip />
<el-table-column prop="doctor_name" label="开方医生" min-width="100" show-overflow-tooltip />
<el-table-column prop="primary_dept_name" label="归属部门" min-width="100" show-overflow-tooltip />
<el-table-column prop="amount" label="金额" width="108" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column label="分段" width="88" align="center">
<template #default="{ row }">
<el-tag v-if="row.bucket === 'current'" type="success" size="small">本期</el-tag>
<el-tag v-else type="warning" size="small">顺延</el-tag>
</template>
</el-table-column>
<el-table-column label="结转" width="72" align="center">
<template #default="{ row }">
<span v-if="row.is_carry_in"></span>
<span v-else class="cs-muted"></span>
</template>
</el-table-column>
<el-table-column prop="sign_time_text" label="签收时间" min-width="152" show-overflow-tooltip />
<el-table-column prop="pay_time_text" label="尾款支付" min-width="152" show-overflow-tooltip />
<el-table-column prop="create_time_text" label="订单创建" width="152" />
</el-table>
<div class="cs-dialog__pager">
<el-pagination
layout="total, prev, pager, next"
:total="linesTotal"
:page-size="linesPageSize"
:current-page="linesPage"
@current-change="loadOrderLines"
/>
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { Search, RefreshRight } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
commissionSettlementChannelOptions,
commissionSettlementConfirmFinalize,
commissionSettlementDeptOptions,
commissionSettlementOrderLines,
commissionSettlementOverview,
commissionSettlementSaveReconcile,
} from '@/api/stats'
interface DeptOption {
id: number
name: string
pid: number
path: string
}
interface DeptTreeNode extends DeptOption {
children?: DeptTreeNode[]
}
const deptTreeSelectProps = {
value: 'id',
label: 'name',
children: 'children',
}
function buildDeptTreeFromFlat(flat: DeptOption[]): DeptTreeNode[] {
if (!flat.length) return []
const map = new Map<number, DeptTreeNode>()
for (const d of flat) {
map.set(d.id, { ...d, children: [] })
}
const roots: DeptTreeNode[] = []
for (const d of flat) {
const node = map.get(d.id)!
const pid = Number(d.pid)
if (pid > 0 && map.has(pid)) {
map.get(pid)!.children!.push(node)
} else {
roots.push(node)
}
}
const stripEmptyChildren = (n: DeptTreeNode) => {
if (n.children?.length) {
n.children.forEach(stripEmptyChildren)
} else {
delete n.children
}
}
roots.forEach(stripEmptyChildren)
const sortLevel = (nodes: DeptTreeNode[]) => {
nodes.sort((a, b) => {
const ac = a.name.includes('中心') ? -1 : 1
const bc = b.name.includes('中心') ? -1 : 1
if (ac !== bc) return ac - bc
return a.id - b.id
})
for (const n of nodes) {
if (n.children?.length) sortLevel(n.children)
}
}
sortLevel(roots)
return roots
}
interface ChannelOption {
channel_code: string
channel_name: string
source_tag_id: string
source_group_name: string
customer_count: number
}
interface ChannelGroup {
group_name: string
channels: ChannelOption[]
}
interface CsRow {
dept_id: number
dept_name: string
completed_order_count: number
completed_order_amount: number
current_period_count: number
current_period_amount: number
deferred_period_count: number
deferred_period_amount: number
}
interface CsAssistRow {
assistant_id: number
assistant_name: string
primary_dept_id: number
primary_dept_name: string
carry_in_order_count: number
completed_order_count: number
completed_order_amount: number
current_period_count: number
current_period_amount: number
deferred_period_count: number
deferred_period_amount: number
}
interface CsDoctorRow {
doctor_id: number
doctor_name: string
primary_dept_id: number
primary_dept_name: string
carry_in_order_count: number
completed_order_count: number
completed_order_amount: number
current_period_count: number
current_period_amount: number
deferred_period_count: number
deferred_period_amount: number
}
type CsPersonMetricRow = CsAssistRow | CsDoctorRow
function defaultSettlementMonth(): string {
const d = new Date()
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
return `${y}-${m}`
}
const settlementMonth = ref<string>(defaultSettlementMonth())
const loading = ref(false)
const deptOptions = ref<DeptOption[]>([])
const deptOptionsLoading = ref(false)
const deptTreeOptions = computed(() => buildDeptTreeFromFlat(deptOptions.value))
const selectedDeptIds = ref<number[]>([])
const channelGroups = ref<ChannelGroup[]>([])
const channelOptionsLoading = ref(false)
const selectedChannel = ref<string>('')
const rows = ref<CsRow[]>([])
const assistantRows = ref<CsAssistRow[]>([])
const doctorRows = ref<CsDoctorRow[]>([])
const summaryTab = ref<'dept' | 'assistant' | 'doctor'>('dept')
const totalAgg = ref<{
completed_order_count?: number
completed_order_amount?: number
current_period_count?: number
current_period_amount?: number
deferred_period_count?: number
deferred_period_amount?: number
carry_in_order_count?: number
}>({})
const meta = reactive({
settlement_month: '',
order_month: '',
cutoff_end: '',
rule_note: '',
channel_name: '',
})
const confirmInfo = ref<{
status?: number
reconcile_note?: string
confirmed_admin_name?: string
confirmed_at_text?: string
} | null>(null)
const reconcileNote = ref('')
const reconcileSaving = ref(false)
const confirmSubmitting = ref(false)
const reconcileDialogVisible = ref(false)
const linesLoading = ref(false)
const linesBucket = ref<string>('all')
const linesRows = ref<Record<string, any>[]>([])
const linesTotal = ref(0)
const linesPage = ref(1)
const linesPageSize = ref(20)
function formatMoney(v: number | null | undefined) {
if (v === null || v === undefined || !Number.isFinite(Number(v))) return '—'
return `¥ ${Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
async function loadDeptOptions() {
deptOptionsLoading.value = true
try {
const res: DeptOption[] = await commissionSettlementDeptOptions()
deptOptions.value = (res || []).sort((a, b) => {
const aIsCenter = a.name.includes('中心') ? -1 : 1
const bIsCenter = b.name.includes('中心') ? -1 : 1
if (aIsCenter !== bIsCenter) return aIsCenter - bIsCenter
return a.id - b.id
})
} catch (_e) {
// ignore
} finally {
deptOptionsLoading.value = false
}
}
async function loadChannelOptions() {
channelOptionsLoading.value = true
try {
const res: any = await commissionSettlementChannelOptions()
channelGroups.value = res?.groups || []
} catch (_e) {
// ignore
} finally {
channelOptionsLoading.value = false
}
}
async function loadOverview() {
const sm = settlementMonth.value
if (!sm) {
ElMessage.warning('请选择结算月')
return
}
loading.value = true
try {
const res: any = await commissionSettlementOverview(buildQueryParams() as any)
rows.value = res?.rows || []
assistantRows.value = res?.assistant_rows || []
doctorRows.value = res?.doctor_rows || []
totalAgg.value = res?.total || {}
meta.settlement_month = res?.settlement_month || sm
meta.order_month = res?.order_month || ''
meta.cutoff_end = res?.cutoff_end || ''
meta.rule_note = res?.rule_note || ''
meta.channel_name = res?.channel_name || ''
confirmInfo.value = res?.confirm ?? null
reconcileNote.value =
res?.confirm?.reconcile_note != null ? String(res.confirm.reconcile_note) : ''
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载失败')
rows.value = []
assistantRows.value = []
doctorRows.value = []
totalAgg.value = {}
confirmInfo.value = null
} finally {
loading.value = false
}
}
function buildQueryParams(): Record<string, any> {
const sm = settlementMonth.value
const p: Record<string, any> = { settlement_month: sm }
if (selectedDeptIds.value.length > 0) {
p.dept_ids = selectedDeptIds.value.join(',')
}
if (selectedChannel.value) {
p.channel_code = selectedChannel.value
}
return p
}
function openReconcileDialog() {
if (!settlementMonth.value) {
ElMessage.warning('请选择结算月')
return
}
reconcileDialogVisible.value = true
}
function onReconcileDialogOpen() {
linesBucket.value = 'all'
loadOrderLines(1)
}
async function loadOrderLines(page: number) {
if (!settlementMonth.value) return
linesLoading.value = true
linesPage.value = page
try {
const p: Record<string, any> = {
...buildQueryParams(),
page,
page_size: linesPageSize.value,
}
if (linesBucket.value && linesBucket.value !== 'all') {
p.bucket = linesBucket.value
}
const res: any = await commissionSettlementOrderLines(p as any)
linesRows.value = res?.list || []
linesTotal.value = Number(res?.count ?? 0)
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载明细失败')
linesRows.value = []
linesTotal.value = 0
} finally {
linesLoading.value = false
}
}
async function handleSaveReconcile() {
if (!settlementMonth.value) {
ElMessage.warning('请选择结算月')
return
}
if (confirmInfo.value?.status === 1) {
ElMessage.warning('已确定业绩不可修改备注')
return
}
reconcileSaving.value = true
try {
await commissionSettlementSaveReconcile({
...buildQueryParams(),
reconcile_note: reconcileNote.value,
})
ElMessage.success('已保存核对备注')
await loadOverview()
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '保存失败')
} finally {
reconcileSaving.value = false
}
}
async function handleConfirmFinalize() {
if (!settlementMonth.value) {
ElMessage.warning('请选择结算月')
return
}
try {
await ElMessageBox.confirm(
'确定后将为「顺延下期」订单写入结转记录;下一结算月在相同渠道与部门筛选下自动并入统计。已确定后不可在线撤销,请与财务核对后再操作。',
'确定本期业绩',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
} catch {
return
}
confirmSubmitting.value = true
try {
const out: any = await commissionSettlementConfirmFinalize({
...buildQueryParams(),
reconcile_note: reconcileNote.value,
})
ElMessage.success(
`已确定本期业绩;结转 ${Number(out?.deferred_carry_count ?? 0)} 单至 ${String(out?.next_settlement_month ?? '下期')}`
)
await loadOverview()
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '提交失败')
} finally {
confirmSubmitting.value = false
}
}
function handleReset() {
settlementMonth.value = defaultSettlementMonth()
selectedDeptIds.value = []
selectedChannel.value = ''
loadOverview()
}
function getPersonSummaries(param: { columns: Array<{ property?: string }>; data: CsPersonMetricRow[] }) {
const { columns } = param
const t = totalAgg.value
const sums: string[] = []
columns.forEach((column, index: number) => {
if (index === 0) {
sums[index] = '合计'
return
}
const prop = column.property as string | undefined
if (!t || !prop) {
sums[index] = ''
return
}
if (prop === 'primary_dept_name') {
sums[index] = ''
return
}
if (prop === 'carry_in_order_count') {
sums[index] = String(t.carry_in_order_count ?? 0)
return
}
const map: Record<string, string> = {
completed_order_count: String(t.completed_order_count ?? 0),
completed_order_amount: formatMoney(t.completed_order_amount),
current_period_count: String(t.current_period_count ?? 0),
current_period_amount: formatMoney(t.current_period_amount),
deferred_period_count: String(t.deferred_period_count ?? 0),
deferred_period_amount: formatMoney(t.deferred_period_amount),
}
sums[index] = map[String(prop)] ?? ''
})
return sums
}
function getSummaries(param: { columns: Array<{ property?: string }>; data: CsRow[] }) {
const { columns } = param
const t = totalAgg.value
const sums: string[] = []
columns.forEach((column, index: number) => {
if (index === 0) {
sums[index] = '合计'
return
}
const prop = column.property as keyof CsRow | undefined
if (!t || !prop) {
sums[index] = ''
return
}
const map: Record<string, string> = {
completed_order_count: String(t.completed_order_count ?? 0),
completed_order_amount: formatMoney(t.completed_order_amount),
current_period_count: String(t.current_period_count ?? 0),
current_period_amount: formatMoney(t.current_period_amount),
deferred_period_count: String(t.deferred_period_count ?? 0),
deferred_period_amount: formatMoney(t.deferred_period_amount),
}
sums[index] = map[String(prop)] ?? ''
})
return sums
}
onMounted(async () => {
await Promise.all([loadDeptOptions(), loadChannelOptions()])
await loadOverview()
})
</script>
<style lang="scss" scoped>
.cs-page {
min-width: 0;
--cs-brand: #6366f1;
--cs-brand-soft: #eef2ff;
--cs-ink: #1e293b;
--cs-muted: #64748b;
--cs-line: #e2e8f0;
--cs-surface: #ffffff;
--cs-page: #f1f5f9;
--cs-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
--cs-radius: 16px;
--cs-radius-sm: 10px;
min-height: 100%;
padding: 24px;
color: var(--cs-ink);
background: var(--cs-page);
}
.cs-masthead {
margin-bottom: 24px;
&__inner {
padding: 28px 32px;
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
border-radius: var(--cs-radius);
box-shadow: var(--cs-shadow-sm);
}
&__title {
margin: 0;
font-size: 1.5rem;
font-weight: 800;
color: #fff;
line-height: 1.2;
}
&__desc {
margin: 8px 0 0;
font-size: 0.875rem;
color: #94a3b8;
line-height: 1.5;
}
}
.cs-panel {
margin-bottom: 20px;
border-radius: var(--cs-radius);
border: 1px solid var(--cs-line);
background: var(--cs-surface);
box-shadow: var(--cs-shadow-sm);
:deep(.el-card__body) {
padding: 0;
}
}
.filter-card {
overflow: hidden;
}
.filter-surface {
padding: 20px;
background: var(--cs-surface);
}
.filter-row--top {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 16px 20px;
}
.filter-field {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
.lbl {
font-size: 12px;
font-weight: 600;
color: var(--cs-muted);
}
}
.filter-field--wide {
flex: 1 1 260px;
min-width: 220px;
}
.filter-month {
width: 168px;
}
.filter-select--channel {
width: 220px;
}
.filter-dept-tree-select {
width: 100%;
min-width: 260px;
}
.filter-actions {
display: flex;
gap: 10px;
margin-left: auto;
}
.filter-btn {
padding: 8px 20px;
font-weight: 600;
border-radius: var(--cs-radius-sm);
&__icon {
margin-right: 4px;
vertical-align: -2px;
}
&--query {
background: var(--cs-brand);
color: #fff;
border: none;
}
&--reset {
border: 1px solid var(--cs-line);
background: var(--cs-surface);
}
}
.cs-alert {
margin-bottom: 20px;
border-radius: var(--cs-radius-sm);
&__title {
font-weight: 700;
}
&__body {
font-size: 13px;
line-height: 1.65;
color: var(--cs-muted);
p {
margin: 0 0 8px;
}
p:last-child {
margin-bottom: 0;
}
}
&__meta {
margin-top: 10px !important;
font-size: 12px;
color: var(--cs-ink);
}
}
.cs-summary-card {
:deep(.el-tabs__header) {
margin: 0;
padding: 12px 20px 0;
border-bottom: 1px solid var(--cs-line);
}
:deep(.el-tabs__nav-wrap::after) {
height: 0;
}
:deep(.el-tabs__content) {
padding: 0;
}
}
.cs-table-head {
padding: 14px 20px 10px;
background: #f8fafc;
border-bottom: 1px solid var(--cs-line);
&__title {
font-size: 15px;
font-weight: 700;
}
&__hint {
display: block;
margin-top: 6px;
font-size: 12px;
color: var(--cs-muted);
}
}
.cs-table {
width: 100%;
:deep(.el-table__header th) {
background: #f8fafc !important;
font-weight: 600;
font-size: 12px;
}
:deep(.el-table__footer-wrapper td) {
font-weight: 700;
background: #fff7ed !important;
}
}
.cs-reconcile-card {
:deep(.el-card__body) {
padding: 16px 20px 18px;
}
}
.cs-reconcile {
display: flex;
flex-direction: column;
gap: 12px;
&__head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
}
&__tags {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
&__meta {
font-size: 12px;
color: var(--cs-muted);
}
&__toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
&__note {
width: 100%;
max-width: 720px;
}
&__carry {
margin: 0;
font-size: 12px;
color: var(--cs-muted);
line-height: 1.5;
}
}
.cs-dialog__bar {
margin-bottom: 12px;
}
.cs-dialog__pager {
margin-top: 14px;
display: flex;
justify-content: flex-end;
}
.cs-muted {
color: var(--cs-muted);
}
</style>
File diff suppressed because one or more lines are too long