feat: 去除不需要的页面元素和路由
parent
1d41af1e7c
commit
aa3b909993
@ -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,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,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>
|
||||
Loading…
Reference in New Issue