员工类型新增领导与普通员工区分
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user