|
|
@@ -1,20 +1,190 @@
|
|
|
<template>
|
|
|
<div class="app-container home">
|
|
|
<!-- <el-divider /> -->
|
|
|
- <div class="index-style">
|
|
|
- <div class="typewriter-container">
|
|
|
- <span v-for="(char, index) in 'welcome!'" :key="index" :style="{ animationDelay: `${index * 0.5}s` }" class="typewriter-char">{{
|
|
|
- char
|
|
|
- }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <el-row :gutter="12" class="stat-row">
|
|
|
+ <el-col :xs="12" :sm="12" :md="6" :lg="6" :xl="6">
|
|
|
+ <el-card shadow="hover" class="stat-card stat-card--blue">
|
|
|
+ <div class="stat-title">商品总数</div>
|
|
|
+ <div class="stat-value">{{ stats.totalGoods }}</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="12" :sm="12" :md="6" :lg="6" :xl="6">
|
|
|
+ <el-card shadow="hover" class="stat-card stat-card--orange">
|
|
|
+ <div class="stat-title">停售商品</div>
|
|
|
+ <div class="stat-value">{{ stats.stoppedGoods }}</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="12" :sm="12" :md="6" :lg="6" :xl="6">
|
|
|
+ <el-card shadow="hover" class="stat-card stat-card--purple">
|
|
|
+ <div class="stat-title">上下架申请</div>
|
|
|
+ <div class="stat-value">{{ stats.shelfApply }}</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="12" :sm="12" :md="6" :lg="6" :xl="6">
|
|
|
+ <el-card shadow="hover" class="stat-card stat-card--green">
|
|
|
+ <div class="stat-title">新鲜闪购</div>
|
|
|
+ <div class="stat-value">{{ stats.flashSale }}</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row :gutter="12" class="chart-row">
|
|
|
+ <el-col :xs="24" :sm="24" :md="14" :lg="14" :xl="14">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <template #header>
|
|
|
+ <span>商品状态统计</span>
|
|
|
+ </template>
|
|
|
+ <div ref="goodsStatusRef" class="chart" />
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="24" :md="10" :lg="10" :xl="10">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <template #header>
|
|
|
+ <span>新鲜好物分布</span>
|
|
|
+ </template>
|
|
|
+ <div ref="freshPieRef" class="chart" />
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ <el-row :gutter="12" class="info-row">
|
|
|
+ <el-col :xs="24" :sm="24" :md="14" :lg="14" :xl="14">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <template #header>
|
|
|
+ <span>新鲜闪购TOP5</span>
|
|
|
+ </template>
|
|
|
+ <div class="top-list">
|
|
|
+ <div v-for="(item, idx) in freshTop" :key="idx" class="top-item">
|
|
|
+ <div class="top-left">
|
|
|
+ <span class="top-rank">{{ idx + 1 }}</span>
|
|
|
+ <span class="top-name">{{ item.name }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="top-right">{{ item.count }}件</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="24" :md="10" :lg="10" :xl="10">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <template #header>
|
|
|
+ <span>待处理</span>
|
|
|
+ </template>
|
|
|
+ <div class="todo-box">
|
|
|
+ <div class="todo-item">
|
|
|
+ <span class="todo-label">商品审核</span>
|
|
|
+ <span class="todo-value">{{ pending.review }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="todo-item">
|
|
|
+ <span class="todo-label">上下架申请</span>
|
|
|
+ <span class="todo-value">{{ pending.shelfApply }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="todo-item">
|
|
|
+ <span class="todo-label">停售复核</span>
|
|
|
+ <span class="todo-value">{{ pending.stopCheck }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="Index" lang="ts">
|
|
|
-const goTarget = (url: string) => {
|
|
|
- window.open(url, '__blank');
|
|
|
+import * as echarts from 'echarts';
|
|
|
+
|
|
|
+const goodsStatusRef = ref();
|
|
|
+const freshPieRef = ref();
|
|
|
+let goodsStatusChart: echarts.ECharts | null = null;
|
|
|
+let freshPieChart: echarts.ECharts | null = null;
|
|
|
+
|
|
|
+const stats = reactive({
|
|
|
+ totalGoods: 0,
|
|
|
+ stoppedGoods: 0,
|
|
|
+ shelfApply: 0,
|
|
|
+ flashSale: 0
|
|
|
+});
|
|
|
+
|
|
|
+const freshTop = ref([
|
|
|
+ { name: '精品草莓(500g)', count: 12 },
|
|
|
+ { name: '云南小番茄(1kg)', count: 10 },
|
|
|
+ { name: '原切牛排(2片)', count: 8 },
|
|
|
+ { name: '高山生菜(2颗)', count: 7 },
|
|
|
+ { name: '鲜活基围虾(500g)', count: 6 }
|
|
|
+]);
|
|
|
+
|
|
|
+const pending = reactive({
|
|
|
+ review: 5,
|
|
|
+ shelfApply: 12,
|
|
|
+ stopCheck: 3
|
|
|
+});
|
|
|
+
|
|
|
+const renderCharts = () => {
|
|
|
+ stats.totalGoods = 1280;
|
|
|
+ stats.stoppedGoods = 36;
|
|
|
+ stats.shelfApply = 12;
|
|
|
+ stats.flashSale = 58;
|
|
|
+
|
|
|
+ const goodsStatus = [
|
|
|
+ { name: '商品总数', value: stats.totalGoods },
|
|
|
+ { name: '停售商品', value: stats.stoppedGoods },
|
|
|
+ { name: '上下架申请', value: stats.shelfApply },
|
|
|
+ { name: '新鲜闪购', value: stats.flashSale }
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (goodsStatusRef.value) {
|
|
|
+ goodsStatusChart?.dispose();
|
|
|
+ goodsStatusChart = echarts.init(goodsStatusRef.value, 'macarons');
|
|
|
+ goodsStatusChart.setOption({
|
|
|
+ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|
|
+ grid: { left: 20, right: 20, bottom: 20, top: 20, containLabel: true },
|
|
|
+ xAxis: { type: 'category', data: goodsStatus.map((i) => i.name), axisLabel: { interval: 0 } },
|
|
|
+ yAxis: { type: 'value' },
|
|
|
+ series: [{ type: 'bar', data: goodsStatus.map((i) => i.value), barMaxWidth: 40 }]
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const freshGoods = [
|
|
|
+ { name: '果蔬生鲜', value: 18 },
|
|
|
+ { name: '零食饮料', value: 14 },
|
|
|
+ { name: '粮油调味', value: 9 },
|
|
|
+ { name: '日用百货', value: 11 },
|
|
|
+ { name: '乳品烘焙', value: 6 }
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (freshPieRef.value) {
|
|
|
+ freshPieChart?.dispose();
|
|
|
+ freshPieChart = echarts.init(freshPieRef.value, 'macarons');
|
|
|
+ freshPieChart.setOption({
|
|
|
+ tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
|
|
+ legend: { top: 'bottom' },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['38%', '72%'],
|
|
|
+ center: ['50%', '45%'],
|
|
|
+ label: { show: true, formatter: '{b}' },
|
|
|
+ data: freshGoods
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ }
|
|
|
};
|
|
|
+
|
|
|
+const resizeCharts = () => {
|
|
|
+ goodsStatusChart?.resize();
|
|
|
+ freshPieChart?.resize();
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ renderCharts();
|
|
|
+ window.addEventListener('resize', resizeCharts);
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener('resize', resizeCharts);
|
|
|
+ goodsStatusChart?.dispose();
|
|
|
+ goodsStatusChart = null;
|
|
|
+ freshPieChart?.dispose();
|
|
|
+ freshPieChart = null;
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
@@ -67,80 +237,131 @@ const goTarget = (url: string) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .update-log {
|
|
|
- ol {
|
|
|
- display: block;
|
|
|
- list-style-type: decimal;
|
|
|
- margin-block-start: 1em;
|
|
|
- margin-block-end: 1em;
|
|
|
- margin-inline-start: 0;
|
|
|
- margin-inline-end: 0;
|
|
|
- padding-inline-start: 40px;
|
|
|
- }
|
|
|
+ .chart-row {
|
|
|
+ margin-top: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .info-row {
|
|
|
+ margin-top: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-row {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card {
|
|
|
+ height: 92px;
|
|
|
+ border-radius: 10px;
|
|
|
+ overflow: hidden;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-title {
|
|
|
+ font-size: 14px;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ font-size: 26px;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-top: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card--blue {
|
|
|
+ background: linear-gradient(135deg, #2d8cf0 0%, #57a3f3 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card--orange {
|
|
|
+ background: linear-gradient(135deg, #ff9900 0%, #ffb347 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card--purple {
|
|
|
+ background: linear-gradient(135deg, #7c5cff 0%, #a491ff 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card--green {
|
|
|
+ background: linear-gradient(135deg, #19be6b 0%, #47cb89 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart {
|
|
|
+ height: 320px;
|
|
|
+ width: 100%;
|
|
|
}
|
|
|
- .index-style {
|
|
|
- font-size: 48px;
|
|
|
- font-weight: bold;
|
|
|
- letter-spacing: 15px;
|
|
|
+
|
|
|
+ .top-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .top-item {
|
|
|
display: flex;
|
|
|
- justify-content: center;
|
|
|
align-items: center;
|
|
|
- height: 300px;
|
|
|
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
|
- border-radius: 10px;
|
|
|
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
|
|
|
- .typewriter-container {
|
|
|
- display: flex;
|
|
|
- }
|
|
|
+ .top-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
|
|
|
- .typewriter-char {
|
|
|
- opacity: 0;
|
|
|
- transform: translateY(20px) rotate(-5deg);
|
|
|
- animation:
|
|
|
- typewriter-animation 0.8s ease forwards,
|
|
|
- pulse 2s ease-in-out infinite 1s;
|
|
|
- color: #2d8cf0;
|
|
|
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
|
|
- transition: color 0.3s ease;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- color: #f06292;
|
|
|
- transform: scale(1.1);
|
|
|
- animation-play-state: paused;
|
|
|
- }
|
|
|
- }
|
|
|
+ .top-rank {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 22px;
|
|
|
+ height: 22px;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: rgba(45, 140, 240, 0.12);
|
|
|
+ color: #2d8cf0;
|
|
|
+ font-size: 12px;
|
|
|
+ flex: 0 0 auto;
|
|
|
}
|
|
|
|
|
|
- @keyframes typewriter-animation {
|
|
|
- 0% {
|
|
|
- opacity: 0;
|
|
|
- transform: translateY(20px) rotate(-5deg);
|
|
|
- }
|
|
|
- 50% {
|
|
|
- opacity: 0.5;
|
|
|
- transform: translateY(10px) rotate(-2deg);
|
|
|
- }
|
|
|
- 100% {
|
|
|
- opacity: 1;
|
|
|
- transform: translateY(0) rotate(0deg);
|
|
|
- }
|
|
|
+ .top-name {
|
|
|
+ color: #303133;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
}
|
|
|
|
|
|
- @keyframes pulse {
|
|
|
- 0% {
|
|
|
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
|
|
- transform: scale(1);
|
|
|
- }
|
|
|
- 50% {
|
|
|
- text-shadow:
|
|
|
- 0 0 15px rgba(45, 140, 240, 0.8),
|
|
|
- 0 0 30px rgba(45, 140, 240, 0.4);
|
|
|
- transform: scale(1.05);
|
|
|
- }
|
|
|
- 100% {
|
|
|
- text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
|
|
- transform: scale(1);
|
|
|
+ .top-right {
|
|
|
+ color: #606266;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .todo-box {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .todo-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+
|
|
|
+ .todo-label {
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+
|
|
|
+ .todo-value {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ .update-log {
|
|
|
+ ol {
|
|
|
+ display: block;
|
|
|
+ list-style-type: decimal;
|
|
|
+ margin-block-start: 1em;
|
|
|
+ margin-block-end: 1em;
|
|
|
+ margin-inline-start: 0;
|
|
|
+ margin-inline-end: 0;
|
|
|
+ padding-inline-start: 40px;
|
|
|
}
|
|
|
}
|
|
|
}
|