更新
This commit is contained in:
@@ -4,7 +4,10 @@
|
||||
<div class="group">
|
||||
<div class="group__header">
|
||||
<div class="group__accent" />
|
||||
<div class="group__name">{{ node.dept_name }}</div>
|
||||
<div class="group__name">
|
||||
<el-icon class="group__name-icon"><FolderOpened /></el-icon>
|
||||
{{ node.dept_name }}
|
||||
</div>
|
||||
<div class="group__amount-wrap">
|
||||
<el-input-number
|
||||
v-model="node.target_amount"
|
||||
@@ -25,7 +28,8 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="group__badge-wrap">
|
||||
<el-tag v-if="node.target_id" size="small" type="success" effect="plain">已保存</el-tag>
|
||||
<el-tag v-if="node.target_id" size="small" type="success" effect="plain" round>已保存</el-tag>
|
||||
<span v-else class="group__unsaved-dot" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,22 +47,19 @@
|
||||
<!-- 子节点:表格行 -->
|
||||
<template v-else>
|
||||
<div class="row" :data-depth="String(Math.min(depth, 4))">
|
||||
<!-- 缩进空间 + 连接线 -->
|
||||
<div class="row__indent" :style="{ width: `${(depth - 1) * 20 + 16}px` }">
|
||||
<div class="row__connector" />
|
||||
</div>
|
||||
|
||||
<!-- 部门名 -->
|
||||
<div class="row__dept">
|
||||
<div class="row__dot" />
|
||||
<div class="row__dept-text">
|
||||
<span class="row__name">{{ node.dept_name }}</span>
|
||||
<span v-if="parentPath" class="row__path">{{ parentPath }}</span>
|
||||
</div>
|
||||
<el-tag v-if="node.target_id" size="small" type="success" effect="plain" class="row__saved">已保存</el-tag>
|
||||
<el-tag v-if="node.target_id" size="small" type="success" effect="plain" round class="row__saved">已保存</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 金额 -->
|
||||
<div class="row__amount">
|
||||
<el-input-number
|
||||
v-model="node.target_amount"
|
||||
@@ -71,7 +72,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 备注 -->
|
||||
<div class="row__remark">
|
||||
<el-input
|
||||
v-model="node.remark"
|
||||
@@ -82,7 +82,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 递归子节点 -->
|
||||
<MubiaoDeptNode
|
||||
v-for="c in node.children"
|
||||
:key="c.dept_id"
|
||||
@@ -94,6 +93,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { FolderOpened } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({ name: 'MubiaoDeptNode' })
|
||||
|
||||
@@ -124,7 +124,7 @@ const parentPath = computed(() =>
|
||||
<style lang="scss" scoped>
|
||||
/* ── 根节点:分组区块 ── */
|
||||
.group {
|
||||
border-bottom: 2px solid var(--el-border-color-lighter);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -135,14 +135,23 @@ const parentPath = computed(() =>
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
min-height: 52px;
|
||||
background: linear-gradient(90deg, var(--el-color-primary-light-9) 0%, var(--el-fill-color-extra-light) 60%);
|
||||
border-bottom: 1px solid var(--el-color-primary-light-7);
|
||||
min-height: 54px;
|
||||
background: linear-gradient(90deg,
|
||||
color-mix(in srgb, var(--el-color-primary) 6%, var(--el-bg-color)) 0%,
|
||||
color-mix(in srgb, var(--el-color-primary) 2%, var(--el-bg-color)) 50%,
|
||||
var(--el-bg-color) 100%
|
||||
);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--el-color-primary) 12%, var(--el-border-color-lighter));
|
||||
padding-right: 16px;
|
||||
transition: background 0.15s;
|
||||
cursor: default;
|
||||
transition: background 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg, var(--el-color-primary-light-8) 0%, var(--el-fill-color-light) 60%);
|
||||
background: linear-gradient(90deg,
|
||||
color-mix(in srgb, var(--el-color-primary) 10%, var(--el-bg-color)) 0%,
|
||||
color-mix(in srgb, var(--el-color-primary) 4%, var(--el-bg-color)) 50%,
|
||||
var(--el-fill-color-extra-light) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +159,7 @@ const parentPath = computed(() =>
|
||||
flex-shrink: 0;
|
||||
width: 4px;
|
||||
align-self: stretch;
|
||||
background: var(--el-color-primary);
|
||||
background: linear-gradient(180deg, var(--el-color-primary), var(--el-color-primary-light-3));
|
||||
border-radius: 0 2px 2px 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
@@ -158,40 +167,70 @@ const parentPath = computed(() =>
|
||||
.group__name {
|
||||
flex: 0 0 auto;
|
||||
width: 220px;
|
||||
font-size: 14px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
color: var(--el-color-primary-dark-2);
|
||||
padding-right: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.group__name-icon {
|
||||
font-size: 15px;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.group__amount-wrap {
|
||||
flex: 0 0 194px;
|
||||
border-left: 1px solid var(--el-color-primary-light-6);
|
||||
padding: 8px 12px;
|
||||
border-left: 1px solid color-mix(in srgb, var(--el-color-primary) 15%, var(--el-border-color-lighter));
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.group__amount-input {
|
||||
width: 170px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 6px;
|
||||
border-radius: 7px;
|
||||
background: var(--el-bg-color);
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-5) inset;
|
||||
box-shadow: 0 0 0 1.5px var(--el-color-primary-light-5) inset,
|
||||
0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
transition: box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1.5px var(--el-color-primary-light-3) inset,
|
||||
0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary-light-3) inset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group__remark-wrap {
|
||||
flex: 1;
|
||||
border-left: 1px solid var(--el-color-primary-light-6);
|
||||
padding: 8px 12px;
|
||||
border-left: 1px solid color-mix(in srgb, var(--el-color-primary) 15%, var(--el-border-color-lighter));
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.group__badge-wrap {
|
||||
flex: 0 0 60px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group__unsaved-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--el-border-color);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.group__body {
|
||||
@@ -202,10 +241,11 @@ const parentPath = computed(() =>
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
min-height: 46px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
transition: background 0.12s;
|
||||
transition: background 0.12s ease;
|
||||
padding-right: 16px;
|
||||
cursor: default;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -213,13 +253,29 @@ const parentPath = computed(() =>
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-extra-light);
|
||||
|
||||
.row__dot {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* 深度色阶 */
|
||||
&[data-depth='1'] { --dot: #67c23a; }
|
||||
&[data-depth='2'] { --dot: #e6a23c; }
|
||||
&[data-depth='3'] { --dot: #909399; }
|
||||
&[data-depth='4'] { --dot: var(--el-border-color-darker); }
|
||||
&[data-depth='1'] {
|
||||
--dot: #67c23a;
|
||||
--connector: color-mix(in srgb, #67c23a 30%, var(--el-border-color));
|
||||
}
|
||||
&[data-depth='2'] {
|
||||
--dot: #e6a23c;
|
||||
--connector: color-mix(in srgb, #e6a23c 30%, var(--el-border-color));
|
||||
}
|
||||
&[data-depth='3'] {
|
||||
--dot: #909399;
|
||||
--connector: var(--el-border-color);
|
||||
}
|
||||
&[data-depth='4'] {
|
||||
--dot: var(--el-border-color-darker);
|
||||
--connector: var(--el-border-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
.row__indent {
|
||||
@@ -235,8 +291,8 @@ const parentPath = computed(() =>
|
||||
top: 0;
|
||||
bottom: 50%;
|
||||
width: 10px;
|
||||
border-left: 1px dashed var(--el-border-color);
|
||||
border-bottom: 1px dashed var(--el-border-color);
|
||||
border-left: 1.5px dashed var(--connector, var(--el-border-color));
|
||||
border-bottom: 1.5px dashed var(--connector, var(--el-border-color));
|
||||
border-radius: 0 0 0 4px;
|
||||
}
|
||||
|
||||
@@ -255,6 +311,8 @@ const parentPath = computed(() =>
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--dot, #909399);
|
||||
transition: transform 0.15s ease;
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dot, #909399) 20%, transparent);
|
||||
}
|
||||
|
||||
.row__dept-text {
|
||||
@@ -297,8 +355,14 @@ const parentPath = computed(() =>
|
||||
|
||||
.row__amount-input {
|
||||
width: 170px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 6px;
|
||||
border-radius: 7px;
|
||||
transition: box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary-light-5) inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+200
-190
@@ -1,51 +1,46 @@
|
||||
<template>
|
||||
<div class="mubiao-page">
|
||||
|
||||
<!-- ── 顶部 Header ── -->
|
||||
<div class="header">
|
||||
<!-- 左:图标 + 文字 -->
|
||||
<div class="header__left">
|
||||
<div class="header__icon">
|
||||
<!-- ── 顶部工具栏 ── -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar__left">
|
||||
<div class="toolbar__icon">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="header__text">
|
||||
<span class="header__title">制定业绩目标</span>
|
||||
<span class="header__desc">按部门层级设置每月销售目标,填 0 并保存将删除该记录</span>
|
||||
<div>
|
||||
<div class="toolbar__title">制定业绩目标</div>
|
||||
<div class="toolbar__desc">按部门设置每月销售目标,填 0 并保存将删除该记录</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右:KPI + 操作 -->
|
||||
<div class="header__right">
|
||||
<!-- KPI 胶囊 -->
|
||||
<div class="kpi-capsule">
|
||||
<div class="kpi-cell">
|
||||
<span class="kpi-cell__label">目标月份</span>
|
||||
<el-date-picker
|
||||
v-model="yearMonth"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
placeholder="选择月份"
|
||||
:clearable="false"
|
||||
size="small"
|
||||
class="kpi-cell__picker"
|
||||
@change="loadMatrix"
|
||||
/>
|
||||
<div class="toolbar__center">
|
||||
<div class="month-picker-wrap">
|
||||
<span class="month-picker-wrap__label">目标月份</span>
|
||||
<el-date-picker
|
||||
v-model="yearMonth"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
placeholder="选择月份"
|
||||
:clearable="false"
|
||||
size="default"
|
||||
class="month-picker"
|
||||
@change="loadMatrix"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar__right">
|
||||
<div class="summary-strip">
|
||||
<div class="summary-item">
|
||||
<span class="summary-item__val">{{ deptCount }}</span>
|
||||
<span class="summary-item__label">部门</span>
|
||||
</div>
|
||||
<div class="kpi-sep" />
|
||||
<div class="kpi-cell">
|
||||
<span class="kpi-cell__label">部门数量</span>
|
||||
<span class="kpi-cell__val">{{ deptCount }}</span>
|
||||
</div>
|
||||
<div class="kpi-sep" />
|
||||
<div class="kpi-cell kpi-cell--primary">
|
||||
<span class="kpi-cell__label">目标合计</span>
|
||||
<span class="kpi-cell__val">¥ {{ formatMoney(totalDraft) }}</span>
|
||||
<div class="summary-divider" />
|
||||
<div class="summary-item">
|
||||
<span class="summary-item__val primary">¥{{ formatMoneyShort(totalDraft) }}</span>
|
||||
<span class="summary-item__label">目标合计</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-button :loading="loading" @click="loadMatrix">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
<el-button circle :loading="loading" @click="loadMatrix" title="刷新">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-perms="['finance.dept_performance_target/batchSave']"
|
||||
@@ -59,10 +54,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 主内容面板 ── -->
|
||||
<div class="panel" v-loading="loading" element-loading-text="数据加载中…">
|
||||
<!-- ── 看板区 ── -->
|
||||
<div v-loading="loading" element-loading-text="数据加载中…" class="board-wrap">
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!loading && formRows.length === 0" class="empty-state">
|
||||
<div class="empty-state__icon"><el-icon><OfficeBuilding /></el-icon></div>
|
||||
<p class="empty-state__title">暂无部门数据</p>
|
||||
@@ -70,25 +64,30 @@
|
||||
<el-button size="small" @click="loadMatrix"><el-icon><Refresh /></el-icon>重新加载</el-button>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 列头 - 严格对齐内容列 -->
|
||||
<div class="col-head">
|
||||
<div class="col-head__dept">部门</div>
|
||||
<div class="col-head__amount">目标金额(元)</div>
|
||||
<div class="col-head__remark">备注</div>
|
||||
<div class="col-head__badge" />
|
||||
</div>
|
||||
|
||||
<!-- 数据区 -->
|
||||
<div class="data-scroll">
|
||||
<MubiaoDeptNode
|
||||
<div v-else class="board-grid">
|
||||
<!-- 根级卡片 -->
|
||||
<TransitionGroup name="card" tag="div" class="board-grid__inner">
|
||||
<MubiaoDeptCard
|
||||
v-for="r in formRows"
|
||||
:key="r.dept_id"
|
||||
:key="'root-' + r.dept_id"
|
||||
:node="r"
|
||||
:depth="0"
|
||||
:is-root="true"
|
||||
:drilled-id="drilledId"
|
||||
@drill-in="handleDrillIn"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 钻取卡片(顺序追加在根卡片后) -->
|
||||
<MubiaoDeptCard
|
||||
v-for="item in drillStack"
|
||||
:key="'drill-' + item.dept_id"
|
||||
:node="item"
|
||||
:is-root="false"
|
||||
:drilled-id="drilledId"
|
||||
class="card--drilled"
|
||||
@drill-in="handleDrillIn"
|
||||
@close="handleClose(item.dept_id)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -97,7 +96,7 @@
|
||||
<script lang="ts" setup name="financeMubiao">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { TrendCharts, Refresh, Check, OfficeBuilding } from '@element-plus/icons-vue'
|
||||
import MubiaoDeptNode from './mubiao-dept-node.vue'
|
||||
import MubiaoDeptCard from './mubiao-dept-card.vue'
|
||||
import {
|
||||
deptPerformanceTargetBatchSave,
|
||||
deptPerformanceTargetMonthMatrix,
|
||||
@@ -153,21 +152,50 @@ function ymNow(): string {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function formatMoneyShort(v: number): string {
|
||||
if (v >= 10000) return (v / 10000).toFixed(1) + 'w'
|
||||
return v.toLocaleString('zh-CN', { maximumFractionDigits: 0 })
|
||||
}
|
||||
|
||||
const yearMonth = ref<string>(ymNow())
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const formRows = ref<Row[]>([])
|
||||
|
||||
// 钻取卡片栈:每次 drill-in 追加,close 时移除及其后续
|
||||
const drillStack = ref<Row[]>([])
|
||||
// 当前被钻取高亮的 dept_id(让来源卡片的那一行高亮)
|
||||
const drilledId = ref<number | null>(null)
|
||||
|
||||
const totalDraft = computed(() => sumTree(formRows.value))
|
||||
const deptCount = computed(() => countTree(formRows.value))
|
||||
|
||||
function formatMoney(v: number) {
|
||||
return Number(v || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
function handleDrillIn(node: Row) {
|
||||
// 已存在则不重复开
|
||||
if (drillStack.value.some(d => d.dept_id === node.dept_id)) {
|
||||
drilledId.value = node.dept_id
|
||||
return
|
||||
}
|
||||
drillStack.value.push(node)
|
||||
drilledId.value = node.dept_id
|
||||
}
|
||||
|
||||
function handleClose(deptId: number) {
|
||||
const idx = drillStack.value.findIndex(d => d.dept_id === deptId)
|
||||
if (idx !== -1) {
|
||||
// 关闭该卡及其之后所有钻取卡(子级)
|
||||
drillStack.value.splice(idx)
|
||||
}
|
||||
drilledId.value = drillStack.value.length > 0
|
||||
? drillStack.value[drillStack.value.length - 1].dept_id
|
||||
: null
|
||||
}
|
||||
|
||||
async function loadMatrix() {
|
||||
if (!yearMonth.value) return
|
||||
loading.value = true
|
||||
drillStack.value = []
|
||||
drilledId.value = null
|
||||
try {
|
||||
const res: any = await deptPerformanceTargetMonthMatrix({ year_month: yearMonth.value })
|
||||
formRows.value = ((res?.rows || []) as Record<string, any>[]).map(normalizeRow)
|
||||
@@ -189,6 +217,8 @@ async function handleSave() {
|
||||
})
|
||||
if (res?.rows) {
|
||||
formRows.value = (res.rows as Record<string, any>[]).map(normalizeRow)
|
||||
drillStack.value = []
|
||||
drilledId.value = null
|
||||
} else {
|
||||
await loadMatrix()
|
||||
}
|
||||
@@ -208,116 +238,79 @@ onMounted(loadMatrix)
|
||||
padding: 20px 20px 40px;
|
||||
}
|
||||
|
||||
/* ════════════════════ Header ════════════════════ */
|
||||
.header {
|
||||
/* ── 工具栏 ── */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
padding: 14px 20px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header__left {
|
||||
.toolbar__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header__icon {
|
||||
.toolbar__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--el-color-primary-light-7), var(--el-color-primary-light-9));
|
||||
color: var(--el-color-primary);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3));
|
||||
color: #fff;
|
||||
font-size: 19px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 6px color-mix(in srgb, var(--el-color-primary) 20%, transparent);
|
||||
box-shadow: 0 4px 10px color-mix(in srgb, var(--el-color-primary) 30%, transparent);
|
||||
}
|
||||
|
||||
.header__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.header__title {
|
||||
font-size: 15px;
|
||||
.toolbar__title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.header__desc {
|
||||
font-size: 12px;
|
||||
.toolbar__desc {
|
||||
font-size: 11.5px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
line-height: 1.3;
|
||||
line-height: 1.4;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.header__right {
|
||||
.toolbar__center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* KPI 胶囊 */
|
||||
.kpi-capsule {
|
||||
.month-picker-wrap {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 14px;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.kpi-sep {
|
||||
width: 1px;
|
||||
background: var(--el-border-color-lighter);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.kpi-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.kpi-cell--primary {
|
||||
background: linear-gradient(135deg, var(--el-color-primary-light-9), var(--el-color-primary-light-8));
|
||||
|
||||
.kpi-cell__val {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 700;
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-cell__label {
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.kpi-cell__val {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
white-space: nowrap;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.kpi-cell__picker {
|
||||
width: 112px;
|
||||
margin-top: 1px;
|
||||
.month-picker {
|
||||
width: 130px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
box-shadow: none !important;
|
||||
@@ -327,7 +320,7 @@ onMounted(loadMatrix)
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: var(--el-text-color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -337,72 +330,89 @@ onMounted(loadMatrix)
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════════════════ Panel ════════════════════ */
|
||||
.panel {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05);
|
||||
overflow: hidden;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* 列头 —— 宽度需与子组件 .group__name/.row__dept (220px) 对齐 */
|
||||
.col-head {
|
||||
.toolbar__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
padding-right: 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
letter-spacing: 0.04em;
|
||||
user-select: none;
|
||||
|
||||
/* 左侧 4px accent 占位 + 12px margin = 16px */
|
||||
padding-left: 16px;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 与 group/row 列宽严格对应 */
|
||||
.col-head__dept {
|
||||
flex: 0 0 220px;
|
||||
.summary-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.col-head__amount {
|
||||
flex: 0 0 194px;
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
gap: 1px;
|
||||
|
||||
.col-head__remark {
|
||||
flex: 1;
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
&__val {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
|
||||
.col-head__badge {
|
||||
flex: 0 0 60px;
|
||||
}
|
||||
&.primary { color: var(--el-color-primary); }
|
||||
}
|
||||
|
||||
/* 数据滚动区 */
|
||||
.data-scroll {
|
||||
max-height: min(72vh, 820px);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar { width: 5px; }
|
||||
&::-webkit-scrollbar-track { background: transparent; }
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--el-border-color);
|
||||
border-radius: 3px;
|
||||
&__label {
|
||||
font-size: 10.5px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════════════════ Empty ════════════════════ */
|
||||
.summary-divider {
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
background: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
/* ── 看板 ── */
|
||||
.board-wrap {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.board-grid__inner {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* 钻取卡片入场动画 */
|
||||
.card-enter-active {
|
||||
animation: card-in 0.25s ease;
|
||||
}
|
||||
.card-leave-active {
|
||||
animation: card-in 0.2s ease reverse;
|
||||
}
|
||||
|
||||
@keyframes card-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 钻取来源高亮标记 */
|
||||
:deep(.card--drilled) {
|
||||
--drill-glow: color-mix(in srgb, var(--el-color-primary) 18%, transparent);
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary-light-5), 0 4px 16px var(--drill-glow);
|
||||
}
|
||||
|
||||
/* ── 空状态 ── */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -413,12 +423,12 @@ onMounted(loadMatrix)
|
||||
}
|
||||
|
||||
.empty-state__icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 18px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 20px;
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 28px;
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
Reference in New Issue
Block a user