feat: 去除不需要的页面元素和路由

master
7 days ago
parent 1d41af1e7c
commit aa3b909993

@ -175,7 +175,7 @@
/>
<!-- 租户切换器全局可见1步切换 -->
<FaTenantSwitcher />
<!-- <FaTenantSwitcher /> -->
<!-- 用户头像菜单 -->
<FaUserMenu />

@ -96,30 +96,6 @@
</ElFormItem>
<div class="login-form-tail flex flex-col gap-[1.1rem]">
<div class="relative pb-3">
<div
class="relative z-2 overflow-hidden select-none rounded-lg border border-transparent transition duration-300"
:class="{ 'border-[#FF4E4F]!': !isPassing && isClickPass }"
>
<FaDragVerify
ref="dragVerifyRef"
v-model:value="isPassing"
:text="$t('login.sliderText')"
:text-color="dragVerifyTextColor"
:success-text="$t('login.sliderSuccessText')"
progress-bar-bg="var(--el-color-primary)"
:background="isDark ? '#26272F' : '#F1F1F4'"
handler-bg="var(--default-box-color)"
/>
</div>
<p
class="absolute top-0 z-1 mt-2 px-px text-xs text-[#f56c6c] transition duration-300"
:class="{ 'translate-y-10': !isPassing && isClickPass }"
>
{{ $t("login.placeholder.slider") }}
</p>
</div>
<div class="login-options-row flex items-center justify-between text-sm">
<ElCheckbox v-model="loginForm.remember" class="login-remember">
{{ $t("login.rememberPwd") }}
@ -159,11 +135,11 @@
<!-- <FaLoginThirdPartySection @oauth="$emit('oauth', $event)" /> -->
<FaLoginAuthLinkRow
<!-- <FaLoginAuthLinkRow
:hint="$t('login.noAccount')"
:link-text="$t('login.register')"
@link="$emit('register')"
/>
/> -->
</div>
</template>
@ -185,15 +161,11 @@ interface Props {
accounts: Account[];
formKey: number | string;
isDark: boolean;
dragVerifyTextColor: string;
loading: boolean;
}
withDefaults(defineProps<Props>(), {});
const isPassing = defineModel<boolean>("isPassing", { required: true });
const isClickPass = defineModel<boolean>("isClickPass", { required: true });
interface Emits {
submit: [];
setupAccount: [key: AccountKey];
@ -208,7 +180,6 @@ interface Emits {
const emit = defineEmits<Emits>();
const formRef = ref();
const dragVerifyRef = ref<{ reset?: () => void } | null>(null);
const isCapsLock = ref(false);
function onPasswordKeyup(event: KeyboardEvent) {
@ -223,7 +194,6 @@ function onPasswordKeyup(event: KeyboardEvent) {
defineExpose({
validate: () => formRef.value?.validate?.(),
clearValidate: () => formRef.value?.clearValidate?.(),
resetDragVerify: () => dragVerifyRef.value?.reset?.(),
});
</script>

@ -144,39 +144,39 @@ const IframeView = defineComponent({
* `/dashboard`
* `component`
*/
export const dashboardLayoutChildren: AppRouteRecordRaw[] = [
{
path: "workplace",
name: "DashboardWorkplace",
component: () => import("@views/dashboard/workplace/index.vue"),
meta: {
title: "menus.dashboard.workplace",
icon: "ri:bar-chart-box-line",
keepAlive: true,
},
},
{
path: "analysis",
name: "DashboardAnalysis",
component: () => import("@views/dashboard/analysis/index.vue"),
meta: {
title: "menus.dashboard.analysis",
icon: "ri:align-item-bottom-line",
keepAlive: false,
},
},
{
path: "screen",
name: "DashboardScreen",
component: () => import("@views/dashboard/screen/index.vue"),
meta: {
title: "数据大屏",
icon: "ri:tv-line",
keepAlive: false,
hidden: false,
},
},
];
// export const dashboardLayoutChildren: AppRouteRecordRaw[] = [
// {
// path: "workplace",
// name: "DashboardWorkplace",
// component: () => import("@views/dashboard/workplace/index.vue"),
// meta: {
// title: "menus.dashboard.workplace",
// icon: "ri:bar-chart-box-line",
// keepAlive: true,
// },
// },
// {
// path: "analysis",
// name: "DashboardAnalysis",
// component: () => import("@views/dashboard/analysis/index.vue"),
// meta: {
// title: "menus.dashboard.analysis",
// icon: "ri:align-item-bottom-line",
// keepAlive: false,
// },
// },
// {
// path: "screen",
// name: "DashboardScreen",
// component: () => import("@views/dashboard/screen/index.vue"),
// meta: {
// title: "数据大屏",
// icon: "ri:tv-line",
// keepAlive: false,
// hidden: false,
// },
// },
// ];
// -----------------------------------------------------------------------------
// 静态壳层菜单:后端未下发 /home、/dashboard 时补全侧栏;混合模式路由按 name 去重合并
@ -198,14 +198,14 @@ function stripRouteRecordForShell(route: RouteRecordRaw): AppRouteRecord {
} as AppRouteRecord;
}
export function getDashboardMenuTreeForMerge(): AppRouteRecord {
return {
name: "Dashboard",
path: "/dashboard",
meta: DASHBOARD_PARENT_META,
children: dashboardLayoutChildren.map(stripRouteRecordForShell),
};
}
// export function getDashboardMenuTreeForMerge(): AppRouteRecord {
// return {
// name: "Dashboard",
// path: "/dashboard",
// meta: DASHBOARD_PARENT_META,
// children: dashboardLayoutChildren.map(stripRouteRecordForShell),
// };
// }
export function mergeAppRouteRecords(
primary: AppRouteRecord[],
@ -296,10 +296,6 @@ export function mergeShellRoutesIntoMenu(menuList: AppRouteRecord[]): AppRouteRe
tryPush(mergeShellHomeMenu);
if (!paths.has("/dashboard")) {
tryPush(dashboardRoutesToShellMenu(structuredClone(getDashboardMenuTreeForMerge())));
}
if (additions.length === 0) return menuList;
return [...additions, ...menuList];
}
@ -371,14 +367,14 @@ export const staticRoutes: AppRouteRecordRaw[] = [
meta: HOME_MENU_META,
},
/** 仪表盘子路由定义见同文件导出的 `dashboardLayoutChildren` */
{
path: "dashboard",
name: "Dashboard",
redirect: "/dashboard/workplace",
component: NestedRouterParent,
meta: DASHBOARD_PARENT_META,
children: dashboardLayoutChildren,
},
// {
// path: "dashboard",
// name: "Dashboard",
// redirect: "/dashboard/workplace",
// component: NestedRouterParent,
// meta: DASHBOARD_PARENT_META,
// children: dashboardLayoutChildren,
// },
/** 快速链接:统一父级,不在菜单中展示,通过 URL 或 fastEnter 直接访问 */
{
path: "fastlink",

@ -1,326 +0,0 @@
<template>
<div>
<FaDashboardSkeleton v-if="loading" />
<template v-else>
<ElRow :gutter="20">
<ElCol :xl="14" :lg="15" :xs="24"><TodaySales /></ElCol>
<ElCol :xl="10" :lg="9" :xs="24"><VisitorInsights /></ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :xl="12" :lg="12" :xs="24" class="flex flex-col gap-5">
<CustomerSatisfaction class="flex-1" />
<TotalRevenue class="flex-1" />
</ElCol>
<ElCol :xl="12" :lg="12" :xs="24" class="flex flex-col gap-5">
<TargetVsReality class="flex-1" />
<TopProducts class="flex-1" />
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :xl="8" :lg="8" :xs="24"><SalesMappingByCountry /></ElCol>
<ElCol :xl="8" :lg="8" :xs="24"><VolumeServiceLevel /></ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">柱状图单数据</span></div>
<FaBarChart
:data="singleBarData"
:xAxisData="xAxisData"
:showLegend="true"
legendPosition="right"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">柱状图多组数据</span></div>
<FaBarChart
:data="multiBarData"
:xAxisData="xAxisData"
:showLegend="true"
barWidth="26%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">柱状图堆叠</span></div>
<FaBarChart
:data="stackBarData"
:xAxisData="stackXAxisData"
:showLegend="true"
:stack="true"
barWidth="26%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">折线图</span></div>
<FaLineChart
:data="[58, 15, 82, 35, 120, 62, 45]"
:xAxisData="['一月', '二月', '三月', '四月', '五月', '六月', '七月']"
symbol="none"
:symbolSize="7"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">折线图渐变</span></div>
<FaLineChart
:data="[28, 45, 82, 35, 100, 32, 55]"
:xAxisData="['一月', '二月', '三月', '四月', '五月', '六月', '七月']"
:showAreaColor="true"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">折线图多组</span></div>
<FaLineChart
:data="multiLineData"
:xAxisData="['1月', '2月', '3月', '4月', '5月', '6月']"
:showLegend="true"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">柱状图水平</span></div>
<FaHBarChart
:data="[50, 80, 120, 90, 60]"
:xAxisData="['产品A', '产品B', '产品C', '产品D', '产品E']"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">柱状图水平多组</span></div>
<FaHBarChart
:data="[
{ name: '系列1', data: [10, 20, 30] },
{ name: '系列2', data: [15, 25, 35] },
]"
:xAxisData="['类目1', '类目2', '类目3']"
:showLegend="true"
barWidth="30%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">柱状图水平堆叠</span></div>
<FaHBarChart
:data="[
{ name: '系列1', data: [10, 20, 30] },
{ name: '系列2', data: [15, 25, 35] },
]"
:xAxisData="['类目1', '类目2', '类目3']"
:showLegend="true"
:stack="true"
barWidth="30%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">环形图</span></div>
<FaRingChart
:data="[
{ value: 35, name: '分类A' },
{ value: 28, name: '分类B' },
{ value: 42, name: '分类C' },
]"
:radius="['54%', '70%']"
legendPosition="bottom"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">环形图中心文字</span></div>
<FaRingChart
:data="[
{ value: 35, name: '分类A' },
{ value: 28, name: '分类B' },
{ value: 42, name: '分类C' },
{ value: 32, name: '分类D' },
{ value: 26, name: '分类E' },
{ value: 37, name: '分类F' },
]"
:radius="['54%', '70%']"
:showLegend="true"
legendPosition="bottom"
centerText="¥300,458"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">饼图</span></div>
<FaRingChart
:data="[
{ value: 30, name: '分类A' },
{ value: 25, name: '分类B' },
{ value: 45, name: '分类C' },
]"
:radius="['0%', '70%']"
:showLegend="true"
legendPosition="right"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">散点图</span></div>
<FaScatterChart
:data="[
{ value: [1, 3] },
{ value: [2, 4] },
{ value: [3, 5] },
{ value: [4, 6] },
{ value: [5, 7] },
{ value: [6, 8] },
{ value: [7, 7] },
{ value: [8, 9] },
{ value: [9, 8] },
{ value: [10, 6] },
{ value: [11, 7] },
{ value: [12, 8] },
]"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">雷达图</span></div>
<FaRadarChart
:indicator="[
{ name: '销售', max: 100 },
{ name: '管理', max: 100 },
{ name: '技术', max: 100 },
{ name: '客服', max: 100 },
{ name: '开发', max: 100 },
]"
:data="[
{ name: '预算分配', value: [80, 70, 90, 85, 75] },
{ name: '实际开销', value: [70, 75, 85, 80, 70] },
]"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">K线图</span></div>
<FaKLineChart
:data="[
{ time: '2024-01-01', open: 20, close: 23, high: 25, low: 18 },
{ time: '2024-01-02', open: 23, close: 21, high: 24, low: 20 },
{ time: '2024-01-03', open: 21, close: 25, high: 26, low: 21 },
]"
:showDataZoom="false"
:dataZoomStart="0"
:dataZoomEnd="100"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5"><span class="text-base font-medium">双向堆叠柱状图</span></div>
<FaDualBarCompareChart
:positiveData="[50, 28, 80, 65, 68, 70, 60, 55]"
:negativeData="[50, 28, 40, 45, 38, 50, 42, 48]"
:xAxisData="[
'0-4岁',
'5-14岁',
'15-24岁',
'25-34岁',
'35-44岁',
'45-54岁',
'55-64岁',
'65岁以上',
]"
positiveName="男性年龄分布"
negativeName="女性年龄分布"
/>
</div>
</ElCol>
</ElRow>
</template>
</div>
</template>
<script setup lang="ts">
import type { LineDataItem } from "@/types/component/chart";
import { ref, onMounted } from "vue";
import FaDashboardSkeleton from "@/components/skeleton/fa-dashboard-skeleton.vue";
import TodaySales from "./modules/today-sales.vue";
import VisitorInsights from "./modules/visitor-insights.vue";
import TotalRevenue from "./modules/total-revenue.vue";
import CustomerSatisfaction from "./modules/customer-satisfaction.vue";
import TargetVsReality from "./modules/target-vs-reality.vue";
import TopProducts from "./modules/top-products.vue";
import SalesMappingByCountry from "./modules/sales-mapping-by-country.vue";
import VolumeServiceLevel from "./modules/volume-service-level.vue";
defineOptions({ name: "DashboardAnalysis" });
const loading = ref(true);
onMounted(() => {
loading.value = false;
});
// === ===
const multiLineData: LineDataItem[] = [
{
name: "销售额",
data: [120, 132, 101, 134, 90, 130],
areaStyle: { startOpacity: 0.1, endOpacity: 0 },
},
{
name: "利润",
data: [80, 92, 71, 94, 60, 80],
areaStyle: { startOpacity: 0.1, endOpacity: 0 },
},
];
const singleBarData = ref([120, 200, 150, 80, 70, 110, 130]);
const xAxisData = ref(["周一", "周二", "周三", "周四", "周五", "周六", "周日"]);
const multiBarData = ref([
{ name: "销售额", data: [120, 200, 150, 80, 70, 110, 130] },
{ name: "利润", data: [20, 50, 30, 15, 10, 25, 35] },
]);
const stackBarData = ref([
{ name: "Q1", data: [20, 25, 30, 35, 40], stack: "total" },
{ name: "Q2", data: [30, 35, 40, 45, 50], stack: "total" },
]);
const stackXAxisData = ref(["产品A", "产品B", "产品C", "产品D", "产品E"]);
</script>
<style scoped>
:deep(.el-card) {
--el-card-border-radius: calc(var(--custom-radius) + 2px);
border: 1px solid var(--fa-card-border);
}
.page-title {
margin: 1.25rem 0;
font-size: 1.25rem;
font-weight: 500;
line-height: 1.75rem;
}
.page-title:first-child {
margin-top: 0;
}
.console-cards-showcase {
padding: 16px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: var(--el-border-radius-base);
}
</style>

@ -1,50 +0,0 @@
<template>
<div class="fa-card h-100 p-5 mb-5 max-sm:mb-4 flex flex-col">
<div class="fa-card-header">
<div class="title">
<h4>客户满意度</h4>
</div>
</div>
<FaLineChart
class="flex-1 min-h-0"
:data="chartData"
:xAxisData="xAxisData"
:showLegend="true"
:showAxisLabel="true"
:showAxisLine="false"
:showSplitLine="true"
/>
</div>
</template>
<script setup lang="ts">
import type { LineDataItem } from "@/types/component/chart";
const AREA_STYLE_CONFIG = {
startOpacity: 0.08,
endOpacity: 0,
} as const;
/**
* X 轴数据配置
* 表示一周的天数周一到周日
*/
const xAxisData = ["1", "2", "3", "4", "5", "6", "7"];
/**
* 客户满意度图表数据
* 对比上个月和本月的满意度趋势
*/
const chartData = computed<LineDataItem[]>(() => [
{
name: "上个月",
data: [65, 72, 68, 75, 82, 78, 85],
areaStyle: AREA_STYLE_CONFIG,
},
{
name: "本月",
data: [78, 85, 82, 88, 92, 89, 95],
areaStyle: AREA_STYLE_CONFIG,
},
]);
</script>

@ -1,19 +0,0 @@
<template>
<div class="fa-card h-82 p-5 mb-5 max-sm:mb-4">
<div class="fa-card-header">
<div class="title">
<h4>全国销售分布</h4>
</div>
</div>
<FaRingChart
:data="[
{ value: 30, name: '北京' },
{ value: 25, name: '上海' },
{ value: 45, name: '广州' },
]"
:color="['#4C87F3', '#93F1B4', '#8BD8FC']"
:radius="['46%', '60%']"
/>
</div>
</template>

@ -1,82 +0,0 @@
<template>
<div class="fa-card h-100 p-5 mb-5 max-sm:mb-4">
<div class="fa-card-header">
<div class="title">
<h4>目标与实际</h4>
</div>
</div>
<FaBarChart
class="p-5"
height="12rem"
:data="revenueData"
:xAxisData="weekDays"
:showAxisLine="false"
barWidth="28%"
/>
<div class="px-5 mt-4">
<div v-for="item in totalItems" :key="item.label" class="flex items-center mb-5 last:mb-0">
<div class="flex items-center justify-start w-3/5 text-sm">
<div
class="w-10 h-10 mr-3 text-lg rounded-md flex items-center justify-center"
:class="item.iconClass"
>
<FaSvgIcon :icon="item.icon" />
</div>
<div class="flex flex-col items-start">
<span class="text-base text-g-800">{{ item.label }}</span>
<span class="mt-1 text-xs text-g-500">{{ item.subLabel }}</span>
</div>
</div>
<div class="text-lg font-normal" :class="item.valueClass">{{ item.value }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface RevenueDataItem {
name: string;
data: number[];
}
/**
* 一周的日期标签
*/
const weekDays = ref(["周一", "周二", "周三", "周四", "周五", "周六", "周日"]);
/**
* 目标与实际销售数据
* 展示一周内的线上销售情况
*/
const revenueData = ref<RevenueDataItem[]>([
{
name: "线上销售",
data: [12, 13, 5, 15, 10, 15, 18],
},
]);
/**
* 统计项数据
*/
const totalItems = [
{
icon: "ri:shopping-bag-line",
iconClass: "text-theme bg-theme/12",
label: "实际销售额",
subLabel: "全球",
value: "8,823",
valueClass: "text-theme",
},
{
icon: "ri:money-dollar-circle-line",
iconClass: "text-theme bg-theme/12",
label: "目标销售额",
subLabel: "商业",
value: "12,122",
valueClass: "text-theme",
},
];
</script>

@ -1,89 +0,0 @@
<template>
<div class="fa-card h-82 p-5 mb-5 overflow-hidden max-lg:h-auto max-sm:mb-4">
<div class="fa-card-header pr-0">
<div class="title">
<h4>今日销售</h4>
<p>销售总结</p>
</div>
<div
class="flex items-center justify-center h-7.5 min-w-17 border border-g-300 rounded-lg text-g-500 cursor-pointer"
>
<FaSvgIcon icon="ri:arrow-up-line" class="text-base mr-1.5" />
<span class="text-xs">导出</span>
</div>
</div>
<div class="mt-2">
<ElRow :gutter="20">
<ElCol :span="6" :xs="24" v-for="(item, index) in salesData" :key="index">
<div
class="flex px-5 flex-col justify-center h-55 border border-g-300/85 rounded-xl max-lg:mb-4 max-sm:flex-row max-sm:justify-between max-sm:items-center max-sm:h-40"
>
<div class="size-12 rounded-lg flex items-center justify-center bg-theme/10">
<FaSvgIcon :icon="item.icon" class="text-xl text-theme" />
</div>
<div class="max-sm:ml-4 mt-3.5 max-sm:mt-0 max-sm:text-end">
<FaCountTo class="text-2xl font-medium" :target="item.value" :duration="1500" />
<p class="mt-2 text-base text-g-600 max-sm:mt-1">{{ item.label }}</p>
<small class="text-g-500 mt-1 max-sm:mt-0.5">
较昨天
<span
class="font-medium"
:class="[item.change.indexOf('+') === -1 ? 'text-danger' : 'text-success']"
>
{{ item.change }}
</span>
</small>
</div>
</div>
</ElCol>
</ElRow>
</div>
</div>
</template>
<script setup lang="ts">
interface SalesDataItem {
label: string;
value: number;
change: string;
icon: string;
class: string;
}
/**
* 今日销售数据统计
* 包含销售额订单量产品销量和新客户数等关键指标
*/
const salesData = ref<SalesDataItem[]>([
{
label: "总销售额",
value: 999,
change: "+10%",
icon: "ri:bar-chart-box-ai-line",
class: "bg-theme",
},
{
label: "总订单量",
value: 300,
change: "+15%",
icon: "ri:bar-chart-grouped-line",
class: "bg-warning",
},
{
label: "产品销售量",
value: 56,
change: "-5%",
icon: "ri:bar-chart-2-line",
class: "bg-error",
},
{
label: "新客户数",
value: 68,
change: "+8%",
icon: "ri:user-add-line",
class: "bg-success",
},
]);
</script>

@ -1,96 +0,0 @@
<template>
<div class="fa-card h-100 p-5 mb-5 max-sm:mb-4">
<div class="fa-card-header">
<div class="title">
<h4>热门产品</h4>
</div>
</div>
<ElScrollbar class="h-full">
<FaTable
:data="products"
:style="{ width: '100%' }"
size="large"
:border="false"
:stripe="false"
:header-cell-style="{ background: 'transparent' }"
>
<ElTableColumn type="index" label="序号" width="60" />
<ElTableColumn prop="name" label="产品名称" width="200" />
<ElTableColumn prop="popularity" label="销量">
<template #default="scope">
<ElProgress
:percentage="scope.row.popularity"
:color="getColor(scope.row.popularity)"
:stroke-width="5"
:show-text="false"
/>
</template>
</ElTableColumn>
<ElTableColumn prop="sales" label="销量" width="80">
<template #default="scope">
<span
:style="{
color: getColor(scope.row.popularity),
backgroundColor: `rgba(${hexToRgb(getColor(scope.row.popularity))}, 0.08)`,
border: '1px solid',
padding: '3px 6px',
borderRadius: '4px',
fontSize: '12px',
}"
>
{{ scope.row.sales }}
</span>
</template>
</ElTableColumn>
</FaTable>
</ElScrollbar>
</div>
</template>
<script setup lang="ts">
import { hexToRgb } from "@utils";
interface Product {
name: string;
popularity: number;
sales: string;
}
const COLOR_THRESHOLDS = {
LOW: 25,
MEDIUM: 50,
HIGH: 75,
} as const;
const POPULARITY_COLORS = {
LOW: "#00E096",
MEDIUM: "#0095FF",
HIGH: "#884CFF",
VERY_HIGH: "#FE8F0E",
} as const;
/**
* 热门产品列表数据
* 包含产品名称热度和销量信息
*/
const products = computed<Product[]>(() => [
{ name: "智能手机", popularity: 10, sales: "100" },
{ name: "笔记本电脑", popularity: 29, sales: "100" },
{ name: "平板电脑", popularity: 65, sales: "100" },
{ name: "智能手表", popularity: 32, sales: "100" },
{ name: "无线耳机", popularity: 78, sales: "100" },
{ name: "智能音箱", popularity: 41, sales: "100" },
]);
/**
* 根据热度百分比获取对应的颜色
* @param percentage 热度百分比 (0-100)
* @returns 对应的颜色值
*/
const getColor = (percentage: number): string => {
if (percentage < COLOR_THRESHOLDS.LOW) return POPULARITY_COLORS.LOW;
if (percentage < COLOR_THRESHOLDS.MEDIUM) return POPULARITY_COLORS.MEDIUM;
if (percentage < COLOR_THRESHOLDS.HIGH) return POPULARITY_COLORS.HIGH;
return POPULARITY_COLORS.VERY_HIGH;
};
</script>

@ -1,44 +0,0 @@
<template>
<div class="fa-card h-100 p-5 mb-5 max-sm:mb-4 flex flex-col">
<div class="fa-card-header">
<div class="title">
<h4>总收入</h4>
</div>
</div>
<FaBarChart
class="flex-1 min-h-0"
:data="revenueData"
:xAxisData="weekDays"
:showLegend="true"
:showAxisLine="false"
barWidth="18%"
/>
</div>
</template>
<script setup lang="ts">
interface RevenueDataItem {
name: string;
data: number[];
}
/**
* 一周的日期标签
*/
const weekDays = ref(["周一", "周二", "周三", "周四", "周五", "周六", "周日"]);
/**
* 总收入数据
* 对比线上和线下销售的一周收入情况
*/
const revenueData = ref<RevenueDataItem[]>([
{
name: "线上销售",
data: [12, 13, 5, 15, 10, 15, 18],
},
{
name: "线下销售",
data: [10, 11, 20, 5, 11, 13, 10],
},
]);
</script>

@ -1,43 +0,0 @@
<template>
<div class="fa-card h-82 p-5 mb-5 max-sm:mb-4 flex flex-col">
<div class="fa-card-header">
<div class="title">
<h4>访客洞察</h4>
</div>
</div>
<FaLineChart
class="flex-1 min-h-0"
:data="chartData"
:xAxisData="xAxisData"
:showLegend="true"
:showAxisLabel="true"
:showAxisLine="false"
:showSplitLine="true"
/>
</div>
</template>
<script setup lang="ts">
import type { LineDataItem } from "@/types/component/chart";
/**
* X 轴数据配置
* 表示一年的月份1-12
*/
const xAxisData = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];
/**
* 访客洞察图表数据
* 对比老客户和新客户的全年访问趋势
*/
const chartData = computed<LineDataItem[]>(() => [
{
name: "老客户",
data: [280, 350, 300, 250, 230, 210, 240, 280, 320, 350, 300, 200],
},
{
name: "新客户",
data: [260, 200, 150, 130, 180, 270, 340, 380, 300, 220, 170, 130],
},
]);
</script>

@ -1,51 +0,0 @@
<template>
<div class="fa-card h-82 p-5 mb-5 max-sm:mb-4">
<div class="fa-card-header">
<div class="title">
<h4>业务量与服务水平</h4>
</div>
</div>
<FaBarChart
class="mt-6"
height="14.3rem"
:data="volumeServiceData"
:xAxisData="serviceCategories"
:showLegend="true"
:showAxisLine="false"
:stack="true"
barWidth="22%"
/>
</div>
</template>
<script setup lang="ts">
interface VolumeServiceDataItem {
name: string;
data: number[];
stack: string;
}
/**
* 服务类别数据
* 不同产品的分类标签
*/
const serviceCategories = ref(["产品A", "产品B", "产品C", "产品D", "产品E"]);
/**
* 业务量与服务量数据
* 展示各产品的业务量和服务量对比使用堆叠柱状图展示
*/
const volumeServiceData = ref<VolumeServiceDataItem[]>([
{
name: "业务量",
data: [20, 25, 30, 35, 40],
stack: "total",
},
{
name: "服务量",
data: [30, 35, 40, 45, 50],
stack: "total",
},
]);
</script>

@ -1,750 +0,0 @@
<template>
<div ref="containerRef" class="screen-container">
<canvas ref="canvasRef" class="particle-canvas" />
<div class="scan-line scan-1" />
<div class="scan-line scan-2" />
<ScreenHeader />
<!-- 顶部核心指标卡 x6 -->
<div class="stat-row">
<div v-for="s in stats" :key="s.label" class="stat-card">
<div class="stat-val" :class="'stat-' + s.color">{{ s.value }}</div>
<div class="stat-label">{{ s.label }}</div>
<div class="stat-sub">
<span :class="s.up ? 'up' : 'down'">{{ s.up ? "▲" : "▼" }} {{ s.change }}</span>
<span class="stat-vs">较昨日</span>
</div>
</div>
</div>
<!-- 主体布局 -->
<div class="screen-grid">
<!-- 左列 -->
<ServiceLevel class="gp-r1 gp-c1-3" />
<TargetReality class="gp-r2 gp-c1-3" />
<!-- 中间左 -->
<LiveMetrics class="gp-r1 gp-c3-5" />
<FunnelChart class="gp-r2 gp-c3-5" />
<!-- 中国地图 -->
<div class="panel p1 map-panel gp-r1 gp-c5-9" style="grid-row: 1 / 3">
<FaMapChart :dynamic="true" />
</div>
<!-- 中间右 -->
<ChannelDonut class="gp-r1 gp-c9-11" />
<UserProfile class="gp-r2 gp-c9-11" />
<!-- 右列 -->
<ProductBar class="gp-r1 gp-c11-13" />
<RealtimeMessages class="gp-r2 gp-c11-13" />
</div>
<!-- 大卡片行 x6 统计图 -->
<div class="card-row">
<MiniRevenue />
<MiniOrders />
<MiniTraffic />
<MiniRefund />
<MiniInventory />
<MiniRating />
</div>
<!-- 底部状态栏 -->
<div class="bottom-bar">
<div class="bb-item"><span class="bb-dot pulse" />系统运行正常</div>
<div class="bb-item">数据更新时间{{ updateTime }}</div>
<div class="bb-ticker">
<span class="ticker-track">
<span v-for="t in tickerItems" :key="t" class="ticker-item">{{ t }}</span>
</span>
</div>
<div class="bb-items-right">
<div v-for="s in structItems" :key="s.label" class="bb-meta">
<span class="bb-meta-dot" :class="'sr-' + s.cls" />
{{ s.label }}: {{ s.value }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
defineAsyncComponent,
onMounted,
onUnmounted,
provide,
reactive,
ref,
shallowRef,
} from "vue";
import ScreenHeader from "./modules/ScreenHeader.vue";
import ServiceLevel from "./modules/ServiceLevel.vue";
import TargetReality from "./modules/TargetReality.vue";
import LiveMetrics from "./modules/LiveMetrics.vue";
import FunnelChart from "./modules/FunnelChart.vue";
import ChannelDonut from "./modules/ChannelDonut.vue";
import UserProfile from "./modules/UserProfile.vue";
import ProductBar from "./modules/ProductBar.vue";
import RealtimeMessages from "./modules/RealtimeMessages.vue";
import MiniRevenue from "./modules/MiniRevenue.vue";
import MiniOrders from "./modules/MiniOrders.vue";
import MiniTraffic from "./modules/MiniTraffic.vue";
import MiniRefund from "./modules/MiniRefund.vue";
import MiniInventory from "./modules/MiniInventory.vue";
import MiniRating from "./modules/MiniRating.vue";
defineOptions({ name: "DashboardScreen" });
const FaMapChart = defineAsyncComponent(() => import("@/components/charts/fa-map-chart/index.vue"));
const containerRef = ref<HTMLDivElement>();
const canvasRef = ref<HTMLCanvasElement>();
const isFullscreen = ref(false);
const updateTime = ref("");
const tickerItems = ref([
"交易引擎: 23%",
"消息队列: 58%",
"缓存命中: 94.1%",
"网关吞吐: 128Mbps",
"QPS: 3.2K",
"P99 延迟: 12ms",
]);
const animFrame = shallowRef(0);
let statsTimer = 0;
const stats = reactive([
{ label: "总交易额", value: "¥128.6万", change: "12.5%", up: true, color: "cyan" as const },
{ label: "订单总量", value: "8,350", change: "8.2%", up: true, color: "purple" as const },
{ label: "活跃商户", value: "1,286", change: "3.1%", up: true, color: "green" as const },
{ label: "售后工单", value: "356", change: "5.7%", up: false, color: "warn" as const },
{ label: "平均客单价", value: "¥468", change: "2.3%", up: true, color: "teal" as const },
{ label: "支付转化率", value: "38.2%", change: "1.8%", up: true, color: "rose" as const },
]);
const structItems = reactive([
{ label: "交易节点", value: "128", cls: "cyan" },
{ label: "主数据库", value: "运行中", cls: "green" },
{ label: "缓存集群", value: "94.1%", cls: "purple" },
{ label: "带宽使用", value: "1.2G", cls: "warn" },
{ label: "存储容量", value: "2.4T", cls: "cyan" },
{ label: "工作节点", value: "32", cls: "green" },
{ label: "API 网关", value: "99.9%", cls: "purple" },
{ label: "日志服务", value: "运行中", cls: "cyan" },
]);
function fmt(n: number): string {
return n >= 10000 ? (n / 10000).toFixed(1) + "万" : n.toLocaleString();
}
function randFloat(base: number, pct: number): number {
return base * (1 + (Math.random() - 0.5) * pct * 2);
}
function randPct(): string {
return (Math.random() * 15 - 3).toFixed(1) + "%";
}
function updateStats() {
const t = randFloat(1286000, 0.05);
const o = randFloat(8350, 0.04);
const m = randFloat(1286, 0.03);
const w = randFloat(356, 0.08);
stats[0]!.value = "¥" + fmt(Math.round(t));
stats[1]!.value = fmt(Math.round(o));
stats[2]!.value = fmt(Math.round(m));
stats[3]!.value = fmt(Math.round(w));
stats[4]!.value = "¥" + Math.round(randFloat(468, 0.03));
stats[5]!.value = randFloat(38.2, 0.06).toFixed(1) + "%";
stats[0]!.change = randPct();
stats[0]!.up = Math.random() > 0.3;
stats[1]!.change = randPct();
stats[1]!.up = Math.random() > 0.4;
stats[2]!.change = randPct();
stats[2]!.up = Math.random() > 0.5;
stats[3]!.change = randPct();
stats[3]!.up = Math.random() > 0.6;
stats[4]!.change = randPct();
stats[4]!.up = Math.random() > 0.3;
stats[5]!.change = randPct();
stats[5]!.up = Math.random() > 0.4;
}
async function toggleFullscreen() {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
} else {
await containerRef.value?.requestFullscreen();
}
} catch {
/* ignored */
}
}
function onFullscreenChange() {
isFullscreen.value = !!document.fullscreenElement;
}
// fullscreenchange document onUnmounted
// keep-alive /
onMounted(() => {
document.addEventListener("fullscreenchange", onFullscreenChange);
});
onUnmounted(() => {
document.removeEventListener("fullscreenchange", onFullscreenChange);
});
provide("toggleFullscreen", toggleFullscreen);
provide("isFullscreen", isFullscreen);
/* ========== 粒子背景 ========== */
let ctx: CanvasRenderingContext2D | null = null;
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
r: number;
}
let particles: Particle[] = [];
function initParticles() {
const c = canvasRef.value;
if (!c) return;
ctx = c.getContext("2d");
resizeCanvas();
spawnParticles();
tick();
}
function resizeCanvas() {
const c = canvasRef.value;
if (!c || !containerRef.value) return;
c.width = containerRef.value.clientWidth;
c.height = containerRef.value.clientHeight;
}
function spawnParticles() {
const c = canvasRef.value;
if (!c) return;
const count = Math.floor((c.width * c.height) / 18000);
particles = Array.from({ length: count }, () => ({
x: Math.random() * c.width,
y: Math.random() * c.height,
vx: (Math.random() - 0.5) * 0.4,
vy: (Math.random() - 0.5) * 0.4,
r: Math.random() * 1.5 + 0.5,
}));
}
function tick() {
if (!ctx || !canvasRef.value) return;
const c = canvasRef.value;
const w = c.width;
const h = c.height;
ctx.clearRect(0, 0, w, h);
for (const p of particles) {
p.x += p.vx;
p.y += p.vy;
if (p.x < 0) p.x = w;
if (p.x > w) p.x = 0;
if (p.y < 0) p.y = h;
if (p.y > h) p.y = 0;
}
for (let i = 0; i < particles.length; i++) {
const a = particles[i]!;
ctx.beginPath();
ctx.arc(a.x, a.y, a.r, 0, Math.PI * 2);
ctx.fillStyle = "rgba(0,212,255,0.25)";
ctx.fill();
for (let j = i + 1; j < particles.length; j++) {
const b = particles[j]!;
const dx = a.x - b.x;
const dy = a.y - b.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 120) {
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = `rgba(0,212,255,${0.06 * (1 - dist / 120)})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
}
}
animFrame.value = requestAnimationFrame(tick);
}
onMounted(() => {
const timeTick = () => {
updateTime.value = new Date().toLocaleTimeString("zh-CN", { hour12: false });
};
timeTick();
window.setInterval(timeTick, 1000);
initParticles();
statsTimer = window.setInterval(updateStats, 3000);
updateStats();
window.addEventListener("resize", () => {
resizeCanvas();
spawnParticles();
});
});
onUnmounted(() => {
cancelAnimationFrame(animFrame.value);
clearInterval(statsTimer);
document.removeEventListener("fullscreenchange", onFullscreenChange);
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
}
});
</script>
<style scoped>
.screen-container {
--bg: #060b24;
--accent: #00d4ff;
--text: #b8c6e0;
--border: rgb(0 180 255 / 12%);
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
font-family: "PingFang SC", "Microsoft YaHei", monospace;
color: var(--text);
background: var(--bg);
}
.particle-canvas {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
}
.scan-line {
position: absolute;
left: 0;
z-index: 1;
width: 100%;
height: 1px;
pointer-events: none;
opacity: 0.08;
will-change: transform;
}
.scan-1 {
top: 0;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
animation: scanDown 6s linear infinite;
}
.scan-2 {
top: 100%;
background: linear-gradient(90deg, transparent, #7c3aed, transparent);
animation: scanDown 6s linear 3s infinite;
}
@keyframes scanDown {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-100vh);
}
}
/* ===== 背景网格 ===== */
.screen-container::after {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
content: "";
background-image:
linear-gradient(rgb(0 212 255 / 3%) 1px, transparent 1px),
linear-gradient(90deg, rgb(0 212 255 / 3%) 1px, transparent 1px);
background-size: 60px 60px;
}
/* ===== 顶部指标卡 ===== */
.stat-row {
position: relative;
z-index: 2;
display: flex;
flex-shrink: 0;
gap: 16px;
padding: 0 16px 16px;
}
.stat-card {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
padding: 18px 24px;
overflow: hidden;
background: linear-gradient(135deg, rgb(0 20 60 / 60%) 0%, rgb(0 10 40 / 50%) 100%);
border: 1px solid var(--border);
border-radius: 10px;
}
.stat-card::before {
position: absolute;
top: -1px;
right: 14px;
left: 14px;
height: 1px;
content: "";
background: linear-gradient(90deg, transparent, rgb(0 212 255 / 40%), transparent);
}
.stat-card::after {
position: absolute;
top: 4px;
left: 4px;
width: 8px;
height: 8px;
content: "";
border-top: 1px solid rgb(0 212 255 / 50%);
border-left: 1px solid rgb(0 212 255 / 50%);
}
.stat-val {
margin-bottom: 6px;
font-size: 28px;
font-weight: 800;
line-height: 1;
}
.stat-cyan {
color: #00d4ff;
}
.stat-purple {
color: #7c3aed;
}
.stat-green {
color: #10b981;
}
.stat-warn {
color: #f59e0b;
}
.stat-teal {
color: #14b8a6;
}
.stat-rose {
color: #f43f5e;
}
.stat-label {
margin-bottom: 6px;
font-size: 11px;
opacity: 0.4;
}
.stat-sub {
display: flex;
gap: 6px;
align-items: center;
font-size: 10px;
}
.stat-sub .up {
color: #10b981;
}
.stat-sub .down {
color: #f59e0b;
}
.stat-vs {
opacity: 0.3;
}
/* ===== Grid 主体 ===== */
.screen-grid {
position: relative;
z-index: 2;
display: grid;
flex: 1;
grid-template-rows: repeat(2, 1fr);
grid-template-columns: repeat(12, 1fr);
gap: 16px;
min-height: 0;
padding: 0 16px 16px;
}
.gp-r1 {
grid-row: 1;
}
.gp-r2 {
grid-row: 2;
}
.gp-c1-3 {
grid-column: 1 / 3;
}
.gp-c1-4 {
grid-column: 1 / 4;
}
.gp-c3-5 {
grid-column: 3 / 5;
}
.gp-c4-10 {
grid-column: 4 / 10;
}
.gp-c5-9 {
grid-column: 5 / 9;
}
.gp-c9-11 {
grid-column: 9 / 11;
}
.gp-c10-13 {
grid-column: 10 / 13;
}
.gp-c11-13 {
grid-column: 11 / 13;
}
/* ===== 共享面板 ===== */
:deep(.panel) {
position: relative;
display: flex;
flex-direction: column;
padding: 18px 16px;
overflow: hidden;
background: linear-gradient(180deg, rgb(0 30 80 / 55%) 0%, rgb(6 11 36 / 70%) 100%);
border: 1px solid var(--border);
border-radius: 14px;
}
:deep(.panel)::before {
position: absolute;
top: 0;
right: 20px;
left: 20px;
z-index: 1;
height: 1px;
pointer-events: none;
content: "";
background: linear-gradient(90deg, transparent, rgb(0 212 255 / 30%), transparent);
}
:deep(.panel)::after {
position: absolute;
inset: -1px;
z-index: 0;
pointer-events: none;
content: "";
background:
linear-gradient(to right, rgb(0 212 255 / 30%) 1px, transparent 1px) 0 0 / 12px 1px no-repeat,
linear-gradient(to bottom, rgb(0 212 255 / 30%) 1px, transparent 1px) 0 0 / 1px 12px no-repeat,
linear-gradient(to left, rgb(0 212 255 / 30%) 1px, transparent 1px) 100% 0 / 12px 1px no-repeat,
linear-gradient(to bottom, rgb(0 212 255 / 30%) 1px, transparent 1px) 100% 0 / 1px 12px
no-repeat,
linear-gradient(to right, rgb(0 212 255 / 30%) 1px, transparent 1px) 0 100% / 12px 1px no-repeat,
linear-gradient(to top, rgb(0 212 255 / 30%) 1px, transparent 1px) 0 100% / 1px 12px no-repeat,
linear-gradient(to left, rgb(0 212 255 / 30%) 1px, transparent 1px) 100% 100% / 12px 1px
no-repeat,
linear-gradient(to top, rgb(0 212 255 / 30%) 1px, transparent 1px) 100% 100% / 1px 12px
no-repeat;
border-radius: 14px;
}
:deep(.map-panel) {
padding: 0;
overflow: hidden;
}
:deep(.map-panel)::after {
background: none;
}
:deep(.map-panel > div) {
height: 100% !important;
padding: 12px;
}
:deep(#china-map) {
border-radius: 12px;
}
:deep(.panel-hd) {
display: flex;
flex-shrink: 0;
gap: 8px;
align-items: center;
padding-bottom: 12px;
margin-bottom: 10px;
font-size: 15px;
font-weight: 600;
letter-spacing: 1px;
border-bottom: 1px solid rgb(0 212 255 / 8%);
}
:deep(.dot) {
display: inline-block;
flex-shrink: 0;
width: 8px;
height: 8px;
border-radius: 50%;
box-shadow: 0 0 6px currentcolor;
}
:deep(.dot.accent) {
color: var(--accent);
background: var(--accent);
}
:deep(.dot.green) {
color: #10b981;
background: #10b981;
}
:deep(.dot.warn) {
color: #f59e0b;
background: #f59e0b;
}
:deep(.dot.purple) {
color: #7c3aed;
background: #7c3aed;
}
/* ===== 大卡片行 x6 ===== */
.card-row {
position: relative;
z-index: 2;
display: flex;
flex-shrink: 0;
gap: 16px;
height: 180px;
padding: 0 16px 16px;
}
.card-row > * {
flex: 1;
}
/* ===== 底部状态栏 ===== */
.bottom-bar {
position: relative;
z-index: 2;
display: flex;
flex-shrink: 0;
gap: 24px;
align-items: center;
padding: 10px 24px;
margin: 0 16px 16px;
font-size: 11px;
background: linear-gradient(90deg, rgb(0 20 60 / 50%) 0%, rgb(0 20 60 / 30%) 100%);
border: 1px solid rgb(0 180 255 / 10%);
border-radius: 8px;
opacity: 0.7;
}
.bb-item {
display: flex;
flex-shrink: 0;
gap: 6px;
align-items: center;
}
.bb-dot {
width: 6px;
height: 6px;
background: #10b981;
border-radius: 50%;
box-shadow: 0 0 6px #10b981;
}
.bb-ticker {
flex: 1;
overflow: hidden;
mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}
.ticker-track {
display: flex;
gap: 32px;
white-space: nowrap;
animation: tickerScroll 20s linear infinite;
}
.ticker-item {
flex-shrink: 0;
}
@keyframes tickerScroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
.bb-items-right {
display: flex;
flex-shrink: 0;
gap: 14px;
align-items: center;
}
.bb-meta {
display: flex;
gap: 4px;
align-items: center;
font-size: 10px;
opacity: 0.6;
}
.bb-meta-dot {
flex-shrink: 0;
width: 5px;
height: 5px;
border-radius: 50%;
}
.sr-cyan {
background: #00d4ff;
box-shadow: 0 0 5px #00d4ff;
}
.sr-green {
background: #10b981;
box-shadow: 0 0 5px #10b981;
}
.sr-purple {
background: #7c3aed;
box-shadow: 0 0 5px #7c3aed;
}
.sr-warn {
background: #f59e0b;
box-shadow: 0 0 5px #f59e0b;
}
</style>

@ -1,100 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot accent" />交易渠道占比</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "ChannelDonut" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
tooltip: {
trigger: "item" as const,
backgroundColor: "#0f143c",
borderColor: "#1a2050",
textStyle: { color: "#e0e6ff", fontSize: 10 },
formatter: "{b}: {c}万 ({d}%)",
},
legend: {
bottom: 0,
textStyle: { color: "#94a3b8", fontSize: 9 },
itemWidth: 8,
itemHeight: 8,
},
series: [
{
type: "pie",
radius: ["50%", "70%"],
center: ["50%", "45%"],
label: { show: false },
emphasis: {
scaleSize: 6,
label: { show: true, fontSize: 10, color: "#94a3b8", formatter: "{b}\n{d}%" },
},
itemStyle: { borderColor: "#060b24", borderWidth: 2 },
data: [
{ value: 48.5, name: "微信支付", itemStyle: { color: "#00d4ff" } },
{ value: 28.3, name: "支付宝", itemStyle: { color: "#7c3aed" } },
{ value: 15.2, name: "银联云闪付", itemStyle: { color: "#10b981" } },
{ value: 8.0, name: "其他", itemStyle: { color: "#f59e0b" } },
],
},
],
});
}
function handleResize() {
if (chart && !chart.isDisposed()) chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
const r1 = +(40 + Math.random() * 15).toFixed(1);
const r2 = +(22 + Math.random() * 12).toFixed(1);
const r3 = +(10 + Math.random() * 10).toFixed(1);
const r4 = +(100 - +r1 - +r2 - +r3).toFixed(1);
chart.setOption({
series: [{ data: [{ value: r1 }, { value: r2 }, { value: r3 }, { value: r4 }] }],
});
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 5000);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,77 +0,0 @@
<template>
<div class="panel p1 col-3">
<div class="panel-hd"><span class="dot accent" />数据概览</div>
<div class="stats-row">
<div v-for="c in overview" :key="c.label" class="stat-card" :style="{ borderColor: c.color }">
<div class="stat-icon" :style="{ background: c.color + '18', color: c.color }">
{{ c.icon }}
</div>
<div>
<span class="stat-val" :style="{ color: c.color }">{{ c.value }}</span>
<span class="stat-label">{{ c.label }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
defineOptions({ name: "DataOverview" });
const overview = ref([
{ label: "总用户数", value: "12,542", icon: "👤", color: "#00d4ff" },
{ label: "今日活跃", value: "387", icon: "⚡", color: "#10b981" },
{ label: "API 调用", value: "89.2K", icon: "🔗", color: "#7c3aed" },
{ label: "告警数", value: "2", icon: "⚠️", color: "#ef4444" },
]);
</script>
<style scoped>
.stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.stat-card {
display: flex;
gap: 10px;
align-items: center;
padding: 12px;
background: rgb(12 18 48 / 60%);
border: 1px solid;
border-radius: 8px;
transition:
transform 0.2s,
box-shadow 0.3s;
}
.stat-card:hover {
transform: translateY(-1px);
}
.stat-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
font-size: 18px;
border-radius: 8px;
}
.stat-val {
display: block;
font-size: 22px;
font-weight: 700;
line-height: 1.2;
}
.stat-label {
font-size: 12px;
opacity: 0.5;
}
</style>

@ -1,121 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot purple" />用户转化漏斗</div>
<div class="funnel-list">
<div v-for="(item, i) in data" :key="item.label" class="fn-item">
<div class="fn-row">
<span class="fn-idx" :style="{ background: colors[i] }">{{ i + 1 }}</span>
<span class="fn-label">{{ item.label }}</span>
<span class="fn-val">{{ item.value }}</span>
<span class="fn-rate" :style="{ color: colors[i] }">转化率 {{ item.rate }}%</span>
</div>
<div class="fn-bar-wrap">
<div class="fn-bar" :style="{ width: item.pct + '%', background: colors[i] }" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted, onUnmounted } from "vue";
defineOptions({ name: "FunnelChart" });
const colors = ["#00d4ff", "#7c3aed", "#10b981", "#f59e0b"];
const data = reactive([
{ label: "曝光", value: "12,850", rate: 100, pct: 100 },
{ label: "点击", value: "4,320", rate: 34, pct: 34 },
{ label: "下单", value: "1,845", rate: 14, pct: 14 },
{ label: "支付", value: "963", rate: 8, pct: 8 },
]);
let timer = 0;
function tick() {
const bases = [12850, 4320, 1845, 963];
const vals = bases.map((b, i) => Math.round(b + (Math.random() - 0.5) * b * 0.08 * (4 - i)));
const max = vals[0]!;
data.forEach((item, i) => {
item.value = vals[i]!.toLocaleString();
item.rate = Math.round((vals[i]! / max) * 100);
item.pct = (vals[i]! / max) * 100;
});
}
onMounted(() => {
tick();
timer = window.setInterval(tick, 5000);
});
onUnmounted(() => clearInterval(timer));
</script>
<style scoped>
.funnel-list {
display: flex;
flex: 1;
flex-direction: column;
gap: 5px;
justify-content: center;
}
.fn-item {
padding: 2px 0;
}
.fn-row {
display: flex;
gap: 6px;
align-items: center;
margin-bottom: 3px;
font-size: 10px;
}
.fn-idx {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
font-size: 8px;
font-weight: 700;
color: #fff;
border-radius: 3px;
}
.fn-label {
flex-shrink: 0;
width: 28px;
opacity: 0.5;
}
.fn-val {
flex: 1;
font-variant-numeric: tabular-nums;
text-align: right;
opacity: 0.8;
}
.fn-rate {
flex-shrink: 0;
width: 54px;
font-size: 9px;
font-weight: 600;
text-align: right;
}
.fn-bar-wrap {
height: 3px;
overflow: hidden;
background: rgb(26 40 80 / 40%);
border-radius: 2px;
}
.fn-bar {
height: 100%;
border-radius: 2px;
transition: width 0.8s;
}
</style>

@ -1,163 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd">
<span class="dot warn" />热门排行
<span class="rank-badge">实时</span>
</div>
<div class="rank-list">
<div v-for="(item, i) in list" :key="item.name" class="rank-row">
<span class="rank-idx" :class="'top' + (i + 1)">{{ i + 1 }}</span>
<span class="rank-name">{{ item.name }}</span>
<span class="rank-bar-wrap">
<span
class="rank-bar"
:style="{ width: item.ratio * 100 + '%', background: item.color }"
/>
</span>
<span class="rank-val">{{ item.value }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted, onUnmounted } from "vue";
defineOptions({ name: "HotRanking" });
const colors = [
"#00d4ff",
"#7c3aed",
"#10b981",
"#f59e0b",
"#ef4444",
"#ec4899",
"#06b6d4",
"#f97316",
];
const list = reactive([
{ name: "iPhone 15 Pro", value: "892单", ratio: 0, color: colors[0] },
{ name: "MacBook Air M3", value: "651单", ratio: 0, color: colors[1] },
{ name: "AirPods Pro 2", value: "534单", ratio: 0, color: colors[2] },
{ name: "iPad Mini 7", value: "412单", ratio: 0, color: colors[3] },
{ name: "Apple Watch S9", value: "298单", ratio: 0, color: colors[4] },
{ name: "Samsung S24 Ultra", value: "187单", ratio: 0, color: colors[5] },
{ name: "华为 Mate 60 Pro", value: "165单", ratio: 0, color: colors[6] },
{ name: "Sony WH-1000XM5", value: "142单", ratio: 0, color: colors[7] },
]);
let timer = 0;
function tick() {
const vals = list.map(() => Math.round(100 + Math.random() * 800));
const max = Math.max(...vals);
list.forEach((item, i) => {
item.value = vals[i]! + "单";
item.ratio = vals[i]! / max;
});
list.sort((a, b) => parseInt(b.value) - parseInt(a.value));
}
onMounted(() => {
tick();
timer = window.setInterval(tick, 5000);
});
onUnmounted(() => clearInterval(timer));
</script>
<style scoped>
.rank-list {
display: flex;
flex: 1;
flex-direction: column;
gap: 5px;
overflow: auto;
}
.rank-row {
display: flex;
gap: 8px;
align-items: center;
font-size: 11px;
}
.rank-idx {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
font-size: 10px;
font-weight: 700;
background: rgb(26 40 80 / 60%);
border-radius: 4px;
}
.rank-idx.top1 {
color: #00d4ff;
background: rgb(0 212 255 / 20%);
}
.rank-idx.top2 {
color: #7c3aed;
background: rgb(124 58 237 / 20%);
}
.rank-idx.top3 {
color: #10b981;
background: rgb(16 185 129 / 20%);
}
.rank-name {
flex-shrink: 0;
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rank-bar-wrap {
flex: 1;
height: 5px;
overflow: hidden;
background: rgb(26 40 80 / 40%);
border-radius: 3px;
}
.rank-bar {
display: block;
height: 100%;
border-radius: 3px;
transition: width 0.6s;
}
.rank-val {
flex-shrink: 0;
font-size: 11px;
font-variant-numeric: tabular-nums;
opacity: 0.7;
}
.rank-badge {
padding: 2px 8px;
margin-left: auto;
font-size: 10px;
color: #f59e0b;
background: rgb(245 158 11 / 15%);
border-radius: 10px;
opacity: 0.5;
animation: blink 2s infinite;
}
@keyframes blink {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
</style>

@ -1,99 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot warn" />服务健康度</div>
<div class="metric-list">
<div v-for="m in metrics" :key="m.label" class="metric-row">
<div class="mr-label">{{ m.label }}</div>
<div class="mr-value" :style="{ color: m.color }">
{{ m.value }}<span class="mr-unit">{{ m.unit }}</span>
</div>
<div class="mr-bar-wrap">
<div ref="barRefs" class="mr-bar" :style="{ width: m.pct + '%', background: m.color }" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted, onUnmounted } from "vue";
defineOptions({ name: "LiveMetrics" });
const metrics = reactive([
{ label: "QPS(每秒请求)", value: "3.2", unit: "K", color: "#00d4ff", pct: 0, base: 3200 },
{ label: "P99 耗时", value: "12", unit: "ms", color: "#7c3aed", pct: 0, base: 12 },
{ label: "错误率", value: "0.03", unit: "%", color: "#ef4444", pct: 0, base: 0.03 },
{ label: "服务可用率", value: "99.99", unit: "%", color: "#10b981", pct: 0, base: 99.99 },
]);
let timer = 0;
function tick() {
metrics[0]!.base = Math.round(2800 + Math.random() * 800);
metrics[1]!.base = Math.round(8 + Math.random() * 20);
metrics[2]!.base = +(0.01 + Math.random() * 0.08).toFixed(2);
metrics[3]!.base = +(99.95 + Math.random() * 0.05).toFixed(2);
metrics[0]!.value = (metrics[0]!.base / 1000).toFixed(1);
metrics[1]!.value = String(metrics[1]!.base);
metrics[2]!.value = String(metrics[2]!.base);
metrics[3]!.value = String(metrics[3]!.base);
metrics[0]!.pct = (metrics[0]!.base / 4000) * 100;
metrics[1]!.pct = (metrics[1]!.base / 30) * 100;
metrics[2]!.pct = (metrics[2]!.base / 0.1) * 100;
metrics[3]!.pct = (+(metrics[3]!.base - 99.9) / 0.1) * 100;
}
onMounted(() => {
tick();
timer = window.setInterval(tick, 3000);
});
onUnmounted(() => clearInterval(timer));
</script>
<style scoped>
.metric-list {
display: flex;
flex: 1;
flex-direction: column;
gap: 4px;
justify-content: center;
}
.metric-row {
padding: 2px 0;
}
.mr-label {
margin-bottom: 2px;
font-size: 10px;
opacity: 0.35;
}
.mr-value {
margin-bottom: 3px;
font-size: 18px;
font-weight: 700;
line-height: 1;
}
.mr-unit {
margin-left: 2px;
font-size: 10px;
font-weight: 400;
opacity: 0.4;
}
.mr-bar-wrap {
height: 2px;
overflow: hidden;
background: rgb(26 40 80 / 40%);
border-radius: 1px;
}
.mr-bar {
height: 100%;
border-radius: 1px;
transition: width 0.8s;
}
</style>

@ -1,125 +0,0 @@
<template>
<div class="mini-chart-panel">
<div class="mc-hd">库存周转天数</div>
<div class="mc-value" style="color: #14b8a6">{{ val }}<span class="mc-unit"></span></div>
<div class="progress-wrap">
<div class="progress-track">
<div
class="progress-fill"
:style="{ width: pct + '%', background: 'linear-gradient(90deg, #14b8a6, #06b6d4)' }"
/>
</div>
<div class="progress-markers">
<span>0</span><span>5</span><span>10</span><span>15</span><span>20</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
defineOptions({ name: "MiniInventory" });
const val = ref(8.5);
const pct = ref(42.5);
let timer = 0;
function tick() {
const n = +(6 + Math.random() * 6).toFixed(1);
val.value = n;
pct.value = (n / 20) * 100;
}
onMounted(() => {
tick();
timer = window.setInterval(tick, 4000);
});
onUnmounted(() => clearInterval(timer));
</script>
<style scoped>
.mini-chart-panel {
position: relative;
display: flex;
flex-direction: column;
padding: 10px 12px;
overflow: hidden;
background: linear-gradient(180deg, rgb(0 30 80 / 55%) 0%, rgb(6 11 36 / 70%) 100%);
border: 1px solid var(--border, rgb(0 180 255 / 12%));
border-radius: 10px;
}
.mini-chart-panel::after {
position: absolute;
top: 4px;
left: 4px;
width: 6px;
height: 6px;
content: "";
border-top: 1px solid rgb(20 184 166 / 40%);
border-left: 1px solid rgb(20 184 166 / 40%);
}
.mc-hd {
margin-bottom: 2px;
font-size: 10px;
opacity: 0.35;
}
.mc-value {
margin-bottom: 8px;
font-size: 16px;
font-weight: 700;
line-height: 1;
}
.mc-unit {
margin-left: 2px;
font-size: 9px;
font-weight: 400;
opacity: 0.4;
}
.progress-wrap {
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-end;
}
.progress-track {
position: relative;
height: 6px;
overflow: hidden;
background: rgb(26 40 80 / 40%);
border-radius: 3px;
}
.progress-fill {
position: relative;
height: 100%;
border-radius: 3px;
transition: width 0.8s;
}
.progress-fill::after {
position: absolute;
top: -2px;
right: 0;
width: 10px;
height: 10px;
content: "";
background: #14b8a6;
border-radius: 50%;
box-shadow: 0 0 8px #14b8a6;
}
.progress-markers {
display: flex;
justify-content: space-between;
margin-top: 3px;
font-size: 8px;
opacity: 0.25;
}
</style>

@ -1,118 +0,0 @@
<template>
<div class="mini-chart-panel">
<div class="mc-hd">已完成订单</div>
<div class="mc-value" style="color: #7c3aed">{{ val }}</div>
<div ref="chartRef" class="mc-chart" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "MiniOrders" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const val = ref(3856);
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
tooltip: { show: false },
series: [
{
type: "pie",
radius: ["55%", "78%"],
center: ["50%", "58%"],
label: { show: false },
itemStyle: { borderColor: "#060b24", borderWidth: 1 },
data: [
{ value: 3856, name: "已完成", itemStyle: { color: "#7c3aed" } },
{ value: 843, name: "处理中", itemStyle: { color: "#a78bfa" } },
{ value: 213, name: "待付款", itemStyle: { color: "#1a2050" } },
],
},
],
});
}
function tick() {
if (!chart || chart.isDisposed()) return;
const n = Math.round(3000 + Math.random() * 2000);
val.value = n;
chart.setOption({
series: [
{
data: [
{ value: n },
{ value: Math.round(500 + Math.random() * 600) },
{ value: Math.round(100 + Math.random() * 200) },
],
},
],
});
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 5000);
}
});
observer.observe(el);
});
onUnmounted(() => {
clearInterval(timer);
chart?.dispose();
});
</script>
<style scoped>
.mini-chart-panel {
position: relative;
display: flex;
flex-direction: column;
padding: 10px 12px;
overflow: hidden;
background: linear-gradient(180deg, rgb(0 30 80 / 55%) 0%, rgb(6 11 36 / 70%) 100%);
border: 1px solid var(--border, rgb(0 180 255 / 12%));
border-radius: 10px;
}
.mini-chart-panel::after {
position: absolute;
top: 4px;
left: 4px;
width: 6px;
height: 6px;
content: "";
border-top: 1px solid rgb(124 58 237 / 40%);
border-left: 1px solid rgb(124 58 237 / 40%);
}
.mc-hd {
margin-bottom: 2px;
font-size: 10px;
opacity: 0.35;
}
.mc-value {
margin-bottom: 0;
font-size: 16px;
font-weight: 700;
line-height: 1;
}
.mc-chart {
flex: 1;
min-height: 0;
}
</style>

@ -1,127 +0,0 @@
<template>
<div class="mini-chart-panel">
<div class="mc-hd">用户综合评分</div>
<div class="mc-value" style="color: #f43f5e">{{ val }}<span class="mc-unit"></span></div>
<div ref="chartRef" class="mc-chart" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "MiniRating" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const val = ref(92);
const indicators = [
{ name: "商品质量", max: 100 },
{ name: "服务态度", max: 100 },
{ name: "物流速度", max: 100 },
{ name: "性价比", max: 100 },
{ name: "包装完好", max: 100 },
];
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
radar: {
center: ["50%", "56%"],
radius: "60%",
indicator: indicators,
axisName: { color: "#94a3b8", fontSize: 7 },
splitArea: { areaStyle: { color: ["rgba(244,63,94,0.02)", "rgba(244,63,94,0.02)"] } },
splitLine: { lineStyle: { color: "#1a2050" } },
axisLine: { lineStyle: { color: "#1a2050" } },
},
series: [
{
type: "radar",
symbol: "none",
lineStyle: { color: "#f43f5e", width: 1.5 },
areaStyle: { color: "rgba(244,63,94,0.1)" },
data: [{ value: [96, 91, 88, 85, 94] }],
},
],
});
}
function tick() {
if (!chart || chart.isDisposed()) return;
const v = indicators.map(() => Math.round(80 + Math.random() * 20));
const avg = Math.round(v.reduce((a, b) => a + b, 0) / v.length);
val.value = avg;
chart.setOption({ series: [{ data: [{ value: v }] }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 5000);
}
});
observer.observe(el);
});
onUnmounted(() => {
clearInterval(timer);
chart?.dispose();
});
</script>
<style scoped>
.mini-chart-panel {
position: relative;
display: flex;
flex-direction: column;
padding: 10px 12px;
overflow: hidden;
background: linear-gradient(180deg, rgb(0 30 80 / 55%) 0%, rgb(6 11 36 / 70%) 100%);
border: 1px solid var(--border, rgb(0 180 255 / 12%));
border-radius: 10px;
}
.mini-chart-panel::after {
position: absolute;
top: 4px;
left: 4px;
width: 6px;
height: 6px;
content: "";
border-top: 1px solid rgb(244 63 94 / 40%);
border-left: 1px solid rgb(244 63 94 / 40%);
}
.mc-hd {
margin-bottom: 2px;
font-size: 10px;
opacity: 0.35;
}
.mc-value {
margin-bottom: 0;
font-size: 16px;
font-weight: 700;
line-height: 1;
}
.mc-unit {
margin-left: 2px;
font-size: 9px;
font-weight: 400;
opacity: 0.4;
}
.mc-chart {
flex: 1;
min-height: 0;
}
</style>

@ -1,123 +0,0 @@
<template>
<div class="mini-chart-panel">
<div class="mc-hd">售后退款率</div>
<div class="mc-value" style="color: #f59e0b">{{ val }}<span class="mc-unit">%</span></div>
<div ref="chartRef" class="mc-chart" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "MiniRefund" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const val = ref(2.1);
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
grid: { top: 8, right: 4, bottom: 2, left: 2 },
xAxis: {
type: "category",
show: false,
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
},
yAxis: { type: "value", show: false, min: 0, max: 4 },
series: [
{
type: "bar",
barWidth: 5,
barGap: "30%",
itemStyle: {
borderRadius: [2, 2, 0, 0],
color: (p: any) => (p.dataIndex < 5 ? "#f59e0b" : "#ef4444"),
},
data: [2.4, 2.2, 2.5, 2.1, 2.3, 2.0, 2.1],
},
],
});
}
function tick() {
if (!chart || chart.isDisposed()) return;
const n = +(1.5 + Math.random() * 1.5).toFixed(1);
val.value = n;
const d = chart.getOption() as { series: [{ data: number[] }] };
const arr = d.series[0].data;
arr.push(n);
arr.shift();
chart.setOption({ series: [{ data: arr }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 5000);
}
});
observer.observe(el);
});
onUnmounted(() => {
clearInterval(timer);
chart?.dispose();
});
</script>
<style scoped>
.mini-chart-panel {
position: relative;
display: flex;
flex-direction: column;
padding: 10px 12px;
overflow: hidden;
background: linear-gradient(180deg, rgb(0 30 80 / 55%) 0%, rgb(6 11 36 / 70%) 100%);
border: 1px solid var(--border, rgb(0 180 255 / 12%));
border-radius: 10px;
}
.mini-chart-panel::after {
position: absolute;
top: 4px;
left: 4px;
width: 6px;
height: 6px;
content: "";
border-top: 1px solid rgb(245 158 11 / 40%);
border-left: 1px solid rgb(245 158 11 / 40%);
}
.mc-hd {
margin-bottom: 2px;
font-size: 10px;
opacity: 0.35;
}
.mc-value {
margin-bottom: 0;
font-size: 16px;
font-weight: 700;
line-height: 1;
}
.mc-unit {
margin-left: 2px;
font-size: 9px;
font-weight: 400;
opacity: 0.4;
}
.mc-chart {
flex: 1;
min-height: 0;
}
</style>

@ -1,134 +0,0 @@
<template>
<div class="mini-chart-panel">
<div class="mc-hd">当日交易额</div>
<div class="mc-value" style="color: #00d4ff">¥{{ val }}<span class="mc-unit"></span></div>
<div ref="chartRef" class="mc-chart" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "MiniRevenue" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const val = ref(128.6);
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
series: [
{
type: "gauge",
startAngle: 210,
endAngle: -30,
center: ["50%", "58%"],
radius: "78%",
min: 0,
max: 200,
axisLine: {
lineStyle: {
width: 6,
color: [
[0.5, "#00d4ff"],
[0.8, "#7c3aed"],
[1, "#ef4444"],
],
},
},
pointer: { length: "55%", width: 3, itemStyle: { color: "#00d4ff" } },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
detail: {
valueAnimation: true,
fontSize: 14,
fontWeight: 700,
color: "#00d4ff",
offsetCenter: [0, "42%"],
formatter: "",
},
data: [{ value: 128.6 }],
},
],
});
}
function tick() {
if (!chart || chart.isDisposed()) return;
const n = +(105 + Math.random() * 50).toFixed(1);
val.value = n;
chart.setOption({ series: [{ data: [{ value: n }] }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 5000);
}
});
observer.observe(el);
});
onUnmounted(() => {
clearInterval(timer);
chart?.dispose();
});
</script>
<style scoped>
.mini-chart-panel {
position: relative;
display: flex;
flex-direction: column;
padding: 10px 12px;
overflow: hidden;
background: linear-gradient(180deg, rgb(0 30 80 / 55%) 0%, rgb(6 11 36 / 70%) 100%);
border: 1px solid var(--border, rgb(0 180 255 / 12%));
border-radius: 10px;
}
.mini-chart-panel::after {
position: absolute;
top: 4px;
left: 4px;
width: 6px;
height: 6px;
content: "";
border-top: 1px solid rgb(0 212 255 / 40%);
border-left: 1px solid rgb(0 212 255 / 40%);
}
.mc-hd {
margin-bottom: 2px;
font-size: 10px;
opacity: 0.35;
}
.mc-value {
margin-bottom: 0;
font-size: 16px;
font-weight: 700;
line-height: 1;
}
.mc-unit {
margin-left: 2px;
font-size: 9px;
font-weight: 400;
opacity: 0.4;
}
.mc-chart {
flex: 1;
min-height: 0;
}
</style>

@ -1,117 +0,0 @@
<template>
<div class="mini-chart-panel">
<div class="mc-hd">页面访问量(PV)</div>
<div class="mc-value" style="color: #10b981">{{ val }}</div>
<div ref="chartRef" class="mc-chart" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "MiniTraffic" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const val = ref(5210);
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
grid: { top: 8, right: 4, bottom: 2, left: 2 },
xAxis: { type: "category", show: false, data: ["", "", "", "", "", ""] },
yAxis: { type: "value", show: false },
series: [
{
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 4,
lineStyle: { color: "#10b981", width: 2 },
itemStyle: { color: "#10b981" },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(16,185,129,0.25)" },
{ offset: 1, color: "rgba(16,185,129,0)" },
]),
},
data: [3800, 4200, 4500, 4800, 5100, 5210],
},
],
});
}
function tick() {
if (!chart || chart.isDisposed()) return;
const n = Math.round(4000 + Math.random() * 2500);
val.value = n;
const d = chart.getOption() as { series: [{ data: number[] }] };
const arr = d.series[0].data;
arr.push(n);
arr.shift();
chart.setOption({ series: [{ data: arr }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 4000);
}
});
observer.observe(el);
});
onUnmounted(() => {
clearInterval(timer);
chart?.dispose();
});
</script>
<style scoped>
.mini-chart-panel {
position: relative;
display: flex;
flex-direction: column;
padding: 10px 12px;
overflow: hidden;
background: linear-gradient(180deg, rgb(0 30 80 / 55%) 0%, rgb(6 11 36 / 70%) 100%);
border: 1px solid var(--border, rgb(0 180 255 / 12%));
border-radius: 10px;
}
.mini-chart-panel::after {
position: absolute;
top: 4px;
left: 4px;
width: 6px;
height: 6px;
content: "";
border-top: 1px solid rgb(16 185 129 / 40%);
border-left: 1px solid rgb(16 185 129 / 40%);
}
.mc-hd {
margin-bottom: 2px;
font-size: 10px;
opacity: 0.35;
}
.mc-value {
margin-bottom: 0;
font-size: 16px;
font-weight: 700;
line-height: 1;
}
.mc-chart {
flex: 1;
min-height: 0;
}
</style>

@ -1,105 +0,0 @@
<template>
<div class="panel p1 col-3 row-2">
<div class="panel-hd"><span class="dot warn" />模块热度 TOP10</div>
<div class="rank-list">
<div v-for="(r, i) in ranks" :key="r.name" class="rank-row">
<span class="rank-no" :class="'rk-' + (i + 1)">{{ i + 1 }}</span>
<span class="rank-name">{{ r.name }}</span>
<span class="rank-bar-wrap"
><span class="rank-bar" :style="{ width: r.pct + '%', background: rankColor(i) }"
/></span>
<span class="rank-val">{{ r.val }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
defineOptions({ name: "ModuleRanking" });
const ranks = ref([
{ name: "用户管理", val: 1542, pct: 100 },
{ name: "操作日志", val: 892, pct: 78 },
{ name: "菜单管理", val: 197, pct: 55 },
{ name: "AI 助手", val: 156, pct: 42 },
{ name: "代码生成", val: 38, pct: 35 },
{ name: "角色管理", val: 45, pct: 28 },
{ name: "字典管理", val: 56, pct: 22 },
{ name: "文件管理", val: 128, pct: 18 },
{ name: "部门管理", val: 12, pct: 12 },
{ name: "定时任务", val: 24, pct: 8 },
]);
const RANK_COLORS = ["#ef4444", "#f59e0b", "#3b82f6", "#00d4ff", "#7c3aed", "#10b981"];
function rankColor(i: number) {
return RANK_COLORS[i % RANK_COLORS.length];
}
</script>
<style scoped>
.rank-list {
display: flex;
flex-direction: column;
gap: 5px;
overflow: auto;
}
.rank-row {
display: flex;
gap: 6px;
align-items: center;
font-size: 12px;
}
.rank-no {
width: 18px;
height: 18px;
font-size: 10px;
font-weight: 700;
background: rgb(26 32 80 / 60%);
border-radius: 4px;
}
.rk-1 {
color: #fff;
background: #ef4444 !important;
}
.rk-2 {
color: #fff;
background: #f59e0b !important;
}
.rk-3 {
color: #fff;
background: #3b82f6 !important;
}
.rank-name {
flex-shrink: 0;
width: 60px;
}
.rank-bar-wrap {
flex: 1;
height: 6px;
overflow: hidden;
background: rgb(26 32 80 / 60%);
border-radius: 3px;
}
.rank-bar {
height: 100%;
border-radius: 3px;
transition: width 0.6s;
}
.rank-val {
width: 30px;
font-variant-numeric: tabular-nums;
text-align: right;
}
</style>

@ -1,67 +0,0 @@
<template>
<div class="panel p1 col-2">
<div class="panel-hd"><span class="dot accent" />在线用户</div>
<div class="online-users">
<div class="online-count">{{ onlineUsers }}</div>
<div class="online-label">当前在线</div>
<div class="online-trend">
<span :class="onlineTrend >= 0 ? 'positive' : 'negative'"
>{{ onlineTrend >= 0 ? "+" : "" }}{{ onlineTrend }}%</span
>
<span class="trend-label">较昨日</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
defineOptions({ name: "OnlineUserCard" });
const onlineUsers = ref(387);
const onlineTrend = ref(15.6);
</script>
<style scoped>
.online-users {
padding: 20px 0;
text-align: center;
}
.online-count {
font-size: 48px;
font-weight: 700;
color: #00d4ff;
}
.online-label {
display: block;
margin-top: 4px;
font-size: 12px;
opacity: 0.5;
}
.online-trend {
margin-top: 10px;
}
.online-trend span:first-child {
font-size: 16px;
font-weight: 600;
}
.online-trend .positive {
color: #10b981;
}
.online-trend .negative {
color: #ef4444;
}
.trend-label {
margin-left: 4px;
font-size: 12px;
opacity: 0.5;
}
</style>

@ -1,106 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot accent" />订单状态</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const chartRef = ref<HTMLDivElement>();
import * as echarts from "echarts";
defineOptions({ name: "OrderStatusChart" });
let chart: echarts.ECharts | null = null;
let timer = 0;
const textStyle = { color: "#94a3b8" };
const categories = ["待付款", "待发货", "已发货", "已完成", "已取消"];
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
grid: { top: 20, right: 20, bottom: 30, left: 60 },
xAxis: {
type: "category",
data: categories,
axisLine: { lineStyle: { color: "#1a2050" } },
axisLabel: textStyle,
},
yAxis: {
type: "value",
splitLine: { lineStyle: { color: "#1a2050", type: "dashed" } },
axisLabel: textStyle,
},
tooltip: {
trigger: "axis" as const,
backgroundColor: "#0f143c",
borderColor: "#1a2050",
textStyle: { color: "#e0e6ff" },
},
series: [
{
type: "bar",
data: [45, 128, 256, 892, 32],
itemStyle: {
borderRadius: [4, 4, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#7c3aed" },
{ offset: 1, color: "rgba(124,58,237,0.3)" },
]),
},
barWidth: "50%",
},
],
});
}
function handleResize() {
if (!chart || chart.isDisposed()) return;
chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
const data = [
Math.round(20 + Math.random() * 60),
Math.round(60 + Math.random() * 150),
Math.round(100 + Math.random() * 300),
Math.round(500 + Math.random() * 600),
Math.round(10 + Math.random() * 50),
];
chart.setOption({ series: [{ data }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 3500);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,95 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot green" />热销品类排行 TOP6</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "ProductBar" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const products = ["手机数码", "电脑办公", "家用电器", "美妆护肤", "服饰鞋包", "食品生鲜"];
const textStyle = { color: "#94a3b8", fontSize: 10 };
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
grid: { top: 5, right: 40, bottom: 10, left: 10, containLabel: true },
xAxis: { type: "value", splitLine: { lineStyle: { color: "#1a2050" } }, axisLabel: textStyle },
yAxis: {
type: "category",
data: products,
inverse: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { ...textStyle, width: 64, overflow: "truncate" },
},
series: [
{
type: "bar",
barWidth: 10,
itemStyle: {
borderRadius: [0, 3, 3, 0],
color: (p: any) =>
["#00d4ff", "#7c3aed", "#10b981", "#f59e0b", "#ef4444", "#ec4899"][p.dataIndex % 6],
},
label: {
show: true,
position: "right",
color: "#94a3b8",
fontSize: 10,
formatter: "{c}单",
},
data: [892, 651, 534, 412, 298, 187],
},
],
});
}
function handleResize() {
if (chart && !chart.isDisposed()) chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
const data = products.map(() => Math.round(120 + Math.random() * 800));
chart.setOption({ series: [{ data }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 4000);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,136 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd">
<span class="dot accent" />实时操作日志
<span class="msg-count">NEW</span>
</div>
<div class="msg-scroll">
<div class="msg-inner">
<div v-for="(m, i) in messagesDuplicated" :key="i" class="msg-row">
<span class="msg-time">{{ m.time }}</span>
<span class="msg-tag" :class="m.tag">{{ m.tagText }}</span>
<span>{{ m.text }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: "RealtimeMessages" });
const messages = [
{
time: "22:30",
tag: "order",
tagText: "交易",
text: "商户「星辰科技」完成大额订单支付 ¥86,400",
},
{ time: "22:15", tag: "system", tagText: "系统", text: "数据备份定时任务执行完成,耗时 12.3s" },
{ time: "21:52", tag: "audit", tagText: "审计", text: "运营主管 zhangsan 导出本月交易报表" },
{ time: "21:30", tag: "deploy", tagText: "部署", text: "v3.2.1 版本灰度发布至华东区节点" },
{ time: "21:05", tag: "notice", tagText: "公告", text: "系统公告: 6月1日进行数据库扩容维护" },
{
time: "20:40",
tag: "alarm",
tagText: "告警",
text: "[已恢复] 华南区 API 网关延迟突增至 320ms",
},
{ time: "20:15", tag: "order", tagText: "交易", text: "退款工单 #TK-2024-08921 已自动处理完成" },
{
time: "19:52",
tag: "system",
tagText: "系统",
text: "缓存集群节点 node-07 内存使用率降至 62%",
},
];
const messagesDuplicated = [...messages, ...messages];
</script>
<style scoped>
.msg-scroll {
position: relative;
flex: 1;
min-width: 0;
overflow: hidden;
}
.msg-inner {
width: 100%;
animation: scrollUp 28s linear infinite;
}
.msg-row {
display: flex;
gap: 6px;
align-items: center;
padding: 5px 0;
font-size: 11px;
border-bottom: 1px solid rgb(26 40 80 / 30%);
opacity: 0.75;
}
.msg-time {
flex-shrink: 0;
width: 38px;
font-variant-numeric: tabular-nums;
opacity: 0.4;
}
.msg-tag {
flex-shrink: 0;
padding: 1px 4px;
font-size: 9px;
border-radius: 3px;
}
.msg-tag.order {
color: #00d4ff;
background: rgb(0 212 255 / 15%);
}
.msg-tag.system {
color: #7c3aed;
background: rgb(124 58 237 / 15%);
}
.msg-tag.audit {
color: #10b981;
background: rgb(16 185 129 / 15%);
}
.msg-tag.deploy {
color: #f59e0b;
background: rgb(245 158 11 / 15%);
}
.msg-tag.notice {
color: #00d4ff;
background: rgb(0 212 255 / 15%);
}
.msg-tag.alarm {
color: #ef4444;
background: rgb(239 68 68 / 15%);
}
.msg-count {
padding: 2px 6px;
margin-left: auto;
font-size: 10px;
color: #00d4ff;
background: rgb(0 212 255 / 15%);
border-radius: 10px;
opacity: 0.6;
}
@keyframes scrollUp {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
</style>

@ -1,127 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot accent" />实时销售分时</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "RealtimeSales" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const textStyle = { color: "#94a3b8" };
const hours = [
"08:00",
"09:00",
"10:00",
"11:00",
"12:00",
"13:00",
"14:00",
"15:00",
"16:00",
"17:00",
"18:00",
"19:00",
"20:00",
];
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
grid: { top: 20, right: 30, bottom: 30, left: 55 },
xAxis: {
type: "category",
data: hours,
axisLine: { lineStyle: { color: "#1a2050" } },
axisLabel: { ...textStyle, rotate: 30 },
},
yAxis: {
type: "value",
name: "万元",
nameTextStyle: { color: "#94a3b8", fontSize: 11 },
splitLine: { lineStyle: { color: "#1a2050", type: "dashed" } },
axisLabel: textStyle,
},
tooltip: {
trigger: "axis" as const,
backgroundColor: "#0f143c",
borderColor: "#1a2050",
textStyle: { color: "#e0e6ff" },
},
legend: { textStyle, top: 0 },
series: [
{
name: "销售额",
type: "bar",
barWidth: "60%",
itemStyle: {
borderRadius: [4, 4, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#00d4ff" },
{ offset: 1, color: "rgba(0,212,255,0.25)" },
]),
},
data: [2.1, 3.5, 4.2, 5.8, 3.2, 2.8, 4.5, 5.1, 6.3, 8.2, 4.8, 3.2, 2.5],
},
{
name: "订单数",
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 5,
lineStyle: { color: "#f59e0b", width: 2 },
itemStyle: { color: "#f59e0b" },
data: [18, 32, 45, 52, 35, 28, 42, 48, 55, 72, 48, 35, 22],
},
],
});
}
function handleResize() {
if (chart && !chart.isDisposed()) chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
const sales = hours.map(() => +(1.5 + Math.random() * 7).toFixed(1));
const orders = hours.map(() => Math.round(15 + Math.random() * 60));
chart.setOption({ series: [{ data: sales }, { data: orders }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 3500);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,108 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot green" />实时流量</div>
<div class="traffic-grid">
<div v-for="item in data" :key="item.label" class="tr-card">
<div class="tr-label">{{ item.label }}</div>
<div class="tr-value" :style="{ color: item.color }">
{{ item.value }}<span class="tr-unit">{{ item.unit }}</span>
</div>
<div class="tr-bar-wrap">
<div class="tr-bar" :style="{ width: item.pct + '%', background: item.color }" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted, onUnmounted } from "vue";
defineOptions({ name: "RealtimeTraffic" });
const data = reactive([
{ label: "PV", value: "8,350", unit: "", color: "#00d4ff", pct: 0, base: 8350 },
{ label: "UV", value: "3,216", unit: "", color: "#7c3aed", pct: 0, base: 3216 },
{ label: "IP", value: "2,048", unit: "", color: "#10b981", pct: 0, base: 2048 },
{ label: "跳出率", value: "32.5", unit: "%", color: "#f59e0b", pct: 0, base: 32.5 },
]);
let timer = 0;
function tick() {
data[0]!.base += Math.round((Math.random() - 0.5) * 120);
data[1]!.base += Math.round((Math.random() - 0.5) * 60);
data[2]!.base += Math.round((Math.random() - 0.5) * 40);
data[3]!.base = +(30 + Math.random() * 8).toFixed(1);
data[0]!.value = data[0]!.base.toLocaleString();
data[1]!.value = data[1]!.base.toLocaleString();
data[2]!.value = data[2]!.base.toLocaleString();
data[3]!.value = data[3]!.base.toFixed(1);
const max = Math.max(data[0]!.base, data[1]!.base, data[2]!.base);
data[0]!.pct = (data[0]!.base / max) * 100;
data[1]!.pct = (data[1]!.base / max) * 100;
data[2]!.pct = (data[2]!.base / max) * 100;
data[3]!.pct = +data[3]!.value;
}
onMounted(() => {
tick();
timer = window.setInterval(tick, 3000);
});
onUnmounted(() => clearInterval(timer));
</script>
<style scoped>
.traffic-grid {
display: grid;
flex: 1;
grid-template-rows: 1fr 1fr;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.tr-card {
display: flex;
flex-direction: column;
justify-content: center;
padding: 10px 12px;
background: linear-gradient(135deg, rgb(0 212 255 / 4%) 0%, rgb(0 20 60 / 30%) 100%);
border: 1px solid rgb(0 180 255 / 6%);
border-radius: 8px;
}
.tr-label {
margin-bottom: 2px;
font-size: 10px;
opacity: 0.35;
}
.tr-value {
display: flex;
gap: 2px;
align-items: baseline;
margin-bottom: 4px;
font-size: 18px;
font-weight: 700;
line-height: 1;
}
.tr-unit {
font-size: 10px;
font-weight: 400;
opacity: 0.4;
}
.tr-bar-wrap {
height: 2px;
overflow: hidden;
background: rgb(26 40 80 / 40%);
border-radius: 1px;
}
.tr-bar {
height: 100%;
border-radius: 1px;
transition: width 0.8s;
}
</style>

@ -1,90 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot accent" />地域分布</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "RegionDistributionChart" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
const textStyle = { color: "#94a3b8" };
function initChart() {
if (!chartRef.value) return;
chart = echarts.init(chartRef.value);
chart.setOption({
grid: { top: 20, right: 20, bottom: 30, left: 50 },
xAxis: {
type: "category",
data: ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "南京"],
axisLabel: { color: "#94a3b8", rotate: 30 },
axisLine: { lineStyle: { color: "#1a2050" } },
},
yAxis: {
type: "value",
splitLine: { lineStyle: { color: "#1a2050", type: "dashed" } },
axisLabel: textStyle,
},
tooltip: {
trigger: "axis" as const,
backgroundColor: "#0f143c",
borderColor: "#1a2050",
textStyle: { color: "#e0e6ff" },
},
series: [
{
type: "bar",
data: [280, 220, 180, 150, 130, 95, 80, 65],
itemStyle: {
borderRadius: [4, 4, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#00d4ff" },
{ offset: 1, color: "rgba(0,212,255,0.3)" },
]),
},
barWidth: 22,
},
],
});
}
function handleResize() {
if (!chart || chart.isDisposed()) return;
chart.resize();
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart();
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,80 +0,0 @@
<template>
<div class="panel p1 col-3">
<div class="panel-hd"><span class="dot green" />销售统计</div>
<div class="sales-cards">
<div class="sales-card">
<span class="sales-label">今日销售额</span>
<span class="sales-value">¥{{ formatNumber(todaySales) }}</span>
<span class="sales-change positive">+12.5%</span>
</div>
<div class="sales-card">
<span class="sales-label">订单数量</span>
<span class="sales-value">{{ todayOrders }}</span>
<span class="sales-change positive">+8.3%</span>
</div>
<div class="sales-card">
<span class="sales-label">转化率</span>
<span class="sales-value">{{ conversionRate }}%</span>
<span class="sales-change negative">-2.1%</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
defineOptions({ name: "SalesStatCards" });
const todaySales = ref(125860);
const todayOrders = ref(328);
const conversionRate = ref(3.2);
function formatNumber(num: number): string {
return num.toLocaleString();
}
</script>
<style scoped>
.sales-cards {
display: flex;
flex-direction: column;
gap: 10px;
}
.sales-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: rgb(12 18 48 / 60%);
border-radius: 8px;
}
.sales-label {
font-size: 12px;
opacity: 0.6;
}
.sales-value {
font-size: 18px;
font-weight: 600;
color: #e0e6ff;
}
.sales-change {
padding: 2px 6px;
font-size: 12px;
border-radius: 4px;
}
.sales-change.positive {
color: #10b981;
background: rgb(16 185 129 / 20%);
}
.sales-change.negative {
color: #ef4444;
background: rgb(239 68 68 / 20%);
}
</style>

@ -1,113 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot accent" />销售趋势分析</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const chartRef = ref<HTMLDivElement>();
import * as echarts from "echarts";
defineOptions({ name: "SalesTrendChart" });
let chart: echarts.ECharts | null = null;
let timer = 0;
const textStyle = { color: "#94a3b8" };
const tooltip = {
trigger: "axis" as const,
backgroundColor: "#0f143c",
borderColor: "#1a2050",
textStyle: { color: "#e0e6ff" },
};
const xData = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
grid: { top: 20, right: 30, bottom: 30, left: 50 },
xAxis: {
type: "category",
data: xData,
axisLine: { lineStyle: { color: "#1a2050" } },
axisLabel: textStyle,
},
yAxis: {
type: "value",
splitLine: { lineStyle: { color: "#1a2050", type: "dashed" } },
axisLabel: textStyle,
},
tooltip,
legend: { textStyle, top: 0 },
series: [
{
name: "订单金额",
type: "bar",
stack: "total",
data: [120, 132, 101, 134, 190, 230, 210],
itemStyle: { color: "#00d4ff" },
},
{
name: "退款金额",
type: "bar",
stack: "total",
data: [20, 32, 11, 34, 90, 30, 10],
itemStyle: { color: "#ef4444" },
},
{
name: "实际收入",
type: "bar",
stack: "total",
data: [100, 100, 90, 100, 100, 200, 200],
itemStyle: { color: "#10b981" },
},
],
});
}
function handleResize() {
if (!chart || chart.isDisposed()) return;
chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
const d1 = xData.map(() => Math.round(80 + Math.random() * 180));
const d2 = d1.map((v) => Math.round(v * (0.05 + Math.random() * 0.25)));
const d3 = d1.map((v, i) => Math.round(v - d2[i]!));
chart.setOption({ series: [{ data: d1 }, { data: d2 }, { data: d3 }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 4000);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,38 +0,0 @@
<template>
<footer class="screen-footer">
<span>CPU {{ cpu }}%</span><span class="sep"></span> <span>内存 {{ mem }}%</span
><span class="sep"></span> <span>磁盘 {{ disk }}%</span><span class="sep"></span>
<span>网络延迟 {{ networkLatency }}ms</span><span class="sep"></span>
<span>{{ uptime }}</span>
</footer>
</template>
<script setup lang="ts">
import { ref } from "vue";
defineOptions({ name: "ScreenFooter" });
const cpu = ref(42);
const mem = ref(61);
const disk = ref(35);
const uptime = ref("7d 14h 32m");
const networkLatency = ref(12);
</script>
<style scoped>
.screen-footer {
position: relative;
z-index: 10;
display: flex;
gap: 12px;
justify-content: center;
padding: 10px 0;
font-size: 12px;
border-top: 1px solid rgb(26 40 80 / 60%);
opacity: 0.4;
}
.sep {
opacity: 0.3;
}
</style>

@ -1,152 +0,0 @@
<template>
<header class="screen-header">
<div class="header-left">
<span class="dot" :class="{ pulse: online }" />
<span class="text-xs">SYSTEM ONLINE</span>
</div>
<h1 class="screen-title">
<span class="deco-line" />
FastapiAdmin · 智能运营数据监控平台
<span class="deco-line" />
</h1>
<div class="header-right">
<button class="fullscreen-btn" :title="isFs ? '退出全屏' : '进入全屏'" @click="handleToggle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="fs-icon">
<path
v-if="isFs"
d="M4 14h2v2h2v2H4v-4zm12 0h2v2h2v2h-4v-4zM4 4h4v2H6v2H4V4zm14 0h4v4h-2V6h-2V4z"
/>
<path
v-else
d="M4 4h4v2H6v2H4V4zm14 0h4v4h-2V6h-2V4zM4 20h4v-2H6v-2H4v4zm14 0h4v-4h-2v2h-2v2z"
/>
</svg>
</button>
<span class="header-time font-mono text-sm">{{ currentTime }}</span>
</div>
</header>
</template>
<script setup lang="ts">
import { ref, inject, onMounted, onUnmounted } from "vue";
defineOptions({ name: "ScreenHeader" });
const toggleFullscreen = inject<() => void>("toggleFullscreen", () => {});
const isFs = inject("isFullscreen", ref(false));
const online = ref(true);
const currentTime = ref("");
let timer = 0;
onMounted(() => {
const tick = () => {
currentTime.value = new Date().toLocaleString("zh-CN", { hour12: false });
};
tick();
timer = window.setInterval(tick, 1000);
});
onUnmounted(() => clearInterval(timer));
function handleToggle() {
toggleFullscreen();
}
</script>
<style scoped>
.screen-header {
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 32px 14px;
margin-bottom: 12px;
border-bottom: 1px solid rgb(26 40 80 / 60%);
}
.header-left {
display: flex;
gap: 8px;
align-items: center;
width: 140px;
font-size: 12px;
opacity: 0.7;
}
.header-right {
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-end;
width: 200px;
}
.header-time {
font-variant-numeric: tabular-nums;
opacity: 0.7;
}
.dot {
display: inline-block;
flex-shrink: 0;
width: 8px;
height: 8px;
background: #00d4ff;
border-radius: 50%;
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 8px #00d4ff;
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.screen-title {
flex: 1;
font-size: 20px;
font-weight: 700;
text-align: center;
letter-spacing: 4px;
text-shadow: 0 0 30px rgb(0 212 255 / 50%);
}
.deco-line {
display: inline-block;
width: 40px;
height: 1px;
margin: 0 12px;
vertical-align: middle;
background: linear-gradient(90deg, transparent, #00d4ff, transparent);
}
.fullscreen-btn {
display: flex;
align-items: center;
padding: 4px 8px;
color: var(--accent, #00d4ff);
cursor: pointer;
background: rgb(0 212 255 / 10%);
border: 1px solid rgb(0 212 255 / 30%);
border-radius: 6px;
transition: background 0.2s;
}
.fullscreen-btn:hover {
background: rgb(0 212 255 / 20%);
}
.fs-icon {
width: 16px;
height: 16px;
}
</style>

@ -1,78 +0,0 @@
<template>
<div class="panel p1 col-3">
<div class="panel-hd"><span class="dot green" />服务器状态</div>
<div class="server-status">
<div v-for="server in servers" :key="server.name" class="server-item">
<div class="server-header">
<span class="server-name">{{ server.name }}</span>
<span class="server-status-dot" :class="server.status" />
</div>
<div class="server-metrics">
<span>CPU: {{ server.cpu }}%</span>
<span>内存: {{ server.memory }}%</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
defineOptions({ name: "ServerStatusList" });
const servers = ref([
{ name: "API Server-01", status: "online", cpu: 45, memory: 62 },
{ name: "API Server-02", status: "online", cpu: 38, memory: 58 },
{ name: "DB Server-01", status: "online", cpu: 25, memory: 72 },
{ name: "Redis Server", status: "online", cpu: 15, memory: 45 },
]);
</script>
<style scoped>
.server-status {
display: flex;
flex-direction: column;
gap: 10px;
overflow: auto;
}
.server-item {
padding: 10px;
background: rgb(12 18 48 / 60%);
border-radius: 8px;
}
.server-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.server-name {
font-size: 12px;
}
.server-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.server-status-dot.online {
background: #10b981;
box-shadow: 0 0 8px #10b981;
}
.server-status-dot.offline {
background: #ef4444;
}
.server-metrics {
display: flex;
gap: 12px;
font-size: 11px;
opacity: 0.6;
}
</style>

@ -1,129 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot green" />API 响应时长(均值)</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "ServiceLevel" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const textStyle = { color: "#94a3b8" };
const xData = [
"08:00",
"09:00",
"10:00",
"11:00",
"12:00",
"13:00",
"14:00",
"15:00",
"16:00",
"17:00",
];
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
grid: { top: 20, right: 25, bottom: 25, left: 45 },
xAxis: {
type: "category",
data: xData,
boundaryGap: false,
axisLine: { lineStyle: { color: "#1a2050" } },
axisLabel: { ...textStyle, rotate: 30 },
},
yAxis: {
type: "value",
name: "ms",
nameTextStyle: { color: "#94a3b8", fontSize: 10 },
min: 20,
max: 80,
interval: 15,
splitLine: { lineStyle: { color: "#1a2050", type: "dashed" } },
axisLabel: textStyle,
},
tooltip: {
trigger: "axis" as const,
valueFormatter: (v: any) => v + "ms",
backgroundColor: "#0f143c",
borderColor: "#1a2050",
textStyle: { color: "#e0e6ff" },
},
series: [
{
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 4,
lineStyle: { color: "#10b981", width: 2 },
itemStyle: { color: "#10b981" },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(16,185,129,0.2)" },
{ offset: 1, color: "rgba(16,185,129,0)" },
]),
},
data: [52, 48, 55, 42, 65, 38, 45, 51, 58, 44],
},
{
type: "line",
smooth: true,
symbol: "diamond",
symbolSize: 3,
lineStyle: { color: "#f59e0b", width: 1.5, type: "dashed" },
itemStyle: { color: "#f59e0b" },
data: [60, 55, 62, 50, 70, 45, 52, 58, 65, 50],
},
],
});
}
function handleResize() {
if (chart && !chart.isDisposed()) chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
const d1 = xData.map(() => Math.round(35 + Math.random() * 35));
const d2 = xData.map(() => Math.round(42 + Math.random() * 30));
chart.setOption({ series: [{ data: d1 }, { data: d2 }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 4000);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,160 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot green" />系统负载</div>
<div class="gauge-row">
<div v-for="g in gauges" :key="g.label" class="gauge-item">
<div :ref="(el) => setGaugeRef(g.label, el)" class="chart-box-xs" />
<span class="gauge-label">{{ g.label }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "SystemGauges" });
const gaugeRefs: Record<string, HTMLDivElement> = {};
const charts: echarts.ECharts[] = [];
let timer = 0;
const gauges = [
{ label: "CPU", value: 42 },
{ label: "内存", value: 61 },
{ label: "磁盘", value: 35 },
];
function setGaugeRef(label: string, el: unknown) {
if (el instanceof HTMLDivElement) gaugeRefs[label] = el;
}
function waitForSize(el: HTMLDivElement): Promise<void> {
return new Promise((resolve) => {
if (el.clientWidth > 0 && el.clientHeight > 0) {
resolve();
return;
}
const check = () => {
if (el.clientWidth > 0 && el.clientHeight > 0) {
resolve();
} else {
requestAnimationFrame(check);
}
};
requestAnimationFrame(check);
});
}
async function initGauges() {
const entries = Object.entries(gaugeRefs);
await Promise.all(entries.map(([, el]) => waitForSize(el)));
entries.forEach(([, el]) => {
const chart = echarts.init(el);
charts.push(chart);
chart.setOption({
series: [
{
type: "gauge",
radius: "90%",
center: ["50%", "60%"],
startAngle: 210,
endAngle: -30,
min: 0,
max: 100,
detail: {
formatter: "{value}%",
fontSize: 12,
color: "#e0e6ff",
offsetCenter: [0, "65%"],
},
data: [{ value: 0 }],
axisLine: {
lineStyle: {
width: 6,
color: [
[0.3, "#10b981"],
[0.7, "#f59e0b"],
[1, "#ef4444"],
],
},
},
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
pointer: { length: "55%", width: 3, itemStyle: { color: "#e0e6ff" } },
},
],
});
});
}
function handleResize() {
charts.forEach((c) => {
if (!c.isDisposed()) c.resize();
});
}
function tick() {
const v = [
clamp(gauges[0]!.value + (Math.random() - 0.5) * 8, 10, 90),
clamp(gauges[1]!.value + (Math.random() - 0.5) * 6, 20, 95),
clamp(gauges[2]!.value + (Math.random() - 0.5) * 3, 15, 85),
];
gauges[0]!.value = Math.round(v[0]!);
gauges[1]!.value = Math.round(v[1]!);
gauges[2]!.value = Math.round(v[2]!);
charts.forEach((c, i) => {
if (!c.isDisposed()) c.setOption({ series: [{ data: [{ value: gauges[i]!.value }] }] });
});
}
function clamp(v: number, min: number, max: number) {
return Math.max(min, Math.min(max, v));
}
onMounted(() => {
initGauges().then(() => {
timer = window.setInterval(tick, 2000);
});
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
charts.forEach((c) => c.dispose());
});
</script>
<style scoped>
.gauge-row {
display: flex;
flex: 1;
align-items: center;
justify-content: space-around;
}
.gauge-item {
display: flex;
flex: 1;
flex-direction: column;
text-align: center;
}
.gauge-label {
display: block;
flex-shrink: 0;
margin-top: -6px;
font-size: 12px;
opacity: 0.5;
}
.chart-box-xs {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,109 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot purple" />业绩目标达成率</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "TargetReality" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const categories = ["华东区", "华南区", "华北区", "西南区"];
const textStyle = { color: "#94a3b8", fontSize: 10 };
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
grid: { top: 5, right: 30, bottom: 10, left: 10, containLabel: true },
xAxis: {
type: "value",
max: 150,
splitLine: { lineStyle: { color: "#1a2050" } },
axisLabel: textStyle,
},
yAxis: {
type: "category",
data: categories,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: textStyle,
},
legend: {
textStyle: { color: "#94a3b8", fontSize: 10 },
top: 0,
right: 0,
itemWidth: 10,
itemHeight: 8,
},
series: [
{
name: "目标",
type: "bar",
barWidth: 8,
itemStyle: {
borderRadius: [0, 2, 2, 0],
color: "rgba(0,212,255,0.35)",
borderColor: "#00d4ff",
borderWidth: 1,
},
data: [120, 100, 80, 60],
},
{
name: "已完成",
type: "bar",
barWidth: 8,
itemStyle: { borderRadius: [0, 2, 2, 0], color: "#7c3aed" },
data: [112, 88, 75, 52],
},
],
});
}
function handleResize() {
if (chart && !chart.isDisposed()) chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
const targets = [120, 100, 80, 60];
const actual = targets.map((t) => Math.round(t * (0.7 + Math.random() * 0.25)));
chart.setOption({ series: [{}, { data: actual }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 5000);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,149 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot accent" />今日概览</div>
<div class="overview-grid">
<div v-for="item in data" :key="item.label" class="ov-card">
<div class="ov-label">{{ item.label }}</div>
<div class="ov-value" :style="{ color: item.color }">
<span class="ov-count">{{ item.value }}</span>
<span class="ov-unit">{{ item.unit }}</span>
</div>
<div class="ov-change" :class="item.up ? 'up' : 'down'">
{{ item.up ? "▲" : "▼" }} {{ item.change }}
<span class="ov-compare">较昨日</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted, onUnmounted } from "vue";
defineOptions({ name: "TodayOverview" });
const data = reactive([
{
label: "总销售额",
value: "99.9",
unit: "万",
color: "#00d4ff",
change: "12.5%",
up: true,
base: 999000,
},
{
label: "订单量",
value: "2,356",
unit: "",
color: "#7c3aed",
change: "8.2%",
up: true,
base: 2356,
},
{
label: "活跃用户",
value: "1,286",
unit: "",
color: "#10b981",
change: "3.1%",
up: true,
base: 1286,
},
{
label: "转化率",
value: "3.82",
unit: "%",
color: "#f59e0b",
change: "1.2%",
up: false,
base: 3.82,
},
]);
let timer = 0;
function tick() {
const orders = data[1]!.base + Math.round((Math.random() - 0.5) * 80);
const users = data[2]!.base + Math.round((Math.random() - 0.5) * 60);
data[0]!.base = orders * 420 + Math.round(Math.random() * 10000);
data[1]!.base = orders;
data[2]!.base = users;
data[3]!.base = +(3.5 + Math.random() * 2).toFixed(2);
data[0]!.value = (data[0]!.base / 10000).toFixed(1);
data[1]!.value = data[1]!.base.toLocaleString();
data[2]!.value = data[2]!.base.toLocaleString();
data[3]!.value = data[3]!.base.toFixed(2);
for (const d of data) {
d.change = (Math.random() * 15 - 3).toFixed(1) + "%";
d.up = Math.random() > 0.4;
}
}
onMounted(() => {
tick();
timer = window.setInterval(tick, 4000);
});
onUnmounted(() => clearInterval(timer));
</script>
<style scoped>
.overview-grid {
display: grid;
flex: 1;
grid-template-rows: 1fr 1fr;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.ov-card {
display: flex;
flex-direction: column;
justify-content: center;
padding: 10px 12px;
background: linear-gradient(135deg, rgb(0 212 255 / 4%) 0%, rgb(0 20 60 / 30%) 100%);
border: 1px solid rgb(0 180 255 / 6%);
border-radius: 8px;
}
.ov-label {
margin-bottom: 2px;
font-size: 10px;
opacity: 0.35;
}
.ov-value {
display: flex;
gap: 3px;
align-items: baseline;
margin-bottom: 3px;
}
.ov-count {
font-size: 20px;
font-weight: 700;
line-height: 1;
}
.ov-unit {
font-size: 11px;
opacity: 0.5;
}
.ov-change {
font-size: 10px;
}
.ov-change.up {
color: #10b981;
}
.ov-change.down {
color: #f59e0b;
}
.ov-compare {
margin-left: 3px;
opacity: 0.4;
}
</style>

@ -1,101 +0,0 @@
<template>
<div class="panel p1 col-3">
<div class="panel-hd"><span class="dot purple" />热销产品 TOP5</div>
<div class="product-list">
<div v-for="(p, i) in topProducts" :key="p.name" class="product-row">
<span class="product-rank flex-cc" :class="'rank-' + (i + 1)">{{ i + 1 }}</span>
<div class="product-info">
<span class="product-name">{{ p.name }}</span>
<span class="product-category">{{ p.category }}</span>
</div>
<span class="product-sales">¥{{ formatNumber(p.sales) }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
defineOptions({ name: "TopProductList" });
const topProducts = ref([
{ name: "企业版 License", category: "软件服务", sales: 45200 },
{ name: "高级数据分析", category: "数据服务", sales: 32800 },
{ name: "API 接口调用", category: "接口服务", sales: 28600 },
{ name: "技术支持服务", category: "服务", sales: 15600 },
{ name: "定制开发", category: "定制", sales: 12400 },
]);
function formatNumber(num: number): string {
return num.toLocaleString();
}
</script>
<style scoped>
.product-list {
display: flex;
flex-direction: column;
gap: 8px;
overflow: auto;
}
.product-row {
display: flex;
gap: 8px;
align-items: center;
padding: 10px;
background: rgb(12 18 48 / 60%);
border-radius: 8px;
}
.product-rank {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
font-size: 10px;
font-weight: 700;
background: rgb(26 32 80 / 60%);
border-radius: 50%;
}
.product-rank.rank-1 {
color: #fff;
background: #ef4444;
}
.product-rank.rank-2 {
color: #fff;
background: #f59e0b;
}
.product-rank.rank-3 {
color: #fff;
background: #3b82f6;
}
.product-info {
flex: 1;
min-width: 0;
}
.product-name {
display: block;
font-size: 13px;
font-weight: 500;
}
.product-category {
display: block;
font-size: 11px;
opacity: 0.5;
}
.product-sales {
font-size: 14px;
font-weight: 600;
color: #00d4ff;
}
</style>

@ -1,106 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot purple" />用户分布</div>
<div ref="chartRef" class="chart-box-sm" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const chartRef = ref<HTMLDivElement>();
import * as echarts from "echarts";
defineOptions({ name: "UserDistributionChart" });
let chart: echarts.ECharts | null = null;
let timer = 0;
const textStyle = { color: "#94a3b8" };
const names = ["管理员", "普通用户", "访客", "其他"];
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
series: [
{
type: "pie",
radius: ["55%", "80%"],
center: ["50%", "55%"],
label: { show: false },
data: [
{ value: 620, name: "管理员", itemStyle: { color: "#00d4ff" } },
{ value: 450, name: "普通用户", itemStyle: { color: "#7c3aed" } },
{ value: 180, name: "访客", itemStyle: { color: "#f59e0b" } },
{ value: 292, name: "其他", itemStyle: { color: "#10b981" } },
],
},
],
tooltip: {
trigger: "item",
backgroundColor: "#0f143c",
borderColor: "#1a2050",
textStyle: { color: "#e0e6ff" },
},
legend: {
orient: "vertical",
right: 10,
top: "center",
textStyle,
itemWidth: 10,
itemHeight: 10,
},
});
}
function handleResize() {
if (!chart || chart.isDisposed()) return;
chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
const data = names.map((name) => ({
value: Math.round(100 + Math.random() * 600),
name,
itemStyle: (
chart as unknown as {
getOption: () => { series: [{ data: { name: string; itemStyle?: unknown }[] }] };
}
)
.getOption()
.series[0].data?.find((d: { name: string }) => d.name === name)?.itemStyle,
}));
chart.setOption({ series: [{ data }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 5000);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box-sm {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,107 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot green" />用户画像分析</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts";
defineOptions({ name: "UserProfile" });
const chartRef = ref<HTMLDivElement>();
let chart: echarts.ECharts | null = null;
let timer = 0;
const indicators = [
{ name: "消费力", max: 100 },
{ name: "活跃度", max: 100 },
{ name: "复购率", max: 100 },
{ name: "客单价", max: 100 },
{ name: "满意度", max: 100 },
];
function initChart(dom: HTMLDivElement) {
chart = echarts.init(dom);
chart.setOption({
legend: {
bottom: 0,
textStyle: { color: "#94a3b8", fontSize: 8 },
itemWidth: 8,
itemHeight: 8,
},
radar: {
center: ["50%", "48%"],
radius: "55%",
indicator: indicators,
axisName: { color: "#94a3b8", fontSize: 8 },
splitArea: { areaStyle: { color: ["rgba(0,212,255,0.02)", "rgba(0,212,255,0.02)"] } },
splitLine: { lineStyle: { color: "#1a2050" } },
axisLine: { lineStyle: { color: "#1a2050" } },
},
series: [
{
type: "radar",
symbol: "circle",
symbolSize: 3,
lineStyle: { color: "#00d4ff", width: 2 },
areaStyle: { color: "rgba(0,212,255,0.12)" },
itemStyle: { color: "#00d4ff" },
data: [{ value: [82, 78, 65, 58, 88], name: "高价值客群" }],
},
{
type: "radar",
symbol: "triangle",
symbolSize: 3,
lineStyle: { color: "#f59e0b", width: 1.5, type: "dashed" },
areaStyle: { color: "rgba(245,158,11,0.06)" },
itemStyle: { color: "#f59e0b" },
data: [{ value: [55, 72, 48, 42, 78], name: "潜力客群" }],
},
],
});
}
function handleResize() {
if (chart && !chart.isDisposed()) chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
const v1 = indicators.map(() => Math.round(53 + Math.random() * 42));
const v2 = indicators.map(() => Math.round(38 + Math.random() * 45));
chart.setOption({ series: [{ data: [{ value: v1 }] }, { data: [{ value: v2 }] }] });
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 5000);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,143 +0,0 @@
<template>
<div class="panel p1">
<div class="panel-hd"><span class="dot accent" />访问趋势</div>
<div ref="chartRef" class="chart-box" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
const chartRef = ref<HTMLDivElement>();
import * as echarts from "echarts";
defineOptions({ name: "VisitTrendChart" });
let chart: echarts.ECharts | null = null;
let timer = 0;
const textStyle = { color: "#94a3b8" };
const tooltip = {
trigger: "axis" as const,
backgroundColor: "#0f143c",
borderColor: "#1a2050",
textStyle: { color: "#e0e6ff" },
};
const xData = [
"00:00",
"02:00",
"04:00",
"06:00",
"08:00",
"10:00",
"12:00",
"14:00",
"16:00",
"18:00",
"20:00",
"22:00",
];
let pv = [120, 85, 60, 45, 200, 380, 450, 520, 480, 380, 300, 200];
let uv = [80, 55, 40, 30, 140, 260, 320, 380, 350, 270, 220, 150];
function initChart(dom: HTMLDivElement) {
if (!dom) return;
chart = echarts.init(dom);
chart.setOption({
grid: { top: 20, right: 30, bottom: 30, left: 50 },
xAxis: {
type: "category",
data: xData,
axisLine: { lineStyle: { color: "#1a2050" } },
axisLabel: textStyle,
},
yAxis: {
type: "value",
splitLine: { lineStyle: { color: "#1a2050", type: "dashed" } },
axisLabel: textStyle,
},
tooltip,
legend: { textStyle, top: 0 },
series: [
{
name: "PV",
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 6,
lineStyle: { color: "#00d4ff", width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(0,212,255,0.3)" },
{ offset: 1, color: "rgba(0,212,255,0.02)" },
]),
},
itemStyle: { color: "#00d4ff" },
data: pv,
},
{
name: "UV",
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 6,
lineStyle: { color: "#7c3aed", width: 2 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "rgba(124,58,237,0.3)" },
{ offset: 1, color: "rgba(124,58,237,0.02)" },
]),
},
itemStyle: { color: "#7c3aed" },
data: uv,
},
],
});
}
function handleResize() {
if (!chart || chart.isDisposed()) return;
chart.resize();
}
function tick() {
if (!chart || chart.isDisposed()) return;
pv.push(Math.round(pv[pv.length - 1]! * (0.85 + Math.random() * 0.3)));
uv.push(Math.round(uv[uv.length - 1]! * (0.85 + Math.random() * 0.3)));
pv.shift();
uv.shift();
chart.setOption({
series: [{ data: pv }, { data: uv }],
});
}
onMounted(() => {
const el = chartRef.value;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0 && entry.contentRect.height > 0) {
observer.disconnect();
initChart(el);
timer = window.setInterval(tick, 3000);
}
});
observer.observe(el);
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
clearInterval(timer);
window.removeEventListener("resize", handleResize);
chart?.dispose();
});
</script>
<style scoped>
.chart-box {
flex: 1;
width: 100%;
min-height: 0;
}
</style>

@ -1,52 +0,0 @@
<template>
<div class="flex flex-col relative last:mb-0">
<FaDashboardSkeleton v-if="loading" />
<template v-else>
<ElRow :gutter="24">
<ElCol :sm="12" :md="12" :lg="6" class="mb-5"><TotalOrderVolume /></ElCol>
<ElCol :sm="12" :md="12" :lg="6" class="mb-5"><TotalProducts /></ElCol>
<ElCol :sm="24" :md="12" :lg="6" class="mb-5"><ProductSales /></ElCol>
<ElCol :sm="24" :md="12" :lg="6" class="mb-5"><SalesGrowth /></ElCol>
</ElRow>
<ElRow :gutter="24">
<ElCol :span="24" class="mb-5"><CartConversionRate /></ElCol>
</ElRow>
<ElRow :gutter="24">
<ElCol :sm="12" :md="12" :lg="6" class="mb-5"><SalesTrend /></ElCol>
<ElCol :sm="12" :md="12" :lg="6" class="mb-5"><SalesClassification /></ElCol>
<ElCol :sm="24" :md="12" :lg="6" class="mb-5"><HotCommodity /></ElCol>
<ElCol :sm="24" :md="12" :lg="6" class="mb-5"><AnnualSales /></ElCol>
</ElRow>
<ElRow :gutter="24">
<ElCol :sm="24" :md="12" :lg="12" class="mb-5"><ActiveUser /></ElCol>
<ElCol :sm="24" :md="12" :lg="12" class="mb-5"><SalesOverview /></ElCol>
</ElRow>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import FaDashboardSkeleton from "@/components/skeleton/fa-dashboard-skeleton.vue";
import ActiveUser from "./modules/active-user.vue";
import SalesOverview from "./modules/sales-overview.vue";
import TotalOrderVolume from "./modules/total-order-volume.vue";
import TotalProducts from "./modules/total-products.vue";
import SalesTrend from "./modules/sales-trend.vue";
import SalesClassification from "./modules/sales-classification.vue";
import HotCommodity from "./modules/hot-commodity.vue";
import AnnualSales from "./modules/annual-sales.vue";
import ProductSales from "./modules/product-sales.vue";
import SalesGrowth from "./modules/sales-growth.vue";
import CartConversionRate from "./modules/cart-conversion-rate.vue";
defineOptions({ name: "DashboardWorkplace" });
const loading = ref(true);
onMounted(() => {
loading.value = false;
});
</script>

@ -1,50 +0,0 @@
<template>
<div class="fa-card h-105 p-4 box-border">
<FaBarChart
class="box-border p-2"
barWidth="50%"
height="13.7rem"
:showAxisLine="false"
:data="chartData"
:xAxisData="xAxisLabels"
/>
<div class="ml-1">
<h3 class="mt-5 text-lg font-medium">用户概述</h3>
<p class="mt-1 text-sm">
比上周
<span class="text-success font-medium">+23%</span>
</p>
<p class="mt-1 text-sm">我们为您创建了多个选项可将它们组合在一起并定制为像素完美的页面</p>
</div>
<div class="flex justify-between mt-2">
<div class="flex-1" v-for="(item, index) in list" :key="index">
<p class="text-2xl text-g-900">{{ item.num }}</p>
<p class="text-xs text-g-500">{{ item.name }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface UserStatItem {
name: string;
num: string;
}
// 9
const xAxisLabels = ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月"];
//
const chartData = [160, 100, 150, 80, 190, 100, 175, 120, 160];
/**
* 用户统计数据列表
* 包含总用户量总访问量日访问量和周同比等关键指标
*/
const list: UserStatItem[] = [
{ name: "总用户量", num: "32k" },
{ name: "总访问量", num: "128k" },
{ name: "日访问量", num: "1.2k" },
{ name: "周同比", num: "+5%" },
];
</script>

@ -1,43 +0,0 @@
<template>
<div class="fa-card p-5 h-112">
<div class="fa-card-header">
<div class="title">
<h4>年度销售额</h4>
<p>按季度统计</p>
</div>
</div>
<FaBarChart
:showAxisLabel="false"
:showAxisLine="false"
:showSplitLine="false"
:data="[50, 80, 50, 90, 60, 70, 50]"
barWidth="26px"
height="calc(100% - 155px)"
/>
<div class="flex justify-around mt-10">
<div class="flex items-center">
<div
class="flex items-center justify-center size-10.5 mr-2.5 text-theme bg-theme/10 rounded-lg"
>
<FaSvgIcon icon="ri:money-cny-circle-line" class="text-xl" />
</div>
<div>
<p class="text-lg">¥200,858</p>
<span class="text-sm">线上销售</span>
</div>
</div>
<div class="flex items-center">
<div
class="flex items-center justify-center size-10.5 mr-2.5 text-theme bg-theme/10 rounded-lg"
>
<FaSvgIcon icon="ri:heart-3-line" class="text-xl" />
</div>
<div>
<p class="text-lg">¥102,927</p>
<span class="text-sm">线下销售</span>
</div>
</div>
</div>
</div>
</template>

@ -1,11 +0,0 @@
<template>
<FaLineChartCard
class="fa-card h-53 p-5 pt-3.5"
:value="2545"
label="购物车转化率"
:percentage="1.2"
:height="13.5"
:chartData="[120, 132, 101, 134, 90, 230, 210]"
:showAreaColor="true"
/>
</template>

@ -1,77 +0,0 @@
<template>
<div class="fa-card p-5 h-112">
<div class="fa-card-header">
<div class="title">
<h4>热销商品</h4>
<p>本周销售排行</p>
</div>
</div>
<FaLineChart
:showAxisLabel="false"
:showAxisLine="false"
:showSplitLine="false"
:showAreaColor="true"
:data="[8, 40, 82, 35, 90, 52, 35]"
height="9rem"
/>
<div class="mt-10 space-y-5">
<div v-for="item in weeklyList" :key="item.title" class="flex items-center">
<div
class="size-10.5 flex items-center justify-center rounded-lg"
:class="item.iconBgClass"
>
<FaSvgIcon :icon="item.icon" class="text-xl" />
</div>
<div class="ml-2.5">
<p class="text-sm font-medium text-g-800">{{ item.title }}</p>
<span class="text-sm text-g-600">{{ item.subtitle }}</span>
</div>
<div class="ml-auto px-3 py-1.5 text-sm text-center rounded" :class="item.valueBgClass">
<span>+{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface WeeklyItem {
icon: string;
title: string;
subtitle: string;
value: string;
iconBgClass: string;
valueBgClass: string;
}
/**
* 本周热销商品列表
* 展示销量排名前三的商品信息
*/
const weeklyList: WeeklyItem[] = [
{
icon: "ri:money-cny-circle-line",
title: "智能手表Pro",
subtitle: "电子产品",
value: "1,286件",
iconBgClass: "bg-theme/12 text-theme",
valueBgClass: "bg-theme/12 text-theme",
},
{
icon: "ri:money-cny-circle-line",
title: "时尚连衣裙",
subtitle: "女装服饰",
value: "892件",
iconBgClass: "bg-success/12 text-success",
valueBgClass: "bg-success/12 text-success",
},
{
icon: "ri:money-cny-circle-line",
title: "厨房小家电",
subtitle: "家居用品",
value: "756件",
iconBgClass: "bg-error/12 text-error",
valueBgClass: "bg-error/12 text-error",
},
];
</script>

@ -1,22 +0,0 @@
<template>
<div class="fa-card h-53 p-5 pt-3.5">
<div class="fa-card-header">
<div class="title">
<h4 class="text-2xl">
<span>14.5k</span>
<FaSvgIcon icon="ri:arrow-right-up-line" class="text-success text-lg ml-1" />
</h4>
<p>销售量</p>
</div>
</div>
<FaBarChart
:showAxisLabel="false"
:showAxisLine="false"
:showSplitLine="false"
:data="[50, 80, 50, 90, 60, 70, 50]"
barWidth="16px"
height="5.6rem"
/>
</div>
</template>

@ -1,48 +0,0 @@
<template>
<div class="fa-card p-5 h-112">
<div class="fa-card-header">
<div class="title">
<h4>销售分类</h4>
<p>按产品类别</p>
</div>
</div>
<FaRingChart
:data="[
{ value: 30, name: '电子产品' },
{ value: 55, name: '服装鞋包' },
{ value: 36, name: '家居用品' },
]"
:color="['#4C87F3', '#EDF2FF', '#8BD8FC']"
:radius="['70%', '80%']"
height="16.5rem"
:showLabel="false"
:borderRadius="0"
centerText="¥300,458"
/>
<div class="flex justify-around">
<div class="flex items-center">
<div
class="flex items-center justify-center size-10.5 mr-2.5 text-theme bg-theme/10 rounded-lg"
>
<FaSvgIcon icon="ri:money-cny-circle-line" class="text-xl" />
</div>
<div>
<p class="text-lg">¥500,458</p>
<span class="text-sm">总收入</span>
</div>
</div>
<div class="flex items-center">
<div
class="flex items-center justify-center size-10.5 mr-2.5 text-theme bg-theme/10 rounded-lg"
>
<FaSvgIcon icon="ri:heart-3-line" class="text-xl" />
</div>
<div>
<p class="text-lg">¥130,580</p>
<span class="text-sm">净利润</span>
</div>
</div>
</div>
</div>
</template>

@ -1,23 +0,0 @@
<template>
<div class="fa-card h-53 p-5 pt-3.5">
<div class="fa-card-header">
<div class="title">
<h4 class="text-2xl">
<span>12%</span>
<FaSvgIcon icon="ri:arrow-right-up-line" class="text-success text-lg ml-1" />
</h4>
<p>增长</p>
</div>
</div>
<FaLineChart
:showAreaColor="true"
:showAxisLabel="false"
:showAxisLine="false"
:showSplitLine="false"
:data="[50, 85, 65, 95, 75, 130, 180]"
barWidth="16px"
height="5rem"
/>
</div>
</template>

@ -1,46 +0,0 @@
<template>
<div class="fa-card h-105 p-5">
<div class="fa-card-header">
<div class="title">
<h4>访问量</h4>
<p>
今年增长
<span class="text-success">+15%</span>
</p>
</div>
</div>
<FaLineChart
height="calc(100% - 56px)"
:data="data"
:xAxisData="xAxisData"
:showAreaColor="true"
:showAxisLine="false"
/>
</div>
</template>
<script setup lang="ts">
/**
* 全年访问量数据
* 记录每月的访问量统计
*/
const data = [50, 25, 40, 20, 70, 35, 65, 30, 35, 20, 40, 44];
/**
* X 轴月份标签
*/
const xAxisData = [
"1月",
"2月",
"3月",
"4月",
"5月",
"6月",
"7月",
"8月",
"9月",
"10月",
"11月",
"12月",
];
</script>

@ -1,17 +0,0 @@
<template>
<div class="fa-card p-5 h-112">
<div class="fa-card-header">
<div class="title">
<h4>销售趋势</h4>
<p>月度销售对比</p>
</div>
</div>
<FaDualBarCompareChart
:positiveData="[50, 80, 120, 90, 60]"
:negativeData="[30, 60, 90, 70, 40]"
:xAxisData="['一月', '二月', '三月', '四月', '五月']"
height="19rem"
/>
</div>
</template>

@ -1,23 +0,0 @@
<template>
<div class="fa-card h-53 p-5 pt-3.5">
<div class="fa-card-header">
<div class="title">
<h4 class="text-2xl font-medium">205,216</h4>
<p>这个月增长</p>
</div>
</div>
<FaRingChart
:data="[
{ value: 30, name: '已完成' },
{ value: 25, name: '处理中' },
{ value: 45, name: '待发货' },
]"
:color="['#4C87F3', '#93F1B4', '#8BD8FC']"
:radius="['56%', '76%']"
height="7rem"
:showLabel="false"
:borderRadius="0"
/>
</div>
</template>

@ -1,19 +0,0 @@
<template>
<div class="fa-card h-53 p-5 pt-3.5">
<div class="fa-card-header">
<div class="title">
<h4 class="text-2xl font-medium">55,231</h4>
<p>商品总数</p>
</div>
</div>
<FaBarChart
:showAxisLabel="false"
:showAxisLine="false"
:showSplitLine="false"
:data="[50, 80, 40, 90, 60, 70]"
height="7rem"
barWidth="18px"
/>
</div>
</template>

@ -19,8 +19,9 @@
/>
<ElTabs v-model="menuClientTab" @tab-change="handleMenuClientTabChange">
<ElTabPane label="PC 桌面菜单管理" name="pc" />
<ElTabPane label="APP 移动端菜单管理" name="app" />
<ElTabPane label="菜单管理" name="pc" />
<!-- <ElTabPane label="PC 桌面菜单管理" name="pc" /> -->
<!-- <ElTabPane label="APP 移动端菜单管理" name="app" /> -->
</ElTabs>
<ElCard class="fa-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">

@ -46,8 +46,6 @@
<!-- <template v-if="loginFlowMode === 'account'"> -->
<FaLoginAccountForm
ref="accountFormRef"
v-model:is-passing="isPassing"
v-model:is-click-pass="isClickPass"
v-model:login-form="loginForm"
:rules="rules"
:captcha-state="captchaState"
@ -56,7 +54,6 @@
:accounts="accounts"
:form-key="formKey"
:is-dark="isDark"
:drag-verify-text-color="dragVerifyTextColor"
:loading="loading"
@submit="handleSubmit"
@setup-account="setupAccount"
@ -109,64 +106,6 @@
</div>
</ElScrollbar>
</div>
<footer
class="login-page-footer login-page-footer--pinned shrink-0 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3"
:class="panelAlign === 'center' && 'login-page-footer--floating-layout'"
>
<div class="login-footer-text text-sm">
<div class="login-footer-row">
<a
:href="configStore.configData?.git_code?.config_value || '#'"
target="_blank"
rel="noopener noreferrer"
class="login-page-footer__link"
>
{{ configStore.configData?.copyright?.config_value || "" }}
</a>
</div>
<span class="login-page-footer__sep login-footer-sep-center">|</span>
<div class="login-footer-row">
<a
:href="configStore.configData?.help_doc?.config_value || '#'"
target="_blank"
rel="noopener noreferrer"
class="login-page-footer__link"
>
帮助
</a>
<span class="login-page-footer__sep">|</span>
<a
:href="configStore.configData?.privacy?.config_value || '#'"
target="_blank"
rel="noopener noreferrer"
class="login-page-footer__link"
>
隐私
</a>
<span class="login-page-footer__sep">|</span>
<a
:href="configStore.configData?.clause?.config_value || '#'"
target="_blank"
rel="noopener noreferrer"
class="login-page-footer__link"
>
条款
</a>
<span
v-if="configStore.configData?.keep_record?.config_value"
class="login-page-footer__sep"
>|</span
>
<span
v-if="configStore.configData?.keep_record?.config_value"
class="login-page-footer__record"
>
{{ configStore.configData.keep_record.config_value }}
</span>
</div>
</div>
</footer>
</div>
</div>
</div>
@ -302,9 +241,6 @@ function setAuthPanel(panel: AuthPanel) {
// }
// }
const dragVerifyTextColor = computed(() =>
isDark.value ? "rgba(255, 255, 255, 0.45)" : "var(--fa-gray-700)"
);
const formKey = ref(0);
watch(locale, () => {
@ -316,9 +252,6 @@ watch(authPanel, (panel) => {
if (loginFlowMode.value !== "account") return;
getCaptcha();
loginForm.captcha = "";
accountFormRef.value?.resetDragVerify?.();
isPassing.value = false;
isClickPass.value = false;
});
const accounts = computed<Account[]>(() => [
@ -349,9 +282,6 @@ const demoAccountKey = ref<AccountKey>("super");
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
const isPassing = ref(false);
const isClickPass = ref(false);
const accountFormRef = ref<InstanceType<typeof FaLoginAccountForm> | null>(null);
const registerPanelRef = ref<InstanceType<typeof FaLoginRegisterPanel> | null>(null);
const forgetPanelRef = ref<InstanceType<typeof FaLoginForgetPanel> | null>(null);
@ -561,9 +491,8 @@ onMounted(async () => {
return;
}
if (configStore.configData.captcha_enable?.config_value === "true") {
// getCaptcha();
getCaptcha();
}
getCaptcha();
// voteTimer = setTimeout(showVoteNotification, 500);
});
@ -595,10 +524,6 @@ const handleSubmit = async () => {
const valid = await accountFormRef.value.validate?.();
if (!valid) return;
if (!isPassing.value) {
isClickPass.value = true;
return;
}
loading.value = true;
@ -620,7 +545,6 @@ const handleSubmit = async () => {
}
} finally {
loading.value = false;
accountFormRef.value?.resetDragVerify?.();
}
};

Loading…
Cancel
Save