员工类型新增领导与普通员工区分
This commit is contained in:
@@ -28,7 +28,6 @@
|
||||
:disabled="isViewMode">
|
||||
<el-option v-for="item in employeeOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<!-- 新增/编辑模式显示"新增员工"按钮 -->
|
||||
<el-button v-if="!isViewMode" type="primary" size="small" @click="handleOpenAddEmployeeDialog">
|
||||
新增员工
|
||||
</el-button>
|
||||
@@ -36,6 +35,13 @@
|
||||
<el-input v-if="!showEmployeeSelect && !formData.employeeId" v-model="formData.newEmployeeName"
|
||||
placeholder="请输入员工姓名" class="mt-2" :disabled="isViewMode" />
|
||||
</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>
|
||||
|
||||
<!-- 员工家属模式 -->
|
||||
@@ -239,7 +245,7 @@ import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
import { prescriptionApi } from '@/api'
|
||||
import { medicinalApi } from '@/api/modules/biz/weibingju/medicinal'
|
||||
import { medicinalApi } from '@/api/modules/biz/WeiBingJu/medicinal'
|
||||
|
||||
// 接口定义
|
||||
interface Employee {
|
||||
@@ -247,6 +253,7 @@ interface Employee {
|
||||
name: string;
|
||||
birthday?: string;
|
||||
height?: number;
|
||||
type?: number;
|
||||
}
|
||||
interface Relative {
|
||||
id: string | number;
|
||||
@@ -294,6 +301,7 @@ const isEdit = computed(() => props.isEdit)
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
personType: 1,
|
||||
type: undefined as number | undefined,
|
||||
employeeId: undefined as string | number | undefined,
|
||||
newEmployeeName: '',
|
||||
relatedEmployeeId: undefined as string | number | undefined,
|
||||
@@ -313,6 +321,17 @@ const formData = ref({
|
||||
// 表单校验规则
|
||||
const formRules: FormRules = {
|
||||
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: [{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
@@ -440,7 +459,7 @@ const loadRelatives = async (employeeId: number | string) => {
|
||||
showRelativeSelect.value = false
|
||||
ElMessage.info('该员工为本地新增,暂无家属数据,请手动输入家属姓名')
|
||||
} 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 : []
|
||||
showRelativeSelect.value = relativeOptions.value.length > 0
|
||||
}
|
||||
@@ -489,6 +508,7 @@ const handleEmployeeChange = (selectedId: string | number | undefined) => {
|
||||
if (selectedEmployee) {
|
||||
formData.value.birthday = selectedEmployee.birthday
|
||||
formData.value.height = selectedEmployee.height
|
||||
formData.value.type = selectedEmployee.type
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,6 +517,7 @@ const handleEmployeeClear = () => {
|
||||
formData.value.newEmployeeName = ''
|
||||
formData.value.birthday = undefined
|
||||
formData.value.height = undefined
|
||||
formData.value.type = undefined
|
||||
}
|
||||
}
|
||||
const handleRelatedEmployeeChange = (selectedId: string | number | undefined) => {
|
||||
@@ -712,6 +733,7 @@ const handleVisibleChange = (value: boolean) => {
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
personType: 1,
|
||||
type: undefined,
|
||||
employeeId: undefined,
|
||||
newEmployeeName: '',
|
||||
relatedEmployeeId: undefined,
|
||||
@@ -751,6 +773,7 @@ const handleSubmit = async () => {
|
||||
const personObj: any = {}
|
||||
const isPatientEmployee = formData.value.personType === 1
|
||||
personObj.isEmployee = isPatientEmployee
|
||||
personObj.type = isPatientEmployee ? formData.value.type : undefined
|
||||
|
||||
if (isPatientEmployee) {
|
||||
if (formData.value.employeeId) {
|
||||
@@ -876,6 +899,7 @@ watch(() => props.visible, async (newVal) => {
|
||||
formData.value.employeeId = person.id ?? undefined
|
||||
formData.value.birthday = person.birthday ?? undefined
|
||||
formData.value.height = person.height ?? undefined
|
||||
formData.value.type = person.type ?? undefined
|
||||
} else {
|
||||
formData.value.relatedEmployeeId = person.employeeId ?? undefined
|
||||
formData.value.relativeId = person.id ?? undefined
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
<!-- 原有:就诊人类型自定义插槽(不变) -->
|
||||
<template #personType="scope">
|
||||
<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">
|
||||
员工家属,所属员工:{{ scope.row.employeeName || "未知" }}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="statistics-container">
|
||||
<div class="statistics-container" :class="{ 'is-dark': isDarkMode }">
|
||||
<!-- 顶部筛选栏 -->
|
||||
<div class="header-filter">
|
||||
<div class="filter-left">
|
||||
@@ -17,6 +17,10 @@
|
||||
{{ year }}年
|
||||
</el-radio-button>
|
||||
</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>
|
||||
|
||||
@@ -93,24 +97,39 @@
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-section" v-loading="loading">
|
||||
<!-- 月度趋势图 -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<div>
|
||||
<h3>{{ selectedYear }}月度支出趋势统计</h3>
|
||||
<p class="chart-desc">处方总费用与药材成本月度变化</p>
|
||||
<div class="charts-grid">
|
||||
<!-- 月度趋势图 -->
|
||||
<div class="chart-card trend-card">
|
||||
<div class="chart-header">
|
||||
<div>
|
||||
<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 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 class="chart-content">
|
||||
<div ref="trendChartRef" class="echart-container"></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>
|
||||
@@ -165,7 +184,7 @@
|
||||
|
||||
<script setup lang="ts" name="WeiBingJuStatistics">
|
||||
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 type { ECharts } from 'echarts';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -179,6 +198,11 @@ const currentDate = ref(dayjs().format('YYYY-MM-DD'));
|
||||
|
||||
const trendChartRef = ref<HTMLElement>();
|
||||
const trendChartInstance = ref<ECharts>();
|
||||
const proportionChartRef = ref<HTMLElement>();
|
||||
const proportionChartInstance = ref<ECharts>();
|
||||
|
||||
// 深色模式显示切换
|
||||
const isDarkMode = ref(false);
|
||||
|
||||
// 年度统计数据(来自后端)
|
||||
const yearlyData = ref<YearlyStatisticsDto | null>(null);
|
||||
@@ -292,92 +316,239 @@ const statCards = computed(() => {
|
||||
];
|
||||
});
|
||||
|
||||
// ─── 图表 ─────────────────────────────────────────────────────
|
||||
const buildChartOption = (data: MonthlyDetailDto[]) => ({
|
||||
grid: {
|
||||
top: 30,
|
||||
right: 30,
|
||||
bottom: 40,
|
||||
left: 70,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: '#e5e7eb',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: '#1e293b' },
|
||||
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' }
|
||||
// ─── 图表配置 ─────────────────────────────────────────────────
|
||||
const buildChartOption = (data: MonthlyDetailDto[]) => {
|
||||
const isDark = isDarkMode.value;
|
||||
const xAxisData = data.map(item => item.month);
|
||||
const mainData = data.map(item => item.staffCost + item.familyCost);
|
||||
const subData = data.map(item => item.materialCost);
|
||||
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
top: 30,
|
||||
right: 30,
|
||||
bottom: 40,
|
||||
left: 70,
|
||||
containLabel: true
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#94a3b8',
|
||||
fontSize: 12,
|
||||
formatter: (val: number) => `¥${(val / 1000).toFixed(0)}k`
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '总处方花费',
|
||||
type: 'line',
|
||||
data: data.map(item => item.staffCost + item.familyCost),
|
||||
smooth: true,
|
||||
lineStyle: { width: 3, color: '#3b82f6' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(59, 130, 246, 0.1)' },
|
||||
{ offset: 1, color: 'rgba(59, 130, 246, 0)' }
|
||||
]
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: isDark ? 'rgba(15, 23, 42, 0.9)' : 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: isDark ? '#334155' : '#e5e7eb',
|
||||
borderWidth: 1,
|
||||
textStyle: { color: isDark ? '#f1f5f9' : '#1e293b' },
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
crossStyle: { color: '#94a3b8' }
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: isDark ? '#64748b' : '#94a3b8',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
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`
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '药材花费',
|
||||
type: 'line',
|
||||
data: data.map(item => item.materialCost),
|
||||
smooth: true,
|
||||
lineStyle: { width: 2, color: '#f59e0b', type: 'dashed' },
|
||||
itemStyle: { color: '#f59e0b' }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const initTrendChart = () => {
|
||||
if (!trendChartRef.value) return;
|
||||
if (!trendChartInstance.value) {
|
||||
trendChartInstance.value = echarts.init(trendChartRef.value);
|
||||
}
|
||||
// 初始化时用空数据占位
|
||||
trendChartInstance.value.setOption(buildChartOption([]));
|
||||
series: [
|
||||
{
|
||||
name: '总处方花费',
|
||||
type: 'line',
|
||||
data: mainData,
|
||||
smooth: true,
|
||||
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)' },
|
||||
{ offset: 0.8, color: 'rgba(59, 130, 246, 0.05)' },
|
||||
{ offset: 1, color: 'rgba(59, 130, 246, 0)' }
|
||||
])
|
||||
},
|
||||
itemStyle: { color: '#3b82f6' }
|
||||
},
|
||||
{
|
||||
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));
|
||||
proportionChartInstance.value?.setOption(buildRingChartOption(data));
|
||||
};
|
||||
|
||||
// ─── 数据加载 ─────────────────────────────────────────────────
|
||||
@@ -403,7 +574,7 @@ const loadData = async () => {
|
||||
]);
|
||||
yearlyData.value = yearlyRes.data;
|
||||
tableData.value = monthlyRes.data;
|
||||
updateChart(monthlyRes.data);
|
||||
updateCharts(monthlyRes.data);
|
||||
} catch (e) {
|
||||
console.error('统计数据加载失败', e);
|
||||
} finally {
|
||||
@@ -411,19 +582,25 @@ const loadData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── 年份切换 ─────────────────────────────────────────────────
|
||||
// ─── 状态监听 ─────────────────────────────────────────────────
|
||||
watch(selectedYear, () => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
watch(isDarkMode, () => {
|
||||
// 切换深色模式时重绘图表
|
||||
updateCharts(tableData.value);
|
||||
});
|
||||
|
||||
// ─── 窗口 Resize ──────────────────────────────────────────────
|
||||
const handleResize = () => {
|
||||
trendChartInstance.value?.resize();
|
||||
proportionChartInstance.value?.resize();
|
||||
};
|
||||
|
||||
// ─── 生命周期 ─────────────────────────────────────────────────
|
||||
onMounted(async () => {
|
||||
initTrendChart();
|
||||
initCharts();
|
||||
window.addEventListener('resize', handleResize);
|
||||
// 并发加载:总览(不依赖年份)+ 年度数据
|
||||
await Promise.all([loadOverview(), loadData()]);
|
||||
@@ -431,6 +608,7 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
trendChartInstance.value?.dispose();
|
||||
proportionChartInstance.value?.dispose();
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
@@ -440,6 +618,17 @@ onBeforeUnmount(() => {
|
||||
padding: 20px;
|
||||
background: #f8fafc;
|
||||
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);
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.3s;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px 16px;
|
||||
@@ -519,6 +710,40 @@ onBeforeUnmount(() => {
|
||||
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;
|
||||
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 {
|
||||
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 {
|
||||
@@ -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 {
|
||||
margin-bottom: 32px;
|
||||
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
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 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0 0 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
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 {
|
||||
padding: 20px 24px;
|
||||
@@ -835,8 +1171,19 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
: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 {
|
||||
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张),不需要覆盖列数
|
||||
// .total-overview-section 展示逻辑已通过 section-header 标题区分</style>
|
||||
|
||||
Reference in New Issue
Block a user