mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 04:39:41 +00:00
refactor: 将组件前缀从Art统一替换为Fa
- 更新所有FA前缀的组件文件 - 新增Fa前缀的对应组件 - 更新所有引用路径和组件名 - 修改相关样式和变量名 - 调整部分组件目录结构
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user