refactor: 将组件前缀从Art统一替换为Fa

- 更新所有FA前缀的组件文件
- 新增Fa前缀的对应组件
- 更新所有引用路径和组件名
- 修改相关样式和变量名
- 调整部分组件目录结构
This commit is contained in:
zhangtao
2026-05-11 09:23:31 +08:00
parent 68b50359c5
commit 03a8151513
185 changed files with 2912 additions and 1283 deletions
@@ -0,0 +1,103 @@
<!-- 柱状图卡片 -->
<template>
<div class="fa-card relative overflow-hidden" :style="{ height: `${height}rem` }">
<div class="mb-5 flex-b items-start px-5 pt-5">
<div>
<p class="m-0 text-2xl font-medium leading-tight text-g-900">
{{ value }}
</p>
<p class="mt-1 text-sm text-g-600">{{ label }}</p>
</div>
<div
class="text-sm font-medium text-danger"
:class="[percentage > 0 ? 'text-success' : '', isMiniChart ? 'absolute bottom-5' : '']"
>
{{ percentage > 0 ? "+" : "" }}{{ percentage }}%
</div>
<div v-if="date" class="absolute bottom-5 right-5 text-xs text-g-600">
{{ date }}
</div>
</div>
<div
ref="chartRef"
class="absolute bottom-0 left-0 right-0 mx-auto"
:class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
></div>
</div>
</template>
<script setup lang="ts">
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
import { type EChartsOption } from "@/plugins/echarts";
defineOptions({ name: "FaBarChartCard" });
interface Props {
/** 数值 */
value: number;
/** 标签 */
label: string;
/** 百分比 +(绿色)-(红色) */
percentage: number;
/** 日期 */
date?: string;
/** 高度 */
height?: number;
/** 颜色 */
color?: string;
/** 图表数据 */
chartData: number[];
/** 柱状图宽度 */
barWidth?: string;
/** 是否为迷你图表 */
isMiniChart?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
height: 11,
barWidth: "26%",
});
// 使用新的图表组件抽象
const { chartRef } = useChartComponent({
props: {
height: `${props.height}rem`,
loading: false,
isEmpty: !props.chartData?.length || props.chartData.every((val) => val === 0),
},
checkEmpty: () => !props.chartData?.length || props.chartData.every((val) => val === 0),
watchSources: [() => props.chartData, () => props.color, () => props.barWidth],
generateOptions: (): EChartsOption => {
const computedColor = props.color || useChartOps().themeColor;
return {
grid: {
top: 0,
right: 0,
bottom: 15,
left: 0,
},
xAxis: {
type: "category",
show: false,
},
yAxis: {
type: "value",
show: false,
},
series: [
{
data: props.chartData,
type: "bar",
barWidth: props.barWidth,
itemStyle: {
color: computedColor,
borderRadius: 2,
},
},
],
};
},
});
</script>
@@ -0,0 +1,35 @@
<template>
<div v-loading="loading" class="min-h-0">
<ElEmpty
v-if="!loading && isEmpty"
:image-size="emptyImageSize"
:description="emptyDescription"
class="py-8"
/>
<div v-else class="min-h-0" :class="gridClass">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: "FaCardList" });
interface FaCardListProps {
loading?: boolean;
isEmpty?: boolean;
emptyDescription?: string;
emptyImageSize?: number;
/** Tailwind/Grid class string for layout */
gridClass?: string;
}
withDefaults(defineProps<FaCardListProps>(), {
loading: false,
isEmpty: false,
emptyDescription: "暂无数据",
emptyImageSize: 80,
gridClass:
"grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 auto-rows-max content-start",
});
</script>
@@ -0,0 +1,75 @@
<!-- 数据列表卡片 -->
<template>
<div class="fa-card p-5">
<div class="pb-3.5">
<p class="text-lg font-medium">{{ title }}</p>
<p class="text-sm text-g-600">{{ subtitle }}</p>
</div>
<ElScrollbar :style="{ height: maxHeight }">
<div v-for="(item, index) in list" :key="index" class="flex-c py-3">
<div v-if="item.icon" class="flex-cc mr-3 size-10 rounded-lg" :class="item.class">
<FaSvgIcon :icon="item.icon" class="text-xl" />
</div>
<div class="flex-1">
<div class="mb-1 text-sm">{{ item.title }}</div>
<div class="text-xs text-g-500">{{ item.status }}</div>
</div>
<div class="ml-3 text-xs text-g-500">{{ item.time }}</div>
</div>
</ElScrollbar>
<ElButton
class="mt-[25px] w-full text-center"
v-if="showMoreButton"
v-ripple
@click="handleMore"
>
查看更多
</ElButton>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: "FaDataListCard" });
interface Props {
/** 数据列表 */
list: Activity[];
/** 标题 */
title: string;
/** 副标题 */
subtitle?: string;
/** 最大显示数量 */
maxCount?: number;
/** 是否显示更多按钮 */
showMoreButton?: boolean;
}
interface Activity {
/** 标题 */
title: string;
/** 状态 */
status: string;
/** 时间 */
time: string;
/** 样式类名 */
class: string;
/** 图标 */
icon: string;
}
const ITEM_HEIGHT = 66;
const DEFAULT_MAX_COUNT = 5;
const props = withDefaults(defineProps<Props>(), {
maxCount: DEFAULT_MAX_COUNT,
});
const maxHeight = computed(() => `${ITEM_HEIGHT * props.maxCount}px`);
const emit = defineEmits<{
/** 点击更多按钮事件 */
(e: "more"): void;
}>();
const handleMore = () => emit("more");
</script>
@@ -0,0 +1,124 @@
<!-- 环型图卡片 -->
<template>
<div class="fa-card overflow-hidden" :style="{ height: `${height}rem` }">
<div class="flex box-border h-full p-5 pr-2">
<div class="flex w-full items-start gap-5">
<div class="flex-b h-full flex-1 flex-col">
<p class="m-0 text-xl font-medium leading-tight text-g-900">
{{ title }}
</p>
<div>
<p class="m-0 mt-2.5 text-xl font-medium leading-tight text-g-900">
{{ formatNumber(value) }}
</p>
<div
class="mt-1.5 text-xs font-medium"
:class="percentage > 0 ? 'text-success' : 'text-danger'"
>
{{ percentage > 0 ? "+" : "" }}{{ percentage }}%
<span v-if="percentageLabel">{{ percentageLabel }}</span>
</div>
</div>
<div class="mt-2 flex gap-4 text-xs text-g-600">
<div v-if="currentValue" class="flex-cc">
<div class="size-2 bg-theme/100 rounded mr-2"></div>
{{ currentValue }}
</div>
<div v-if="previousValue" class="flex-cc">
<div class="size-2 bg-g-400 rounded mr-2"></div>
{{ previousValue }}
</div>
</div>
</div>
<div class="flex-c h-full max-w-40 flex-1">
<div ref="chartRef" class="h-30 w-full"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { type EChartsOption } from "@/plugins/echarts";
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
defineOptions({ name: "FaDonutChartCard" });
interface Props {
/** 数值 */
value: number;
/** 标题 */
title: string;
/** 百分比 */
percentage: number;
/** 百分比标签 */
percentageLabel?: string;
/** 当前年份 */
currentValue?: string;
/** 去年年份 */
previousValue?: string;
/** 高度 */
height?: number;
/** 颜色 */
color?: string;
/** 半径 */
radius?: [string, string];
/** 数据 */
data: [number, number];
}
const props = withDefaults(defineProps<Props>(), {
height: 9,
radius: () => ["70%", "90%"],
data: () => [0, 0],
});
const formatNumber = (num: number) => {
return num.toLocaleString();
};
// 使用新的图表组件抽象
const { chartRef } = useChartComponent({
props: {
height: `${props.height}rem`,
loading: false,
isEmpty: props.data.every((val) => val === 0),
},
checkEmpty: () => props.data.every((val) => val === 0),
watchSources: [
() => props.data,
() => props.color,
() => props.radius,
() => props.currentValue,
() => props.previousValue,
],
generateOptions: (): EChartsOption => {
const computedColor = props.color || useChartOps().themeColor;
return {
series: [
{
type: "pie",
radius: props.radius,
avoidLabelOverlap: false,
label: {
show: false,
},
data: [
{
value: props.data[0],
name: props.currentValue,
itemStyle: { color: computedColor },
},
{
value: props.data[1],
name: props.previousValue,
itemStyle: { color: "#e6e8f7" },
},
],
},
],
};
},
});
</script>
@@ -0,0 +1,89 @@
<!-- 图片卡片 -->
<template>
<div class="w-full c-p" @click="handleClick">
<div class="fa-card overflow-hidden">
<div class="relative w-full aspect-[16/10] overflow-hidden">
<ElImage
:src="props.imageUrl"
fit="cover"
loading="lazy"
class="w-full h-full transition-transform duration-300 ease-in-out hover:scale-105"
>
<template #placeholder>
<div class="flex-cc w-full h-full bg-[#f5f7fa]">
<ElIcon><Picture /></ElIcon>
</div>
</template>
</ElImage>
<div
class="absolute right-3.5 bottom-3.5 py-1 px-2 text-xs bg-g-200 rounded"
v-if="props.readTime"
>
{{ props.readTime }} 阅读
</div>
</div>
<div class="p-4">
<div
class="inline-block py-0.5 px-2 mb-2 text-xs bg-g-300/70 rounded"
v-if="props.category"
>
{{ props.category }}
</div>
<p class="m-0 mb-3 text-base font-medium">{{ props.title }}</p>
<div class="flex-c gap-4 text-xs text-g-600">
<span class="flex-c gap-1" v-if="props.views">
<ElIcon class="text-base"><View /></ElIcon>
{{ props.views }}
</span>
<span class="flex-c gap-1" v-if="props.comments">
<ElIcon class="text-base"><ChatLineRound /></ElIcon>
{{ props.comments }}
</span>
<span>{{ props.date }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Picture, View, ChatLineRound } from "@element-plus/icons-vue";
defineOptions({ name: "FaImageCard" });
interface Props {
/** 图片地址 */
imageUrl: string;
/** 标题 */
title: string;
/** 分类 */
category?: string;
/** 阅读时间 */
readTime?: string;
/** 浏览量 */
views?: number;
/** 评论数 */
comments?: number;
/** 日期 */
date?: string;
}
const props = withDefaults(defineProps<Props>(), {
imageUrl: "",
title: "",
category: "",
readTime: "",
views: 0,
comments: 0,
date: "",
});
const emit = defineEmits<{
(e: "click", card: Props): void;
}>();
const handleClick = () => {
emit("click", props);
};
</script>
@@ -0,0 +1,126 @@
<!-- 折线图卡片 -->
<template>
<div class="fa-card relative overflow-hidden" :style="{ height: `${height}rem` }">
<div class="mb-2.5 flex-b items-start p-5">
<div>
<p class="text-2xl font-medium leading-none">
{{ value }}
</p>
<p class="mt-1 text-sm text-g-500">{{ label }}</p>
</div>
<div
class="text-sm font-medium"
:class="[
percentage > 0 ? 'text-success' : 'text-danger',
isMiniChart ? 'absolute bottom-5' : '',
]"
>
{{ percentage > 0 ? "+" : "" }}{{ percentage }}%
</div>
<div v-if="date" class="absolute bottom-5 right-5 text-xs text-g-500">
{{ date }}
</div>
</div>
<div
ref="chartRef"
class="absolute bottom-0 left-0 right-0 box-border w-full"
:class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
:style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
></div>
</div>
</template>
<script setup lang="ts">
import { graphic, type EChartsOption } from "@/plugins/echarts";
import { getCssVar, hexToRgba } from "@utils/ui";
import { useChartOps, useChartComponent } from "@/hooks/core/useChart";
defineOptions({ name: "FaLineChartCard" });
interface Props {
/** 数值 */
value: number;
/** 标签 */
label: string;
/** 百分比 */
percentage: number;
/** 日期 */
date?: string;
/** 高度 */
height?: number;
/** 颜色 */
color?: string;
/** 是否显示区域颜色 */
showAreaColor?: boolean;
/** 图表数据 */
chartData: number[];
/** 是否为迷你图表 */
isMiniChart?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
height: 11,
});
// 使用新的图表组件抽象
const { chartRef } = useChartComponent({
props: {
height: `${props.height}rem`,
loading: false,
isEmpty: !props.chartData?.length || props.chartData.every((val) => val === 0),
},
checkEmpty: () => !props.chartData?.length || props.chartData.every((val) => val === 0),
watchSources: [() => props.chartData, () => props.color, () => props.showAreaColor],
generateOptions: (): EChartsOption => {
const computedColor = props.color || useChartOps().themeColor;
return {
grid: {
top: 0,
right: 0,
bottom: 0,
left: 0,
},
xAxis: {
type: "category",
show: false,
boundaryGap: false,
},
yAxis: {
type: "value",
show: false,
},
series: [
{
data: props.chartData,
type: "line",
smooth: true,
showSymbol: false,
lineStyle: {
width: 3,
color: computedColor,
},
areaStyle: props.showAreaColor
? {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: props.color
? hexToRgba(props.color, 0.2).rgba
: hexToRgba(getCssVar("--el-color-primary"), 0.2).rgba,
},
{
offset: 1,
color: props.color
? hexToRgba(props.color, 0.01).rgba
: hexToRgba(getCssVar("--el-color-primary"), 0.01).rgba,
},
]),
}
: undefined,
},
],
};
},
});
</script>
@@ -0,0 +1,86 @@
<!-- 进度条卡片 -->
<template>
<div class="fa-card h-32 flex flex-col justify-center px-5">
<div class="mb-3.5 flex-c" :style="{ justifyContent: icon ? 'space-between' : 'flex-start' }">
<div v-if="icon" class="size-11 flex-cc bg-g-300 text-xl rounded-lg" :class="iconStyle">
<FaSvgIcon :icon="icon" class="text-2xl"></FaSvgIcon>
</div>
<div>
<FaCountTo
class="mb-1 block text-2xl font-semibold"
:target="percentage"
:duration="2000"
suffix="%"
:style="{ textAlign: icon ? 'right' : 'left' }"
/>
<p class="text-sm text-g-500">{{ title }}</p>
</div>
</div>
<ElProgress
:percentage="currentPercentage"
:stroke-width="strokeWidth"
:show-text="false"
:color="color"
class="[&_.el-progress-bar__outer]:bg-[rgb(240_240_240)]"
/>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: "FaProgressCard" });
interface Props {
/** 进度百分比 */
percentage: number;
/** 标题 */
title: string;
/** 颜色 */
color?: string;
/** 图标 */
icon?: string;
/** 图标样式 */
iconStyle?: string;
/** 进度条宽度 */
strokeWidth?: number;
}
const props = withDefaults(defineProps<Props>(), {
strokeWidth: 5,
color: "#67C23A",
});
const animationDuration = 500;
const currentPercentage = ref(0);
const animateProgress = () => {
const startTime = Date.now();
const startValue = currentPercentage.value;
const endValue = props.percentage;
const animate = () => {
const currentTime = Date.now();
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / animationDuration, 1);
currentPercentage.value = startValue + (endValue - startValue) * progress;
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
};
onMounted(() => {
animateProgress();
});
// 当 percentage 属性变化时重新执行动画
watch(
() => props.percentage,
() => {
animateProgress();
}
);
</script>
@@ -0,0 +1,68 @@
<!-- 统计卡片 -->
<template>
<div
class="fa-card h-32 flex-c px-5 transition-transform duration-200 hover:-translate-y-0.5"
:class="boxStyle"
>
<div v-if="icon" class="mr-4 size-11 flex-cc rounded-lg text-xl text-white" :class="iconStyle">
<FaSvgIcon :icon="icon"></FaSvgIcon>
</div>
<div class="flex-1">
<p class="m-0 text-lg font-medium" :style="{ color: textColor }" v-if="title">
{{ title }}
</p>
<FaCountTo
class="m-0 text-2xl font-medium"
v-if="count !== undefined"
:target="count"
:duration="2000"
:decimals="decimals"
:separator="separator"
/>
<p
class="mt-1 text-sm text-g-500 opacity-90"
:style="{ color: textColor }"
v-if="description"
>
{{ description }}
</p>
</div>
<div v-if="showArrow">
<FaSvgIcon icon="ri:arrow-right-s-line" class="text-xl text-g-500" />
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: "FaStatsCard" });
interface StatsCardProps {
/** 盒子样式 */
boxStyle?: string;
/** 图标 */
icon?: string;
/** 图标样式 */
iconStyle?: string;
/** 标题 */
title?: string;
/** 数值 */
count?: number;
/** 小数位 */
decimals?: number;
/** 分隔符 */
separator?: string;
/** 描述 */
description: string;
/** 文本颜色 */
textColor?: string;
/** 是否显示箭头 */
showArrow?: boolean;
}
withDefaults(defineProps<StatsCardProps>(), {
iconSize: 30,
iconBgRadius: 50,
decimals: 0,
separator: ",",
});
</script>
@@ -0,0 +1,69 @@
<!-- 时间轴列表卡片 -->
<template>
<div class="fa-card p-5">
<div class="pb-3.5">
<p class="text-lg font-medium">{{ title }}</p>
<p class="text-sm text-g-600">{{ subtitle }}</p>
</div>
<ElScrollbar :style="{ height: maxHeight }">
<ElTimeline class="!pl-0.5">
<ElTimelineItem
v-for="item in list"
:key="item.time"
:timestamp="item.time"
:placement="TIMELINE_PLACEMENT"
:color="item.status"
:center="true"
>
<div class="flex-c gap-3">
<div class="flex-c gap-2">
<span class="text-sm">{{ item.content }}</span>
<span v-if="item.code" class="text-sm text-theme">#{{ item.code }}</span>
</div>
</div>
</ElTimelineItem>
</ElTimeline>
</ElScrollbar>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: "FaTimelineListCard" });
// 常量配置
const ITEM_HEIGHT = 65;
const TIMELINE_PLACEMENT = "top";
const DEFAULT_MAX_COUNT = 5;
interface TimelineItem {
/** 时间 */
time: string;
/** 状态颜色 */
status: string;
/** 内容 */
content: string;
/** 代码标识 */
code?: string;
}
interface Props {
/** 时间轴列表数据 */
list: TimelineItem[];
/** 标题 */
title: string;
/** 副标题 */
subtitle?: string;
/** 最大显示数量 */
maxCount?: number;
}
// Props 定义和验证
const props = withDefaults(defineProps<Props>(), {
title: "",
subtitle: "",
maxCount: DEFAULT_MAX_COUNT,
});
// 计算最大高度
const maxHeight = computed(() => `${ITEM_HEIGHT * props.maxCount}px`);
</script>