员工类型新增领导与普通员工区分

This commit is contained in:
fuenmao
2026-04-02 13:54:11 +08:00
parent c2c72fb66c
commit fd90aa3e19
3 changed files with 581 additions and 106 deletions
@@ -28,7 +28,6 @@
:disabled="isViewMode"> :disabled="isViewMode">
<el-option v-for="item in employeeOptions" :key="item.id" :label="item.name" :value="item.id" /> <el-option v-for="item in employeeOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
<!-- 新增/编辑模式显示"新增员工"按钮 -->
<el-button v-if="!isViewMode" type="primary" size="small" @click="handleOpenAddEmployeeDialog"> <el-button v-if="!isViewMode" type="primary" size="small" @click="handleOpenAddEmployeeDialog">
新增员工 新增员工
</el-button> </el-button>
@@ -36,6 +35,13 @@
<el-input v-if="!showEmployeeSelect && !formData.employeeId" v-model="formData.newEmployeeName" <el-input v-if="!showEmployeeSelect && !formData.employeeId" v-model="formData.newEmployeeName"
placeholder="请输入员工姓名" class="mt-2" :disabled="isViewMode" /> placeholder="请输入员工姓名" class="mt-2" :disabled="isViewMode" />
</el-form-item> </el-form-item>
<!-- 员工性质 -->
<el-form-item label="员工性质" prop="type">
<el-radio-group v-model="formData.type" :disabled="isViewMode">
<el-radio :label="1">领导</el-radio>
<el-radio :label="2">普通员工</el-radio>
</el-radio-group>
</el-form-item>
</template> </template>
<!-- 员工家属模式 --> <!-- 员工家属模式 -->
@@ -239,7 +245,7 @@ import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { prescriptionApi } from '@/api' import { prescriptionApi } from '@/api'
import { medicinalApi } from '@/api/modules/biz/weibingju/medicinal' import { medicinalApi } from '@/api/modules/biz/WeiBingJu/medicinal'
// 接口定义 // 接口定义
interface Employee { interface Employee {
@@ -247,6 +253,7 @@ interface Employee {
name: string; name: string;
birthday?: string; birthday?: string;
height?: number; height?: number;
type?: number;
} }
interface Relative { interface Relative {
id: string | number; id: string | number;
@@ -294,6 +301,7 @@ const isEdit = computed(() => props.isEdit)
// 表单数据 // 表单数据
const formData = ref({ const formData = ref({
personType: 1, personType: 1,
type: undefined as number | undefined,
employeeId: undefined as string | number | undefined, employeeId: undefined as string | number | undefined,
newEmployeeName: '', newEmployeeName: '',
relatedEmployeeId: undefined as string | number | undefined, relatedEmployeeId: undefined as string | number | undefined,
@@ -313,6 +321,17 @@ const formData = ref({
// 表单校验规则 // 表单校验规则
const formRules: FormRules = { const formRules: FormRules = {
personType: [{ required: true, message: '请选择人员类型', trigger: 'change' }], personType: [{ required: true, message: '请选择人员类型', trigger: 'change' }],
type: [{
required: true,
validator: (rule, value, callback) => {
if (formData.value.personType === 1 && !formData.value.type) {
callback(new Error('请选择员工性质'))
} else {
callback()
}
},
trigger: 'change'
}],
employeeInfo: [{ employeeInfo: [{
required: true, required: true,
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
@@ -440,7 +459,7 @@ const loadRelatives = async (employeeId: number | string) => {
showRelativeSelect.value = false showRelativeSelect.value = false
ElMessage.info('该员工为本地新增,暂无家属数据,请手动输入家属姓名') ElMessage.info('该员工为本地新增,暂无家属数据,请手动输入家属姓名')
} else { } else {
const res: any = await prescriptionApi.getPersonList({ type: 2, employeeId }) const res: any = await prescriptionApi.getPersonList({ type: 2, employeeId: Number(employeeId) })
relativeOptions.value = (res && res.data) ? res.data : [] relativeOptions.value = (res && res.data) ? res.data : []
showRelativeSelect.value = relativeOptions.value.length > 0 showRelativeSelect.value = relativeOptions.value.length > 0
} }
@@ -489,6 +508,7 @@ const handleEmployeeChange = (selectedId: string | number | undefined) => {
if (selectedEmployee) { if (selectedEmployee) {
formData.value.birthday = selectedEmployee.birthday formData.value.birthday = selectedEmployee.birthday
formData.value.height = selectedEmployee.height formData.value.height = selectedEmployee.height
formData.value.type = selectedEmployee.type
} }
} }
} }
@@ -497,6 +517,7 @@ const handleEmployeeClear = () => {
formData.value.newEmployeeName = '' formData.value.newEmployeeName = ''
formData.value.birthday = undefined formData.value.birthday = undefined
formData.value.height = undefined formData.value.height = undefined
formData.value.type = undefined
} }
} }
const handleRelatedEmployeeChange = (selectedId: string | number | undefined) => { const handleRelatedEmployeeChange = (selectedId: string | number | undefined) => {
@@ -712,6 +733,7 @@ const handleVisibleChange = (value: boolean) => {
const resetForm = () => { const resetForm = () => {
formData.value = { formData.value = {
personType: 1, personType: 1,
type: undefined,
employeeId: undefined, employeeId: undefined,
newEmployeeName: '', newEmployeeName: '',
relatedEmployeeId: undefined, relatedEmployeeId: undefined,
@@ -751,6 +773,7 @@ const handleSubmit = async () => {
const personObj: any = {} const personObj: any = {}
const isPatientEmployee = formData.value.personType === 1 const isPatientEmployee = formData.value.personType === 1
personObj.isEmployee = isPatientEmployee personObj.isEmployee = isPatientEmployee
personObj.type = isPatientEmployee ? formData.value.type : undefined
if (isPatientEmployee) { if (isPatientEmployee) {
if (formData.value.employeeId) { if (formData.value.employeeId) {
@@ -876,6 +899,7 @@ watch(() => props.visible, async (newVal) => {
formData.value.employeeId = person.id ?? undefined formData.value.employeeId = person.id ?? undefined
formData.value.birthday = person.birthday ?? undefined formData.value.birthday = person.birthday ?? undefined
formData.value.height = person.height ?? undefined formData.value.height = person.height ?? undefined
formData.value.type = person.type ?? undefined
} else { } else {
formData.value.relatedEmployeeId = person.employeeId ?? undefined formData.value.relatedEmployeeId = person.employeeId ?? undefined
formData.value.relativeId = person.id ?? undefined formData.value.relativeId = person.id ?? undefined
@@ -11,7 +11,11 @@
<!-- 原有:就诊人类型自定义插槽(不变) --> <!-- 原有:就诊人类型自定义插槽(不变) -->
<template #personType="scope"> <template #personType="scope">
<el-space> <el-space>
<span v-if="scope.row.personType === 1">公司员工</span> <div v-if="scope.row.personType === 1" class="flex items-center">
<span>公司员工</span>
<el-tag v-if="scope.row.type == 1" type="success" size="small" class="ml-2">领导</el-tag>
<el-tag v-if="scope.row.type == 2" type="info" size="small" class="ml-2">普通员工</el-tag>
</div>
<span v-else-if="scope.row.personType === 2"> <span v-else-if="scope.row.personType === 2">
员工家属,所属员工:{{ scope.row.employeeName || "未知" }} 员工家属,所属员工:{{ scope.row.employeeName || "未知" }}
</span> </span>
+549 -102
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="statistics-container"> <div class="statistics-container" :class="{ 'is-dark': isDarkMode }">
<!-- 顶部筛选栏 --> <!-- 顶部筛选栏 -->
<div class="header-filter"> <div class="header-filter">
<div class="filter-left"> <div class="filter-left">
@@ -17,6 +17,10 @@
{{ year }}年 {{ year }}年
</el-radio-button> </el-radio-button>
</el-radio-group> </el-radio-group>
<div class="mode-switch">
<el-switch v-model="isDarkMode" inline-prompt :active-icon="Moon" :inactive-icon="Sunny"
style="--el-switch-on-color: #334155; --el-switch-off-color: #fbbf24" />
</div>
</div> </div>
</div> </div>
@@ -93,24 +97,39 @@
<!-- 图表区域 --> <!-- 图表区域 -->
<div class="charts-section" v-loading="loading"> <div class="charts-section" v-loading="loading">
<!-- 月度趋势图 --> <div class="charts-grid">
<div class="chart-card"> <!-- 月度趋势图 -->
<div class="chart-header"> <div class="chart-card trend-card">
<div> <div class="chart-header">
<h3>{{ selectedYear }}月度支出趋势统计</h3> <div>
<p class="chart-desc">处方总费用与药材成本月度变化</p> <h3>月度收支流光趋势</h3>
<p class="chart-desc">处方总费用与药材成本月度变化</p>
</div>
<div class="legend">
<span class="legend-item">
<i class="legend-dot blue"></i> 处方花费
</span>
<span class="legend-item">
<i class="legend-dot amber"></i> 药材花费
</span>
</div>
</div> </div>
<div class="legend"> <div class="chart-content">
<span class="legend-item"> <div ref="trendChartRef" class="echart-container"></div>
<i class="legend-dot blue"></i> 处方花费
</span>
<span class="legend-item">
<i class="legend-dot amber"></i> 药材花费
</span>
</div> </div>
</div> </div>
<div class="chart-content">
<div ref="trendChartRef" class="echart-container"></div> <!-- 3D proportions chart -->
<div class="chart-card proportion-card">
<div class="chart-header">
<div>
<h3>服务对象占比</h3>
<p class="chart-desc">本年度员工与家属服务分布</p>
</div>
</div>
<div class="chart-content">
<div ref="proportionChartRef" class="echart-container"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -165,7 +184,7 @@
<script setup lang="ts" name="WeiBingJuStatistics"> <script setup lang="ts" name="WeiBingJuStatistics">
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'; import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
import { Calendar, Operation, Filter, Top, Bottom, DataAnalysis } from '@element-plus/icons-vue'; import { Calendar, Operation, Filter, Top, Bottom, DataAnalysis, Moon, Sunny } from '@element-plus/icons-vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import type { ECharts } from 'echarts'; import type { ECharts } from 'echarts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -179,6 +198,11 @@ const currentDate = ref(dayjs().format('YYYY-MM-DD'));
const trendChartRef = ref<HTMLElement>(); const trendChartRef = ref<HTMLElement>();
const trendChartInstance = ref<ECharts>(); const trendChartInstance = ref<ECharts>();
const proportionChartRef = ref<HTMLElement>();
const proportionChartInstance = ref<ECharts>();
// 深色模式显示切换
const isDarkMode = ref(false);
// 年度统计数据(来自后端) // 年度统计数据(来自后端)
const yearlyData = ref<YearlyStatisticsDto | null>(null); const yearlyData = ref<YearlyStatisticsDto | null>(null);
@@ -292,92 +316,239 @@ const statCards = computed(() => {
]; ];
}); });
// ─── 图表 ───────────────────────────────────────────────────── // ─── 图表配置 ─────────────────────────────────────────────────
const buildChartOption = (data: MonthlyDetailDto[]) => ({ const buildChartOption = (data: MonthlyDetailDto[]) => {
grid: { const isDark = isDarkMode.value;
top: 30, const xAxisData = data.map(item => item.month);
right: 30, const mainData = data.map(item => item.staffCost + item.familyCost);
bottom: 40, const subData = data.map(item => item.materialCost);
left: 70,
containLabel: true return {
}, backgroundColor: 'transparent',
tooltip: { grid: {
trigger: 'axis', top: 30,
backgroundColor: 'rgba(255, 255, 255, 0.95)', right: 30,
borderColor: '#e5e7eb', bottom: 40,
borderWidth: 1, left: 70,
textStyle: { color: '#1e293b' }, containLabel: true
axisPointer: {
type: 'cross',
crossStyle: { color: '#94a3b8' }
}
},
xAxis: {
type: 'category',
data: data.map(item => item.month),
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: '#94a3b8',
fontSize: 12,
interval: 0,
rotate: 0
}
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
lineStyle: { color: '#f1f5f9', type: 'dashed' }
}, },
axisLabel: { tooltip: {
color: '#94a3b8', trigger: 'axis',
fontSize: 12, backgroundColor: isDark ? 'rgba(15, 23, 42, 0.9)' : 'rgba(255, 255, 255, 0.95)',
formatter: (val: number) => `¥${(val / 1000).toFixed(0)}k` borderColor: isDark ? '#334155' : '#e5e7eb',
} borderWidth: 1,
}, textStyle: { color: isDark ? '#f1f5f9' : '#1e293b' },
series: [ axisPointer: {
{ type: 'cross',
name: '总处方花费', crossStyle: { color: '#94a3b8' }
type: 'line', }
data: data.map(item => item.staffCost + item.familyCost), },
smooth: true, xAxis: {
lineStyle: { width: 3, color: '#3b82f6' }, type: 'category',
areaStyle: { data: xAxisData,
color: { axisLine: { show: false },
type: 'linear', axisTick: { show: false },
x: 0, y: 0, x2: 0, y2: 1, axisLabel: {
colorStops: [ color: isDark ? '#64748b' : '#94a3b8',
{ offset: 0, color: 'rgba(59, 130, 246, 0.1)' }, fontSize: 12
{ offset: 1, color: 'rgba(59, 130, 246, 0)' } }
] },
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
lineStyle: {
color: isDark ? '#1e293b' : '#f1f5f9',
type: 'dashed'
} }
}, },
itemStyle: { color: '#3b82f6' } axisLabel: {
color: isDark ? '#64748b' : '#94a3b8',
fontSize: 12,
formatter: (val: number) => `¥${(val / 1000).toFixed(0)}k`
}
}, },
{ series: [
name: '药材花费', {
type: 'line', name: '总处方花费',
data: data.map(item => item.materialCost), type: 'line',
smooth: true, data: mainData,
lineStyle: { width: 2, color: '#f59e0b', type: 'dashed' }, smooth: true,
itemStyle: { color: '#f59e0b' } showSymbol: false,
} lineStyle: { width: 4, color: '#3b82f6', shadowBlur: 10, shadowColor: 'rgba(59, 130, 246, 0.5)' },
] areaStyle: {
}); color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(59, 130, 246, 0.4)' },
const initTrendChart = () => { { offset: 0.8, color: 'rgba(59, 130, 246, 0.05)' },
if (!trendChartRef.value) return; { offset: 1, color: 'rgba(59, 130, 246, 0)' }
if (!trendChartInstance.value) { ])
trendChartInstance.value = echarts.init(trendChartRef.value); },
} itemStyle: { color: '#3b82f6' }
// 初始化时用空数据占位 },
trendChartInstance.value.setOption(buildChartOption([])); {
name: '药材花费',
type: 'line',
data: subData,
smooth: true,
showSymbol: false,
lineStyle: { width: 3, color: '#f59e0b', type: 'dashed', shadowBlur: 8, shadowColor: 'rgba(245, 158, 11, 0.5)' },
itemStyle: { color: '#f59e0b' }
},
// 流光特效层(处方线)
{
type: 'lines',
coordinateSystem: 'cartesian2d',
polyline: true,
data: xAxisData.length > 0 ? [{
coords: xAxisData.map((month, i) => [i, mainData[i]])
}] : [],
effect: {
show: true,
period: 4,
trailLength: 0.5,
color: '#60a5fa',
symbolSize: 6,
shadowBlur: 12,
shadowColor: '#60a5fa'
},
lineStyle: { width: 0, opacity: 0 },
zIndex: 10
},
// 流光特效层(药材料线)
{
type: 'lines',
coordinateSystem: 'cartesian2d',
polyline: true,
data: xAxisData.length > 0 ? [{
coords: xAxisData.map((month, i) => [i, subData[i]])
}] : [],
effect: {
show: true,
period: 5,
trailLength: 0.5,
color: '#fbbf24',
symbolSize: 5,
shadowBlur: 10,
shadowColor: '#fbbf24'
},
lineStyle: { width: 0, opacity: 0 },
zIndex: 9
}
]
};
}; };
const updateChart = (data: MonthlyDetailDto[]) => { const buildRingChartOption = (data: MonthlyDetailDto[]) => {
const isDark = isDarkMode.value;
const totalStaff = data.reduce((acc, cur) => acc + cur.staffQty, 0);
const totalFamily = data.reduce((acc, cur) => acc + cur.familyQty, 0);
return {
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
backgroundColor: isDark ? 'rgba(15, 23, 42, 0.9)' : 'rgba(255, 255, 255, 0.95)',
textStyle: { color: isDark ? '#f1f5f9' : '#1e293b' }
},
legend: {
bottom: '5%',
left: 'center',
textStyle: { color: isDark ? '#94a3b8' : '#64748b' },
itemWidth: 10,
itemHeight: 10
},
series: [
// 外发光光环
{
type: 'pie',
radius: ['70%', '72%'],
center: ['50%', '45%'],
silent: true,
label: { show: false },
itemStyle: {
color: isDark ? 'rgba(51, 65, 85, 0.5)' : 'rgba(226, 232, 240, 0.5)'
},
data: [{ value: 1 }]
},
// 背景阴影层,模拟3D感
{
type: 'pie',
radius: ['45%', '65%'],
center: ['50%', '45%'],
silent: true,
label: { show: false },
itemStyle: {
color: isDark ? '#1e293b' : '#f1f5f9',
shadowBlur: 30,
shadowColor: isDark ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.1)'
},
data: [{ value: 1 }]
},
{
name: '服务量占比',
type: 'pie',
radius: ['48%', '68%'],
center: ['50%', '45%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: isDark ? '#1e293b' : '#fff',
borderWidth: 2
},
label: {
show: true,
position: 'center',
formatter: () => '服务对象',
color: isDark ? '#cbd5e1' : '#1e293b',
fontSize: 16,
fontWeight: 'bold'
},
data: [
{
value: totalStaff,
name: '内部员工',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#2563eb' },
{ offset: 1, color: '#60a5fa' }
]),
shadowBlur: 15,
shadowColor: 'rgba(59, 130, 246, 0.4)'
}
},
{
value: totalFamily,
name: '家属成员',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#059669' },
{ offset: 1, color: '#34d399' }
]),
shadowBlur: 15,
shadowColor: 'rgba(16, 185, 129, 0.4)'
}
}
]
}
]
};
};
const initCharts = () => {
if (trendChartRef.value) {
trendChartInstance.value = echarts.init(trendChartRef.value);
trendChartInstance.value.setOption(buildChartOption([]));
}
if (proportionChartRef.value) {
proportionChartInstance.value = echarts.init(proportionChartRef.value);
proportionChartInstance.value.setOption(buildRingChartOption([]));
}
};
const updateCharts = (data: MonthlyDetailDto[]) => {
trendChartInstance.value?.setOption(buildChartOption(data)); trendChartInstance.value?.setOption(buildChartOption(data));
proportionChartInstance.value?.setOption(buildRingChartOption(data));
}; };
// ─── 数据加载 ───────────────────────────────────────────────── // ─── 数据加载 ─────────────────────────────────────────────────
@@ -403,7 +574,7 @@ const loadData = async () => {
]); ]);
yearlyData.value = yearlyRes.data; yearlyData.value = yearlyRes.data;
tableData.value = monthlyRes.data; tableData.value = monthlyRes.data;
updateChart(monthlyRes.data); updateCharts(monthlyRes.data);
} catch (e) { } catch (e) {
console.error('统计数据加载失败', e); console.error('统计数据加载失败', e);
} finally { } finally {
@@ -411,19 +582,25 @@ const loadData = async () => {
} }
}; };
// ─── 年份切换 ───────────────────────────────────────────────── // ─── 状态监听 ─────────────────────────────────────────────────
watch(selectedYear, () => { watch(selectedYear, () => {
loadData(); loadData();
}); });
watch(isDarkMode, () => {
// 切换深色模式时重绘图表
updateCharts(tableData.value);
});
// ─── 窗口 Resize ────────────────────────────────────────────── // ─── 窗口 Resize ──────────────────────────────────────────────
const handleResize = () => { const handleResize = () => {
trendChartInstance.value?.resize(); trendChartInstance.value?.resize();
proportionChartInstance.value?.resize();
}; };
// ─── 生命周期 ───────────────────────────────────────────────── // ─── 生命周期 ─────────────────────────────────────────────────
onMounted(async () => { onMounted(async () => {
initTrendChart(); initCharts();
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
// 并发加载:总览(不依赖年份)+ 年度数据 // 并发加载:总览(不依赖年份)+ 年度数据
await Promise.all([loadOverview(), loadData()]); await Promise.all([loadOverview(), loadData()]);
@@ -431,6 +608,7 @@ onMounted(async () => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
trendChartInstance.value?.dispose(); trendChartInstance.value?.dispose();
proportionChartInstance.value?.dispose();
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
}); });
</script> </script>
@@ -440,6 +618,17 @@ onBeforeUnmount(() => {
padding: 20px; padding: 20px;
background: #f8fafc; background: #f8fafc;
min-height: calc(100vh - 100px); min-height: calc(100vh - 100px);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
&.is-dark {
background-color: #0f172a;
// 科技感网格背景
background-image:
linear-gradient(rgba(51, 65, 85, 0.2) 1px, transparent 1px),
linear-gradient(90deg, rgba(51, 65, 85, 0.2) 1px, transparent 1px);
background-size: 30px 30px;
color: #f1f5f9;
}
} }
// 顶部筛选栏 // 顶部筛选栏
@@ -454,6 +643,8 @@ onBeforeUnmount(() => {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px; gap: 16px;
border: 1px solid transparent;
transition: all 0.3s;
@media (max-width: 768px) { @media (max-width: 768px) {
padding: 12px 16px; padding: 12px 16px;
@@ -519,6 +710,40 @@ onBeforeUnmount(() => {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
} }
} }
.mode-switch {
padding-left: 8px;
border-left: 1px solid #e2e8f0;
}
}
}
// 深色模式下的顶部筛选
.is-dark .header-filter {
background: #1e293b;
border-color: #334155;
.filter-left .title-block {
h1 {
color: #f8fafc;
}
.subtitle {
color: #94a3b8;
}
}
.year-selector :deep(.el-radio-button__inner) {
color: #94a3b8;
}
.year-selector :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
background: #334155;
color: #10b981;
}
.mode-switch {
border-color: #334155;
} }
} }
@@ -573,8 +798,39 @@ onBeforeUnmount(() => {
border: 1px solid #f1f5f9; border: 1px solid #f1f5f9;
transition: all 0.3s; transition: all 0.3s;
// 动画配置
position: relative;
overflow: hidden;
opacity: 0;
animation: floatUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
// 级联渲染延迟
@for $i from 1 through 8 {
&:nth-child(#{$i}) {
animation-delay: #{$i * 0.1}s;
}
}
// 扫光特效伪元素
&::after {
content: '';
position: absolute;
top: 0;
left: -150%;
width: 50%;
height: 100%;
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.4) 50%, rgba(255, 255, 255, 0) 100%);
transform: skewX(-20deg);
z-index: 10;
}
&:hover { &:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
transform: translateY(-4px);
&::after {
animation: shine 1s ease-in-out;
}
} }
.card-header { .card-header {
@@ -677,22 +933,81 @@ onBeforeUnmount(() => {
} }
} }
// 深色模式下的卡片
.is-dark {
.overview-section .section-header h2 {
color: #f1f5f9;
}
.stat-card {
background: #1e293b;
border-color: #334155;
.card-content {
.card-title {
color: #94a3b8;
}
.card-value h3 {
color: #f8fafc;
}
.card-sub {
color: #64748b;
}
}
&:hover {
border-color: #3b82f6;
box-shadow: 0 8px 30px rgba(59, 130, 246, 0.2);
transform: translateY(-4px);
}
&::after {
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0) 100%);
}
}
}
// 图表区域 // 图表区域
.charts-section { .charts-section {
margin-bottom: 32px; margin-bottom: 32px;
.charts-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
@media (max-width: 1200px) {
grid-template-columns: 1fr;
}
}
.chart-card { .chart-card {
background: white; background: white;
border-radius: 16px; border-radius: 16px;
padding: 24px; padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
border: 1px solid #f1f5f9; border: 1px solid #f1f5f9;
transition: all 0.3s;
opacity: 0;
animation: floatUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) 0.3s forwards;
&:hover {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
&.proportion-card {
display: flex;
flex-direction: column;
}
h3 { h3 {
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
color: #1e293b; color: #1e293b;
margin: 0 0 24px; margin: 0;
} }
.chart-header { .chart-header {
@@ -759,13 +1074,34 @@ onBeforeUnmount(() => {
} }
} }
// 表格区域 // 深色模式下的图表
.is-dark .chart-card {
background: #1e293b;
border-color: #334155;
h3 {
color: #f8fafc;
}
.chart-desc {
color: #64748b;
}
.legend .legend-item {
color: #94a3b8;
}
}
.table-section { .table-section {
background: white; background: white;
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
border: 1px solid #f1f5f9; border: 1px solid #f1f5f9;
transition: all 0.3s;
opacity: 0;
animation: floatUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) 0.5s forwards;
.table-header { .table-header {
padding: 20px 24px; padding: 20px 24px;
@@ -835,8 +1171,19 @@ onBeforeUnmount(() => {
} }
:deep(.el-table__row) { :deep(.el-table__row) {
opacity: 0;
animation: fadeUpIn 0.5s ease-out forwards;
@for $i from 1 through 20 {
&:nth-child(#{$i}) {
animation-delay: #{$i * 0.05 + 0.5}s;
}
}
&:hover { &:hover {
background: rgba(248, 250, 252, 0.8); background: rgba(248, 250, 252, 0.8);
transform: scale(1.002);
transition: transform 0.2s, background 0.2s;
} }
} }
@@ -877,5 +1224,105 @@ onBeforeUnmount(() => {
} }
} }
// 深色模式下的表格
.is-dark .table-section {
background: #1e293b;
border-color: #334155;
.table-header {
border-color: #334155;
h3 {
color: #f8fafc;
}
}
.statistics-table {
background: transparent;
:deep(.el-table__header th) {
background: #0f172a;
color: #94a3b8;
}
:deep(.el-table__row) {
background: #1e293b;
color: #cbd5e1;
&:hover {
transform: scale(1.002);
background: #334155 !important;
}
&:hover>td {
background: transparent !important;
}
td {
border-bottom-color: #334155;
}
}
.staff-cost {
color: #60a5fa;
}
.family-cost {
color: #34d399;
}
.total-cost {
color: #f1f5f9;
}
.prescription-tag {
background: rgba(59, 130, 246, 0.1);
color: #60a5fa;
border-color: rgba(59, 130, 246, 0.3);
}
.material-tag {
background: rgba(245, 158, 11, 0.1);
color: #fbbf24;
border-color: rgba(245, 158, 11, 0.3);
}
}
}
// 动画关键帧定义
@keyframes floatUp {
0% {
opacity: 0;
transform: translateY(30px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeUpIn {
0% {
opacity: 0;
transform: translateY(15px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shine {
0% {
left: -150%;
}
100% {
left: 150%;
}
}
// 总数据扆览板块——卡片数与年度一致(4张),不需要覆盖列数 // 总数据扆览板块——卡片数与年度一致(4张),不需要覆盖列数
// .total-overview-section 展示逻辑已通过 section-header 标题区分</style> // .total-overview-section 展示逻辑已通过 section-header 标题区分</style>