fix: 修复前端登录体验和API调用问题

- 修复路由守卫:未登录时直接跳转,不显示提示信息
- 修复API拦截器:401错误直接跳转,无需确认
- 移除不必要的ElMessageBox确认框
- 优化Token过期处理逻辑
- 修复文件管理API引入路径和URL前缀
- 修复调拨/回收管理API端点不匹配问题
- 修复通知管理API方法不匹配问题
- 统一系统配置API路径为单数形式

影响文件:
- src/router/index.ts
- src/api/request.ts
- src/api/file.ts
- src/api/index.ts

测试状态:
- 前端构建通过
- 所有API路径已验证
- 登录流程测试通过

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-01-25 00:26:33 +08:00
commit e48975f9d5
151 changed files with 39477 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
<!--
柱状图组件
支持横向/纵向堆叠分组柱状图
-->
<template>
<BaseChart
:option="chartOption"
:height="height"
:loading="loading"
@ready="handleReady"
@click="handleClick"
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import BaseChart from './BaseChart.vue'
import { barChartOption, echartsTheme, formatNumber } from '@/utils/echarts'
import type { BarChartConfig } from '@/types/charts'
/** Props */
interface Props {
data: Array<{ name: string; value: number; [key: string]: any }>
title?: string
type?: 'vertical' | 'horizontal'
stacked?: boolean
grouped?: boolean
xAxisLabel?: string
yAxisLabel?: string
height?: string
showDataZoom?: boolean
}
const props = withDefaults(defineProps<Props>(), {
data: () => [],
title: '',
type: 'vertical',
stacked: false,
grouped: false,
xAxisLabel: '',
yAxisLabel: '',
height: '400px',
showDataZoom: false,
})
/** Emits */
interface Emits {
(e: 'ready', chart: any): void
(e: 'click', item: any): void
}
const emit = defineEmits<Emits>()
/** 加载状态 */
const loading = ref(false)
/** 图表配置 */
const chartOption = computed(() => {
const categories = props.data.map((item) => item.name)
const values = props.data.map((item) => item.value)
const isHorizontal = props.type === 'horizontal'
return {
...barChartOption,
title: {
...barChartOption.title,
text: props.title,
},
tooltip: {
...barChartOption.tooltip,
formatter: (params: any) => {
const value = params.value
return `${params.name}<br/>${params.seriesName}: ${formatNumber(value)}`
},
},
xAxis: isHorizontal
? {
...barChartOption.yAxis,
name: props.xAxisLabel,
nameTextStyle: {
padding: [0, 0, 0, 10],
},
}
: {
...barChartOption.xAxis,
data: categories,
name: props.xAxisLabel,
nameTextStyle: {
padding: [0, 0, 0, 10],
},
},
yAxis: isHorizontal
? {
...barChartOption.xAxis,
data: categories,
name: props.yAxisLabel,
nameTextStyle: {
padding: [0, 0, 0, 10],
},
}
: {
...barChartOption.yAxis,
name: props.yAxisLabel,
nameTextStyle: {
padding: [0, 10, 0, 0],
},
},
dataZoom: props.showDataZoom
? [
{
type: 'slider',
show: true,
start: 0,
end: 100,
[isHorizontal ? 'yAxisIndex' : 'xAxisIndex']: 0,
},
]
: undefined,
series: [
{
type: 'bar',
name: props.yAxisLabel || '数值',
data: isHorizontal ? values : values,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: echartsTheme.color[0] },
{ offset: 1, color: echartsTheme.color[1] },
]),
borderRadius: isHorizontal ? [0, 4, 4, 0] : [4, 4, 0, 0],
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: echartsTheme.color[4] },
{ offset: 1, color: echartsTheme.color[5] },
]),
},
},
label: {
show: true,
position: isHorizontal ? 'right' : 'top',
formatter: (params: any) => formatNumber(params.value),
color: echartsTheme.textColor2,
fontSize: 11,
},
barMaxWidth: 60,
},
],
}
})
/** 处理图表就绪 */
const handleReady = (chart: any) => {
emit('ready', chart)
}
/** 处理点击事件 */
const handleClick = (params: any) => {
const item = props.data.find((d) => d.name === params.name)
if (item) {
emit('click', { ...item, ...params })
}
}
/** 暴露方法 */
defineExpose({
loading,
})
</script>

View File

@@ -0,0 +1,114 @@
<!--
基础图表组件
封装 ECharts 的基本功能所有图表组件的父类
-->
<template>
<div
ref="chartRef"
class="base-chart"
:style="{ height: height, width: '100%' }"
></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, nextTick, computed } from 'vue'
import { useECharts } from '@/composables/useECharts'
import type { EChartOption } from 'echarts'
/** Props */
interface Props {
option: EChartOption
height?: string
autoResize?: boolean
loading?: boolean
theme?: string | object
}
const props = withDefaults(defineProps<Props>(), {
height: '400px',
autoResize: true,
loading: false,
theme: undefined,
})
/** Emits */
interface Emits {
(e: 'ready', chart: any): void
(e: 'click', params: any): void
}
const emit = defineEmits<Emits>()
/** 图表容器引用 */
const chartRef = ref<HTMLElement | null>(null)
/** 使用 ECharts Composable */
const { chart, isReady, initChart, setOption, showLoading, hideLoading, resize, dispose, on, off } = useECharts(
chartRef,
props.theme
)
/** 监听配置变化 */
watch(
() => props.option,
(newOption) => {
if (isReady.value) {
setOption(newOption, true)
}
},
{ deep: true }
)
/** 监听加载状态 */
watch(
() => props.loading,
(loading) => {
if (loading) {
showLoading()
} else {
hideLoading()
}
},
{ immediate: true }
)
/** 监听图表就绪 */
watch(isReady, (ready) => {
if (ready && chart.value) {
emit('ready', chart.value)
// 绑定点击事件
on('click', (params) => {
emit('click', params)
})
}
})
/** 初始化 */
onMounted(async () => {
await nextTick()
if (chartRef.value) {
initChart()
}
})
/** 清理 */
onBeforeUnmount(() => {
dispose()
})
/** 暴露方法 */
defineExpose({
chart,
resize,
refresh: () => setOption(props.option, true),
})
</script>
<style scoped lang="scss">
.base-chart {
width: 100%;
min-height: 200px;
}
</style>

View File

@@ -0,0 +1,109 @@
<!--
漏斗图组件
用于展示流程转化率等
-->
<template>
<BaseChart
:option="chartOption"
:height="height"
:loading="loading"
@ready="handleReady"
@click="handleClick"
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import BaseChart from './BaseChart.vue'
import { funnelChartOption, echartsTheme, formatNumber } from '@/utils/echarts'
import type { FunnelChartConfig } from '@/types/charts'
/** Props */
interface Props {
data: Array<{ name: string; value: number; [key: string]: any }>
title?: string
height?: string
sort?: 'descending' | 'ascending' | 'none'
}
const props = withDefaults(defineProps<Props>(), {
data: () => [],
title: '',
height: '400px',
sort: 'descending',
})
/** Emits */
interface Emits {
(e: 'ready', chart: any): void
(e: 'click', item: any): void
}
const emit = defineEmits<Emits>()
/** 加载状态 */
const loading = ref(false)
/** 图表配置 */
const chartOption = computed(() => {
// 计算总数
const total = props.data.reduce((sum, item) => sum + item.value, 0)
// 处理数据
const chartData = props.data.map((item, index) => ({
name: item.name,
value: item.value,
itemStyle: {
color: echartsTheme.color[index % echartsTheme.color.length],
},
}))
return {
...funnelChartOption,
title: {
...funnelChartOption.title,
text: props.title,
},
tooltip: {
...funnelChartOption.tooltip,
formatter: (params: any) => {
const percentage = total > 0 ? ((params.value / total) * 100).toFixed(1) : 0
return `${params.name}<br/>数量: ${formatNumber(params.value)}<br/>占比: ${percentage}%`
},
},
series: [
{
...funnelChartOption.series![0],
sort: props.sort,
data: chartData,
label: {
...funnelChartOption.series![0].label,
formatter: (params: any) => {
const percentage = total > 0 ? ((params.value / total) * 100).toFixed(1) : 0
return `${params.name}\n${formatNumber(params.value)} (${percentage}%)`
},
},
},
],
}
})
/** 处理图表就绪 */
const handleReady = (chart: any) => {
emit('ready', chart)
}
/** 处理点击事件 */
const handleClick = (params: any) => {
const item = props.data.find((d) => d.name === params.name)
if (item) {
emit('click', { ...item, ...params })
}
}
/** 暴露方法 */
defineExpose({
loading,
})
</script>

View File

@@ -0,0 +1,114 @@
<!--
仪表盘组件
用于展示百分比利用率等指标
-->
<template>
<BaseChart
:option="chartOption"
:height="height"
:loading="loading"
@ready="handleReady"
/>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import BaseChart from './BaseChart.vue'
import { gaugeChartOption, echartsTheme } from '@/utils/echarts'
import type { GaugeChartConfig } from '@/types/charts'
/** Props */
interface Props {
value: number
min?: number
max?: number
title?: string
unit?: string
height?: string
color?: string[]
showDetail?: boolean
}
const props = withDefaults(defineProps<Props>(), {
value: 0,
min: 0,
max: 100,
title: '',
unit: '%',
height: '300px',
color: () => [echartsTheme.color[5], echartsTheme.color[4], echartsTheme.color[6]],
showDetail: true,
})
/** Emits */
interface Emits {
(e: 'ready', chart: any): void
}
const emit = defineEmits<Emits>()
/** 加载状态 */
const loading = ref(false)
/** 图表配置 */
const chartOption = computed(() => {
// 计算颜色分段
const splitNumber = 10
const step = (props.max - props.min) / splitNumber
return {
...gaugeChartOption,
title: {
...gaugeChartOption.title,
text: props.title,
},
series: [
{
...gaugeChartOption.series![0],
min: props.min,
max: props.max,
splitNumber,
axisLine: {
...gaugeChartOption.series![0].axisLine,
lineStyle: {
width: 18,
color: props.color.map((c, i) => [
(props.min + step * (i + 1)) / props.max,
c,
]),
},
},
detail: {
...gaugeChartOption.series![0].detail,
show: props.showDetail,
valueAnimation: true,
formatter: (value: number) => {
return props.unit === '%' ? `${value.toFixed(1)}%` : `${value.toFixed(1)}${props.unit}`
},
},
data: [
{
value: props.value,
},
],
title: {
offsetCenter: [0, '90%'],
fontSize: 14,
color: echartsTheme.textColor2,
},
},
],
}
})
/** 处理图表就绪 */
const handleReady = (chart: any) => {
emit('ready', chart)
}
/** 暴露方法 */
defineExpose({
loading,
})
</script>

View File

@@ -0,0 +1,225 @@
<!--
折线图组件
支持多条折线面积图平滑曲线
-->
<template>
<BaseChart
:option="chartOption"
:height="height"
:loading="loading"
@ready="handleReady"
@click="handleClick"
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import BaseChart from './BaseChart.vue'
import { lineChartOption, echartsTheme, formatNumber } from '@/utils/echarts'
import type { LineChartConfig } from '@/types/charts'
/** Props */
interface Props {
data: Array<{ name: string; value: number; [key: string]: any }>
series?: Array<{ name: string; data: number[]; color?: string }>
title?: string
area?: boolean
smooth?: boolean
xAxisLabel?: string
yAxisLabel?: string
height?: string
showDataZoom?: boolean
}
const props = withDefaults(defineProps<Props>(), {
data: () => [],
series: undefined,
title: '',
area: false,
smooth: true,
xAxisLabel: '',
yAxisLabel: '',
height: '400px',
showDataZoom: false,
})
/** Emits */
interface Emits {
(e: 'ready', chart: any): void
(e: 'click', item: any): void
}
const emit = defineEmits<Emits>()
/** 加载状态 */
const loading = ref(false)
/** 图表配置 */
const chartOption = computed(() => {
// 如果提供了系列数据,使用系列数据
if (props.series && props.series.length > 0) {
const categories = props.data.map((item) => item.name)
return {
...lineChartOption,
title: {
...lineChartOption.title,
text: props.title,
},
tooltip: {
...lineChartOption.tooltip,
formatter: (params: any) => {
if (Array.isArray(params)) {
let result = `${params[0].name}<br/>`
params.forEach((param: any) => {
result += `${param.marker} ${param.seriesName}: ${formatNumber(param.value)}<br/>`
})
return result
}
return `${params.name}<br/>${params.seriesName}: ${formatNumber(params.value)}`
},
},
legend: {
...lineChartOption.legend,
data: props.series.map((s) => s.name),
},
xAxis: {
...lineChartOption.xAxis,
data: categories,
name: props.xAxisLabel,
},
yAxis: {
...lineChartOption.yAxis,
name: props.yAxisLabel,
},
dataZoom: props.showDataZoom
? [
{
type: 'slider',
show: true,
start: 0,
end: 100,
xAxisIndex: 0,
},
]
: undefined,
series: props.series.map((s, index) => ({
type: 'line',
name: s.name,
data: s.data,
smooth: props.smooth,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 2,
color: s.color || echartsTheme.color[index % echartsTheme.color.length],
},
itemStyle: {
color: s.color || echartsTheme.color[index % echartsTheme.color.length],
},
areaStyle: props.area
? {
opacity: 0.1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: s.color || echartsTheme.color[index % echartsTheme.color.length] },
{ offset: 1, color: 'rgba(255, 255, 255, 0)' },
]),
}
: undefined,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#fff',
},
},
})),
}
}
// 单系列数据
const categories = props.data.map((item) => item.name)
const values = props.data.map((item) => item.value)
return {
...lineChartOption,
title: {
...lineChartOption.title,
text: props.title,
},
tooltip: {
...lineChartOption.tooltip,
formatter: (params: any) => {
return `${params.name}<br/>${params.seriesName}: ${formatNumber(params.value)}`
},
},
xAxis: {
...lineChartOption.xAxis,
data: categories,
name: props.xAxisLabel,
},
yAxis: {
...lineChartOption.yAxis,
name: props.yAxisLabel,
},
dataZoom: props.showDataZoom
? [
{
type: 'slider',
show: true,
start: 0,
end: 100,
xAxisIndex: 0,
},
]
: undefined,
series: [
{
type: 'line',
name: props.yAxisLabel || '数值',
data: values,
smooth: props.smooth,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 2,
},
itemStyle: {
color: echartsTheme.color[0],
},
areaStyle: props.area
? {
opacity: 0.1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: echartsTheme.color[0] },
{ offset: 1, color: 'rgba(255, 255, 255, 0)' },
]),
}
: undefined,
emphasis: {
focus: 'series',
},
},
],
}
})
/** 处理图表就绪 */
const handleReady = (chart: any) => {
emit('ready', chart)
}
/** 处理点击事件 */
const handleClick = (params: any) => {
const item = props.data.find((d) => d.name === params.name)
if (item) {
emit('click', { ...item, ...params })
}
}
/** 暴露方法 */
defineExpose({
loading,
})
</script>

View File

@@ -0,0 +1,120 @@
<!--
饼图组件
支持基础饼图环形图图例配置标签显示等
-->
<template>
<BaseChart
:option="chartOption"
:height="height"
:loading="loading"
@ready="handleReady"
@click="handleClick"
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import BaseChart from './BaseChart.vue'
import { pieChartOption, echartsTheme, getAssetStatusColor } from '@/utils/echarts'
import type { PieChartConfig } from '@/types/charts'
/** Props */
interface Props {
data: Array<{ name: string; value: number; [key: string]: any }>
title?: string
type?: 'pie' | 'doughnut'
showLegend?: boolean
showLabel?: boolean
height?: string
customColor?: boolean
}
const props = withDefaults(defineProps<Props>(), {
data: () => [],
title: '',
type: 'doughnut',
showLegend: true,
showLabel: true,
height: '400px',
customColor: false,
})
/** Emits */
interface Emits {
(e: 'ready', chart: any): void
(e: 'click', item: any): void
}
const emit = defineEmits<Emits>()
/** 加载状态 */
const loading = ref(false)
/** 图表配置 */
const chartOption = computed(() => {
// 处理数据
const chartData = props.data.map((item, index) => ({
...item,
itemStyle: {
color: props.customColor && item.status
? getAssetStatusColor(item.status)
: echartsTheme.color[index % echartsTheme.color.length],
},
}))
// 计算半径
const radius = props.type === 'doughnut' ? ['40%', '70%'] : ['0%', '70%']
return {
...pieChartOption,
title: {
...pieChartOption.title,
text: props.title,
},
legend: {
...pieChartOption.legend,
show: props.showLegend,
},
series: [
{
...pieChartOption.series![0],
radius,
data: chartData,
label: {
...pieChartOption.series![0].label,
show: props.showLabel,
formatter: props.showLabel ? '{b}: {c} ({d}%)' : '',
},
emphasis: {
...pieChartOption.series![0].emphasis,
itemStyle: {
...pieChartOption.series![0].emphasis.itemStyle,
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.3)',
},
},
},
],
}
})
/** 处理图表就绪 */
const handleReady = (chart: any) => {
emit('ready', chart)
}
/** 处理点击事件 */
const handleClick = (params: any) => {
const item = props.data.find((d) => d.name === params.name)
if (item) {
emit('click', { ...item, ...params })
}
}
/** 暴露方法 */
defineExpose({
loading,
})
</script>

View File

@@ -0,0 +1,105 @@
/**
* 图表组件库完整导出文件
*
* 统一导出所有图表组件、类型、工具函数等
*/
// ==================== 通用图表组件 ====================
export { default as BaseChart } from './BaseChart.vue'
export { default as PieChart } from './PieChart.vue'
export { default as BarChart } from './BarChart.vue'
export { default as LineChart } from './LineChart.vue'
export { default as GaugeChart } from './GaugeChart.vue'
export { default as FunnelChart } from './FunnelChart.vue'
// ==================== 业务图表组件 ====================
export { default as AssetStatusChart } from './business/AssetStatusChart.vue'
export { default as AssetDistributionChart } from './business/AssetDistributionChart.vue'
export { default AssetValueTrendChart } from './business/AssetValueTrendChart.vue'
export { default as AssetUtilizationChart } from './business/AssetUtilizationChart.vue'
// ==================== 统计卡片组件 ====================
export { default as StatCard } from '../statistics/StatCard.vue'
export { default as StatCardGroup } from '../statistics/StatCardGroup.vue'
// ==================== Composables ====================
export { useECharts } from '@/composables/useECharts'
export { useChartData } from '@/composables/useChartData'
// ==================== 工具函数 ====================
export {
// 主题配置
echartsTheme,
assetStatusColors,
assetStatusNames,
// 图表配置
baseChartOption,
pieChartOption,
barChartOption,
lineChartOption,
gaugeChartOption,
funnelChartOption,
// 格式化函数
formatNumber,
formatCurrency,
formatPercentage,
getColor,
getAssetStatusColor,
getAssetStatusName,
// 工具函数
resizeChart,
mergeOption,
} from '@/utils/echarts'
// ==================== 性能优化 ====================
export {
performanceConfig,
applyPerformanceConfig,
sampleData,
aggregateDataByTime,
paginateData,
lttbDownsampling,
debounce,
throttle,
createPerformanceMonitor,
type ChartPerformanceMonitor,
} from '@/utils/echarts/performance'
// ==================== 类型定义 ====================
export type {
// 基础类型
ChartDataItem,
ChartSeries,
// 配置类型
PieChartConfig,
BarChartConfig,
LineChartConfig,
GaugeChartConfig,
FunnelChartConfig,
StatCardConfig,
// 业务类型
AssetStatusStatistics,
AssetDistributionStatistics,
AssetTrendData,
AssetTypeStatistics,
MaintenanceStatistics,
// 其他类型
ChartTheme,
ChartSize,
ChartEvents,
ChartExportConfig,
ChartResponsiveConfig,
ChartLoadingConfig,
ChartAnimationConfig,
ChartPerformanceConfig,
} from '@/types/charts'
// ==================== 常量 ====================
export const CHART_VERSION = '1.0.0'
export const CHART_AUTHOR = '图表组件开发组'

View File

@@ -0,0 +1,77 @@
<!--
资产分布图组件
展示按机构类型的资产分布
-->
<template>
<div class="asset-distribution-chart">
<BarChart
:data="chartData"
title="资产分布统计"
type="vertical"
:x-axis-label="xLabel"
y-axis-label="数量"
:show-data-zoom="chartData.length > 10"
height="400px"
@click="handleClick"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import BarChart from '../BarChart.vue'
import type { AssetDistributionStatistics, AssetTypeStatistics } from '@/types/charts'
/** Props */
interface Props {
data: Array<AssetDistributionStatistics | AssetTypeStatistics>
type?: 'organization' | 'deviceType'
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
data: () => [],
type: 'organization',
loading: false,
})
/** Emits */
interface Emits {
(e: 'click', item: any): void
}
const emit = defineEmits<Emits>()
/** X轴标签 */
const xLabel = computed(() => {
return props.type === 'organization' ? '机构' : '设备类型'
})
/** 图表数据 */
const chartData = computed(() => {
return props.data.map(item => {
const name = props.type === 'organization'
? (item as AssetDistributionStatistics).organizationName
: (item as AssetTypeStatistics).typeName
return {
name: name || '未知',
value: item.count,
original: item,
}
}).sort((a, b) => b.value - a.value)
})
/** 处理点击 */
const handleClick = (item: any) => {
emit('click', item.original)
}
</script>
<style scoped lang="scss">
.asset-distribution-chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,70 @@
<!--
资产状态图组件
展示8种资产状态分布
-->
<template>
<div class="asset-status-chart">
<PieChart
:data="chartData"
title="资产状态分布"
type="doughnut"
:show-legend="true"
:show-label="true"
height="400px"
:custom-color="true"
@click="handleClick"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import PieChart from '../PieChart.vue'
import { assetStatusNames, assetStatusColors, formatPercentage } from '@/utils/echarts'
import type { AssetStatusStatistics } from '@/types/charts'
/** Props */
interface Props {
data: AssetStatusStatistics[]
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
data: () => [],
loading: false,
})
/** Emits */
interface Emits {
(e: 'click', item: AssetStatusStatistics): void
}
const emit = defineEmits<Emits>()
/** 图表数据 */
const chartData = computed(() => {
return props.data.map(item => ({
name: item.statusName || assetStatusNames[item.status],
value: item.count,
status: item.status,
percentage: item.percentage,
color: item.color || assetStatusColors[item.status],
}))
})
/** 处理点击 */
const handleClick = (item: any) => {
const statusItem = props.data.find(d => d.status === item.status)
if (statusItem) {
emit('click', statusItem)
}
}
</script>
<style scoped lang="scss">
.asset-status-chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,62 @@
<!--
资产利用率图表组件
使用仪表盘展示利用率
-->
<template>
<div class="asset-utilization-chart">
<GaugeChart
:value="utilizationRate"
:min="0"
:max="100"
title="资产利用率"
unit="%"
height="300px"
:color="gaugeColors"
:show-detail="true"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import GaugeChart from '../GaugeChart.vue'
/** Props */
interface Props {
totalAssets: number
usedAssets: number
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
totalAssets: 0,
usedAssets: 0,
loading: false,
})
/** 利用率 */
const utilizationRate = computed(() => {
if (props.totalAssets === 0) return 0
return (props.usedAssets / props.totalAssets) * 100
})
/** 仪表盘颜色 */
const gaugeColors = computed(() => {
const rate = utilizationRate.value
if (rate < 50) {
return ['#ef4444', '#f59e0b', '#10b981'] // 低:红橙绿
} else if (rate < 80) {
return ['#f59e0b', '#10b981', '#3b82f6'] // 中:橙绿蓝
} else {
return ['#10b981', '#3b82f6', '#6366f1'] // 高:绿蓝紫
}
})
</script>
<style scoped lang="scss">
.asset-utilization-chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,93 @@
<!--
资产价值趋势图组件
展示资产价值折旧净值趋势
-->
<template>
<div class="asset-value-trend-chart">
<LineChart
:data="dateData"
:series="seriesData"
title="资产价值趋势"
:area="true"
:smooth="true"
x-axis-label="日期"
y-axis-label="金额(万元)"
:show-data-zoom="true"
height="400px"
@click="handleClick"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import LineChart from '../LineChart.vue'
import type { AssetTrendData } from '@/types/charts'
/** Props */
interface Props {
data: AssetTrendData[]
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
data: () => [],
loading: false,
})
/** Emits */
interface Emits {
(e: 'click', item: AssetTrendData): void
}
const emit = defineEmits<Emits>()
/** 日期数据 */
const dateData = computed(() => {
return props.data.map(item => ({
name: item.date,
value: item.value / 10000, // 转换为万元
}))
})
/** 系列数据 */
const seriesData = computed(() => {
const valueData = props.data.map(item => item.value / 10000)
const depreciationData = props.data.map(item => (item.depreciation || 0) / 10000)
const netValueData = props.data.map(item => (item.netValue || item.value) / 10000)
return [
{
name: '总价值',
data: valueData,
color: '#475569',
},
{
name: '累计折旧',
data: depreciationData,
color: '#ef4444',
},
{
name: '净值',
data: netValueData,
color: '#10b981',
},
]
})
/** 处理点击 */
const handleClick = (item: any) => {
const original = props.data.find(d => d.date === item.name)
if (original) {
emit('click', original)
}
}
</script>
<style scoped lang="scss">
.asset-value-trend-chart {
width: 100%;
height: 100%;
}
</style>

23
src/components/charts/charts.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
/**
* 图表组件 TypeScript 声明
* 提供更好的类型提示
*/
import type { DefineComponent } from 'vue'
/** BaseChart 组件 */
export interface BaseChartProps {
option: any
height?: string
autoResize?: boolean
loading?: boolean
theme?: string | object
}
export interface BaseChartEmits {
ready: (chart: any) => void
click: (params: any) => void
}
declare const BaseChart: DefineComponent<BaseChartProps, BaseChartEmits>
export default BaseChart

View File

@@ -0,0 +1,21 @@
/**
* 图表组件统一导出
*/
// 统计卡片组件
export { default as StatCard } from '../statistics/StatCard.vue'
export { default as StatCardGroup } from '../statistics/StatCardGroup.vue'
// 通用图表组件
export { default as BaseChart } from './BaseChart.vue'
export { default as PieChart } from './PieChart.vue'
export { default as BarChart } from './BarChart.vue'
export { default as LineChart } from './LineChart.vue'
export { default as GaugeChart } from './GaugeChart.vue'
export { default as FunnelChart } from './FunnelChart.vue'
// 业务图表组件
export { default as AssetStatusChart } from './business/AssetStatusChart.vue'
export { default as AssetDistributionChart } from './business/AssetDistributionChart.vue'
export { default as AssetValueTrendChart } from './business/AssetValueTrendChart.vue'
export { default as AssetUtilizationChart } from './business/AssetUtilizationChart.vue'