refactor: 重构路由与部署脚本,更新静态资源路径

This commit is contained in:
zhangtao
2026-05-13 01:55:47 +08:00
parent db5f8430de
commit 63d76007cb
69 changed files with 1688 additions and 2794 deletions
@@ -34,283 +34,281 @@
</ElCol>
</ElRow>
<!-- —— 图表组件演示 —— -->
<div class="analysis-charts-showcase border-g-200">
<div class="py-5">
<h2 class="page-title">图表</h2>
<ElRow :gutter="20">
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(单数据)</span>
</div>
<FaBarChart
:data="singleBarData"
:xAxisData="xAxisData"
:showLegend="true"
legendPosition="right"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(多组数据)</span>
</div>
<FaBarChart
:data="multiBarData"
:xAxisData="xAxisData"
:showLegend="true"
barWidth="26%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(堆叠)</span>
</div>
<FaBarChart
:data="stackBarData"
:xAxisData="stackXAxisData"
:showLegend="true"
:stack="true"
barWidth="26%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">折线图</span>
</div>
<FaLineChart
:data="[58, 15, 82, 35, 120, 62, 45]"
:xAxisData="['一月', '二月', '三月', '四月', '五月', '六月', '七月']"
symbol="none"
:symbolSize="7"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">折线图(渐变背景)</span>
</div>
<FaLineChart
:data="[28, 45, 82, 35, 100, 32, 55]"
:xAxisData="['一月', '二月', '三月', '四月', '五月', '六月', '七月']"
:showAreaColor="true"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">折线图(多组数据)</span>
</div>
<FaLineChart
:data="multiLineData"
:xAxisData="['1月', '2月', '3月', '4月', '5月', '6月']"
:showLegend="true"
/>
</div>
</ElCol>
<ElRow :gutter="20">
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(单数据)</span>
</div>
<FaBarChart
:data="singleBarData"
:xAxisData="xAxisData"
:showLegend="true"
legendPosition="right"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(多组数据)</span>
</div>
<FaBarChart
:data="multiBarData"
:xAxisData="xAxisData"
:showLegend="true"
barWidth="26%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(堆叠)</span>
</div>
<FaBarChart
:data="stackBarData"
:xAxisData="stackXAxisData"
:showLegend="true"
:stack="true"
barWidth="26%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">折线图</span>
</div>
<FaLineChart
:data="[58, 15, 82, 35, 120, 62, 45]"
:xAxisData="['一月', '二月', '三月', '四月', '五月', '六月', '七月']"
symbol="none"
:symbolSize="7"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">折线图(渐变背景)</span>
</div>
<FaLineChart
:data="[28, 45, 82, 35, 100, 32, 55]"
:xAxisData="['一月', '二月', '三月', '四月', '五月', '六月', '七月']"
:showAreaColor="true"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">折线图(多组数据)</span>
</div>
<FaLineChart
:data="multiLineData"
:xAxisData="['1月', '2月', '3月', '4月', '5月', '6月']"
:showLegend="true"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(水平)</span>
</div>
<FaHBarChart
:data="[50, 80, 120, 90, 60]"
:xAxisData="['产品A', '产品B', '产品C', '产品D', '产品E']"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(水平)</span>
</div>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(水平)</span>
</div>
<FaHBarChart
:data="[50, 80, 120, 90, 60]"
:xAxisData="['产品A', '产品B', '产品C', '产品D', '产品E']"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(水平)</span>
</div>
<FaHBarChart
:data="[
{ name: '系列1', data: [10, 20, 30] },
{ name: '系列2', data: [15, 25, 35] },
]"
:xAxisData="['类目1', '类目2', '类目3']"
:showLegend="true"
barWidth="30%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(水平堆叠)</span>
</div>
<FaHBarChart
:data="[
{ name: '系列1', data: [10, 20, 30] },
{ name: '系列2', data: [15, 25, 35] },
]"
:xAxisData="['类目1', '类目2', '类目3']"
:showLegend="true"
:stack="true"
barWidth="30%"
/>
</div>
</ElCol>
<FaHBarChart
:data="[
{ name: '系列1', data: [10, 20, 30] },
{ name: '系列2', data: [15, 25, 35] },
]"
:xAxisData="['类目1', '类目2', '类目3']"
:showLegend="true"
barWidth="30%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">柱状图(水平堆叠)</span>
</div>
<FaHBarChart
:data="[
{ name: '系列1', data: [10, 20, 30] },
{ name: '系列2', data: [15, 25, 35] },
]"
:xAxisData="['类目1', '类目2', '类目3']"
:showLegend="true"
:stack="true"
barWidth="30%"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">环形图</span>
</div>
<FaRingChart
:data="[
{ value: 35, name: '分类A' },
{ value: 28, name: '分类B' },
{ value: 42, name: '分类C' },
]"
:radius="['54%', '70%']"
legendPosition="bottom"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">环形图</span>
</div>
<FaRingChart
:data="[
{ value: 35, name: '分类A' },
{ value: 28, name: '分类B' },
{ value: 42, name: '分类C' },
]"
:radius="['54%', '70%']"
legendPosition="bottom"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">环形图</span>
</div>
<FaRingChart
:data="[
{ value: 35, name: '分类A' },
{ value: 28, name: '分类B' },
{ value: 42, name: '分类C' },
{ value: 32, name: '分类D' },
{ value: 26, name: '分类E' },
{ value: 37, name: '分类F' },
]"
:radius="['54%', '70%']"
:borderRadius="0"
:showLegend="true"
legendPosition="bottom"
centerText="¥300,458"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">饼图</span>
</div>
<FaRingChart
:data="[
{ value: 30, name: '分类A' },
{ value: 25, name: '分类B' },
{ value: 45, name: '分类C' },
]"
:radius="['0%', '70%']"
:showLegend="true"
legendPosition="right"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">散点图</span>
</div>
<FaScatterChart
:data="[
{ value: [1, 3] },
{ value: [2, 4] },
{ value: [3, 5] },
{ value: [4, 6] },
{ value: [5, 7] },
{ value: [6, 8] },
{ value: [7, 7] },
{ value: [8, 9] },
{ value: [9, 8] },
{ value: [10, 6] },
{ value: [11, 7] },
{ value: [12, 8] },
]"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">环形图</span>
</div>
<FaRingChart
:data="[
{ value: 35, name: '分类A' },
{ value: 28, name: '分类B' },
{ value: 42, name: '分类C' },
{ value: 32, name: '分类D' },
{ value: 26, name: '分类E' },
{ value: 37, name: '分类F' },
]"
:radius="['54%', '70%']"
:borderRadius="0"
:showLegend="true"
legendPosition="bottom"
centerText="¥300,458"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">饼图</span>
</div>
<FaRingChart
:data="[
{ value: 30, name: '分类A' },
{ value: 25, name: '分类B' },
{ value: 45, name: '分类C' },
]"
:radius="['0%', '70%']"
:showLegend="true"
legendPosition="right"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">散点图</span>
</div>
<FaScatterChart
:data="[
{ value: [1, 3] },
{ value: [2, 4] },
{ value: [3, 5] },
{ value: [4, 6] },
{ value: [5, 7] },
{ value: [6, 8] },
{ value: [7, 7] },
{ value: [8, 9] },
{ value: [9, 8] },
{ value: [10, 6] },
{ value: [11, 7] },
{ value: [12, 8] },
]"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">雷达图</span>
</div>
<FaRadarChart
:indicator="[
{ name: '销售', max: 100 },
{ name: '管理', max: 100 },
{ name: '技术', max: 100 },
{ name: '客服', max: 100 },
{ name: '开发', max: 100 },
]"
:data="[
{
name: '预算分配',
value: [80, 70, 90, 85, 75],
},
{
name: '实际开销',
value: [70, 75, 85, 80, 70],
},
]"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">k线图</span>
</div>
<FaKLineChart
:data="[
{ time: '2024-01-01', open: 20, close: 23, high: 25, low: 18 },
{ time: '2024-01-02', open: 23, close: 21, high: 24, low: 20 },
{ time: '2024-01-03', open: 21, close: 25, high: 26, low: 21 },
]"
:showDataZoom="false"
:dataZoomStart="0"
:dataZoomEnd="100"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">双向堆叠柱状图</span>
</div>
<FaDualBarCompareChart
:positiveData="[50, 28, 80, 65, 68, 70, 60, 55]"
:negativeData="[50, 28, 40, 45, 38, 50, 42, 48]"
:xAxisData="[
'0-4岁',
'5-14岁',
'15-24岁',
'25-34岁',
'35-44岁',
'45-54岁',
'55-64岁',
'65岁以上',
]"
positiveName="男性年龄分布"
negativeName="女性年龄分布"
/>
</div>
</ElCol>
</ElRow>
</div>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">雷达图</span>
</div>
<FaRadarChart
:indicator="[
{ name: '销售', max: 100 },
{ name: '管理', max: 100 },
{ name: '技术', max: 100 },
{ name: '客服', max: 100 },
{ name: '开发', max: 100 },
]"
:data="[
{
name: '预算分配',
value: [80, 70, 90, 85, 75],
},
{
name: '实际开销',
value: [70, 75, 85, 80, 70],
},
]"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">k线图</span>
</div>
<FaKLineChart
:data="[
{ time: '2024-01-01', open: 20, close: 23, high: 25, low: 18 },
{ time: '2024-01-02', open: 23, close: 21, high: 24, low: 20 },
{ time: '2024-01-03', open: 21, close: 25, high: 26, low: 21 },
]"
:showDataZoom="false"
:dataZoomStart="0"
:dataZoomEnd="100"
/>
</div>
</ElCol>
<ElCol :xs="24" :md="12" :lg="8" class="mb-5">
<div class="fa-card">
<div class="pb-3.5">
<span class="text-base font-medium">双向堆叠柱状图</span>
</div>
<FaDualBarCompareChart
:positiveData="[50, 28, 80, 65, 68, 70, 60, 55]"
:negativeData="[50, 28, 40, 45, 38, 50, 42, 48]"
:xAxisData="[
'0-4岁',
'5-14岁',
'15-24岁',
'25-34岁',
'35-44岁',
'45-54岁',
'55-64岁',
'65岁以上',
]"
positiveName="男性年龄分布"
negativeName="女性年龄分布"
/>
</div>
</ElCol>
</ElRow>
<div class="page-content" element-loading-text="加载中...">
<FaMapChart />
</div>
</div>
</template>
@@ -325,9 +323,16 @@ import TargetVsReality from "./modules/target-vs-reality.vue";
import TopProducts from "./modules/top-products.vue";
import SalesMappingByCountry from "./modules/sales-mapping-by-country.vue";
import VolumeServiceLevel from "./modules/volume-service-level.vue";
import { defineAsyncComponent } from "vue";
defineOptions({ name: "Analysis" });
/**
* 异步加载地图组件
* 减少初始加载体积
*/
const FaMapChart = defineAsyncComponent(() => import("@/components/charts/fa-map-chart/index.vue"));
/**
* 多组数据折线图示例数据
*/
@@ -1,146 +0,0 @@
<!-- 留言墙(大抽屉,替代独立路由) -->
<template>
<ElDrawer
v-model="visible"
title="留言墙"
direction="rtl"
size="min(960px, 96vw)"
append-to-body
destroy-on-close
class="article-comment-wall-drawer"
>
<p class="mt-0 mb-8 text-g-600">每一份留言都记录了您的想法,也为我们提供了珍贵的回忆</p>
<ul
class="grid grid-cols-5 gap-5 max-2xl:grid-cols-4 max-xl:grid-cols-3 max-lg:grid-cols-2 max-sm:grid-cols-1 mb-5 list-none p-0 m-0"
>
<li
class="relative p-4 c-p aspect-16/12 duration-300 hover:-translate-y-1.5 rounded-custom-sm"
:style="{ background: item.color }"
v-for="item in commentsWithColors"
:key="item.id"
@click="openCardDrawer(item)"
>
<p class="text-g-600 text-sm">{{ item.date }}</p>
<p class="mt-4 text-sm text-gray-800">{{ item.content }}</p>
<div class="absolute bottom-4 left-0 px-4 flex-cb w-full">
<div class="flex-c">
<div class="flex-c mr-5 text-xs text-g-600">
<FaSvgIcon icon="ri:heart-line" class="mr-1 text-base" />
<span>{{ item.collection }}</span>
</div>
<div class="flex-c mr-5 text-xs text-g-600">
<FaSvgIcon icon="ri:message-3-line" class="mr-1 text-base" />
<span>{{ item.comment }}</span>
</div>
</div>
<div>
<span class="text-sm text-gray-700">{{ item.userName }}</span>
</div>
</div>
</li>
</ul>
<ElDrawer
v-model="cardDrawerOpen"
:lock-scroll="false"
:size="360"
append-to-body
modal-class="comment-modal"
>
<template #header>
<h4 class="m-0">详情</h4>
</template>
<template #default>
<div class="drawer-default">
<div
class="relative p-4 aspect-16/12 rounded-md"
:style="{ background: clickItem.color }"
>
<p class="text-g-500 text-sm">{{ clickItem.date }}</p>
<p class="mt-4 text-sm text-gray-800">{{ clickItem.content }}</p>
<div class="absolute bottom-4 left-0 px-4 flex-cb w-full">
<div class="flex-c">
<div class="flex-c mr-5 text-xs text-g-600">
<FaSvgIcon icon="ri:heart-line" class="mr-1 text-base" />
<span>{{ clickItem.collection }}</span>
</div>
<div class="flex-c mr-5 text-xs text-g-600">
<FaSvgIcon icon="ri:message-3-line" class="mr-1 text-base" />
<span>{{ clickItem.comment }}</span>
</div>
</div>
<span class="text-sm text-gray-700">{{ clickItem.userName }}</span>
</div>
</div>
</div>
</template>
</ElDrawer>
</ElDrawer>
</template>
<script setup lang="ts">
import { commentList } from "@/mock/temp/commentList";
defineOptions({ name: "ArticleCommentWallDrawer" });
interface CommentItem {
id: number;
date: string;
content: string;
collection: number;
comment: number;
userName: string;
color?: string;
}
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
"update:modelValue": [boolean];
}>();
const visible = computed({
get: () => props.modelValue,
set: (v: boolean) => emit("update:modelValue", v),
});
const COLOR_LIST = ["#D8F8FF", "#FDDFD9", "#FCE6F0", "#D3F8F0", "#FFEABC", "#F5E1FF", "#E1E6FE"];
const cardDrawerOpen = ref(false);
const clickItem = ref<CommentItem>({
id: 1,
date: "2024-9-3",
content: "加油!学好Node 自己写个小Demo",
collection: 5,
comment: 8,
userName: "匿名",
color: COLOR_LIST[0],
});
const commentsWithColors = computed(() => {
let lastColorIndex = -1;
return commentList.map((item) => {
let newIndex: number;
do {
newIndex = Math.floor(Math.random() * COLOR_LIST.length);
} while (newIndex === lastColorIndex && COLOR_LIST.length > 1);
lastColorIndex = newIndex;
return {
...item,
color: COLOR_LIST[newIndex],
};
});
});
const openCardDrawer = (item: CommentItem) => {
cardDrawerOpen.value = true;
clickItem.value = item;
};
</script>
@@ -1,194 +0,0 @@
<!-- 文章详情(抽屉,无独立路由) -->
<template>
<ElDrawer
v-model="visible"
:title="drawerTitle"
direction="rtl"
size="min(900px, 100vw)"
destroy-on-close
append-to-body
class="article-detail-drawer"
>
<div v-loading="loading" class="article-detail-drawer-inner">
<ElAlert v-if="error" :title="error" type="error" show-icon :closable="false" />
<div
v-else-if="articleHtml"
class="markdown-body article-detail-markdown"
v-highlight
v-html="articleHtml"
/>
</div>
<template #footer>
<div class="flex flex-wrap justify-end gap-2">
<ElButton v-if="articleId != null" @click="emitOpenCommentWall">
<FaSvgIcon icon="ri:message-3-line" class="mr-1" />
留言讨论
</ElButton>
<ElButton @click="visible = false">关闭</ElButton>
<ElButton v-if="articleId != null" v-auth="'edit'" type="primary" @click="onEditClick">
编辑
</ElButton>
</div>
</template>
</ElDrawer>
</template>
<script setup lang="ts">
import "@styles/core/md.scss";
import "@styles/custom/one-dark-pro.scss";
import axios from "axios";
defineOptions({ name: "ArticleDetailDrawer" });
const props = defineProps<{
modelValue: boolean;
articleId?: number | null;
}>();
const emit = defineEmits<{
"update:modelValue": [boolean];
edit: [id: number];
"open-comment-wall": [];
}>();
const emitOpenCommentWall = () => {
emit("open-comment-wall");
};
const visible = computed({
get: () => props.modelValue,
set: (v: boolean) => emit("update:modelValue", v),
});
interface ArticleResponse {
code: number;
data: {
title: string;
html_content: string;
};
}
const articleTitle = ref("");
const articleHtml = shallowRef("");
const loading = ref(false);
const error = ref<string | null>(null);
const drawerTitle = computed(() => articleTitle.value || "文章详情");
const fetchDetail = async () => {
const id = props.articleId;
if (id == null) return;
loading.value = true;
error.value = null;
articleTitle.value = "";
articleHtml.value = "";
try {
const { data } = await axios.get<ArticleResponse>(
"https://www.qiniu.lingchen.kim/blog_detail.json"
);
if (data.code === 200) {
articleTitle.value = data.data.title;
articleHtml.value = data.data.html_content;
} else {
error.value = "文章加载失败";
}
} catch (err) {
error.value = "文章加载失败";
console.error("获取文章详情失败:", err);
} finally {
loading.value = false;
}
};
const onEditClick = () => {
if (props.articleId != null) {
emit("edit", props.articleId);
}
};
watch(
() => [props.modelValue, props.articleId] as const,
([open, id]) => {
if (open && id != null) {
nextTick(() => fetchDetail());
}
}
);
</script>
<style lang="scss" scoped>
.article-detail-drawer-inner {
min-height: 120px;
}
.article-detail-markdown {
margin-top: 8px;
:deep(img) {
width: 100%;
border: 1px solid var(--fa-gray-200);
}
:deep(pre) {
position: relative;
&:hover {
.copy-button {
opacity: 1;
}
}
&::before {
position: absolute;
top: 0;
left: 50px;
width: 1px;
height: 100%;
content: "";
background: #0a0a0e;
}
}
:deep(.code-wrapper) {
overflow-x: auto;
}
:deep(.line-number) {
position: sticky;
left: 0;
z-index: 2;
box-sizing: border-box;
display: inline-block;
width: 50px;
margin-right: 10px;
font-size: 14px;
color: #9e9e9e;
text-align: center;
}
:deep(.copy-button) {
position: absolute;
top: 6px;
right: 6px;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
font-size: 20px;
line-height: 40px;
color: #999;
text-align: center;
cursor: pointer;
background-color: #000;
border: none;
border-radius: 8px;
opacity: 0;
transition: all 0.2s;
}
}
</style>
@@ -1,377 +0,0 @@
<!-- 文章新增 / 编辑(抽屉,替代独立发布页) -->
<template>
<ElDrawer
v-model="visible"
:title="drawerTitle"
direction="rtl"
size="min(920px, 100vw)"
destroy-on-close
append-to-body
class="article-publish-drawer"
>
<div class="article-publish-drawer-body">
<template v-if="resultPhase === 'form'">
<div>
<ElRow :gutter="10">
<ElCol :span="18">
<ElInput
v-model.trim="articleName"
placeholder="请输入文章标题(最多100个字符)"
maxlength="100"
/>
</ElCol>
<ElCol :span="6">
<ElSelect v-model="articleType" placeholder="请选择文章类型" filterable>
<ElOption
v-for="item in articleTypes"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElCol>
</ElRow>
<FaWangEditor class="mt-2.5" v-model="editorHtml" />
<div class="p-5 mt-5 fa-card-xs">
<h2 class="mb-5 text-xl font-medium">发布设置</h2>
<ElForm>
<ElFormItem label="封面">
<div class="mt-2.5">
<ElUpload
:action="uploadImageUrl"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="onSuccess"
:on-error="onError"
:before-upload="beforeUpload"
>
<div
v-if="!cover"
class="flex-cc flex-col w-65 h-40 border border-dashed border-[#d9d9d9] rounded-md"
>
<ElIcon class="!text-xl !text-g-600"><Plus /></ElIcon>
<div class="mt-2 text-sm text-g-600">点击上传封面</div>
</div>
<img v-else :src="cover" class="block w-65 h-40 object-cover" />
</ElUpload>
<div class="mt-2 text-xs text-g-700">建议尺寸 16:9,jpg/png 格式</div>
</div>
</ElFormItem>
<ElFormItem label="可见">
<ElSwitch v-model="visibleFlag" />
</ElFormItem>
</ElForm>
<div class="flex justify-end">
<ElButton type="primary" @click="submit" class="w-25">
{{ pageMode === PageModeEnum.Edit ? "保存" : "发布" }}
</ElButton>
</div>
</div>
</div>
</template>
<FaResultPage
v-else-if="resultPhase === 'success'"
type="success"
:title="successTitle"
message="文章操作已完成。简单反馈也可用全局 Message;此处用于完整结果反馈,下方灰色区域可展示补充说明。"
icon-code="ri:check-fill"
>
<template #content>
<p v-if="pageMode === PageModeEnum.Add">文章已发布,读者可在列表与详情中查看。</p>
<p v-else>修改已保存,内容已更新。</p>
</template>
<template #buttons>
<ElButton type="primary" v-ripple @click="backToForm">继续编辑</ElButton>
<ElButton v-ripple @click="finishAndClose">关闭并返回列表</ElButton>
<ElButton v-if="pageMode === PageModeEnum.Add" v-ripple @click="publishAgain">
新建一篇
</ElButton>
</template>
</FaResultPage>
<FaResultPage
v-else
type="fail"
:title="failTitle"
message="请核对并修改以下信息后,再重新提交。"
icon-code="ri:close-fill"
>
<template #content>
<p>本次提交未成功,可能原因如下:</p>
<p>
<FaSvgIcon icon="ri:close-circle-line" class="text-red-500 mr-1" />
<span>网络或服务暂时不可用,请稍后重试</span>
</p>
<p>
<FaSvgIcon icon="ri:close-circle-line" class="text-red-500 mr-1" />
<span>请确认必填项与封面已正确填写</span>
</p>
</template>
<template #buttons>
<ElButton type="primary" v-ripple @click="backToForm">返回修改</ElButton>
<ElButton v-ripple @click="finishAndClose">关闭并返回列表</ElButton>
</template>
</FaResultPage>
</div>
</ElDrawer>
</template>
<script setup lang="ts">
import { Plus } from "@element-plus/icons-vue";
import { ApiStatus } from "@utils/http";
import { useUserStore } from "@stores/modules/user.store";
import { EmojiText } from "@utils/ui";
import { PageModeEnum } from "@/enums/formEnum";
import axios from "axios";
defineOptions({ name: "ArticlePublishDrawer" });
const props = withDefaults(
defineProps<{
modelValue: boolean;
/** 编辑的文章 id;不传或 null 为新增 */
editId?: number | null;
}>(),
{ editId: null }
);
const emit = defineEmits<{
"update:modelValue": [boolean];
success: [];
}>();
const visible = computed({
get: () => props.modelValue,
set: (v: boolean) => emit("update:modelValue", v),
});
interface ArticleType {
id: number;
name: string;
}
interface UploadResponse {
data: {
url: string;
};
}
interface ArticleDetailResponse {
code: number;
data: {
title: string;
blog_class: string;
html_content: string;
};
}
const MAX_IMAGE_SIZE = 2;
const EMPTY_EDITOR_CONTENT = "<p><br></p>";
const userStore = useUserStore();
const { accessToken } = userStore;
const uploadImageUrl = `${import.meta.env.VITE_API_URL}/api/common/upload`;
const uploadHeaders = { Authorization: accessToken };
type ResultPhase = "form" | "success" | "fail";
const resultPhase = ref<ResultPhase>("form");
const pageMode = ref<PageModeEnum>(PageModeEnum.Add);
const articleName = ref("");
const articleType = ref<number>();
const articleTypes = ref<ArticleType[]>([]);
const editorHtml = ref("");
const cover = ref("");
/** 与 ElSwitch `visible` 避免命名冲突 */
const visibleFlag = ref(true);
const drawerTitle = computed(() =>
pageMode.value === PageModeEnum.Edit ? "编辑文章" : "新增文章"
);
const successTitle = computed(() =>
pageMode.value === PageModeEnum.Edit ? "保存成功" : "发布成功"
);
const failTitle = computed(() => (pageMode.value === PageModeEnum.Edit ? "保存失败" : "发布失败"));
const backToForm = () => {
resultPhase.value = "form";
};
const finishAndClose = () => {
emit("success");
visible.value = false;
};
const publishAgain = () => {
pageMode.value = PageModeEnum.Add;
articleName.value = "";
articleType.value = undefined;
editorHtml.value = "";
cover.value = "";
visibleFlag.value = true;
resultPhase.value = "form";
};
const initWhenOpen = () => {
resultPhase.value = "form";
pageMode.value = props.editId != null ? PageModeEnum.Edit : PageModeEnum.Add;
if (pageMode.value === PageModeEnum.Edit) {
getArticleDetail();
} else {
articleName.value = "";
articleType.value = undefined;
editorHtml.value = "";
cover.value = "";
visibleFlag.value = true;
}
};
const getArticleTypes = async () => {
try {
const { data } = await axios.get("https://www.qiniu.lingchen.kim/classify.json");
if (data.code === 200) {
articleTypes.value = data.data;
}
} catch (error) {
console.error("获取文章分类失败:", error);
ElMessage.error("获取文章分类失败");
}
};
const getArticleDetail = async () => {
try {
const { data } = await axios.get<ArticleDetailResponse>(
"https://www.qiniu.lingchen.kim/blog_list.json"
);
if (data.code === ApiStatus.success) {
const { title, blog_class, html_content } = data.data;
articleName.value = title;
articleType.value = Number(blog_class);
editorHtml.value = html_content;
}
} catch (error) {
console.error("获取文章详情失败:", error);
ElMessage.error("获取文章详情失败");
}
};
const validateArticle = (): boolean => {
if (!articleName.value.trim()) {
ElMessage.error("请输入文章标题");
return false;
}
if (!articleType.value) {
ElMessage.error("请选择文章类型");
return false;
}
if (!editorHtml.value || editorHtml.value === EMPTY_EDITOR_CONTENT) {
ElMessage.error("请输入文章内容");
return false;
}
if (!cover.value) {
ElMessage.error("请上传封面图片");
return false;
}
return true;
};
const cleanCodeContent = (content: string): string => {
return content.replace(/(\s*)<\/code>/g, "</code>");
};
const addArticle = async () => {
if (!validateArticle()) return;
try {
const cleanedContent = cleanCodeContent(editorHtml.value);
console.log("新增文章:", { cleanedContent });
await new Promise<void>((resolve) => setTimeout(resolve, 400));
resultPhase.value = "success";
} catch (error) {
console.error("发布文章失败:", error);
resultPhase.value = "fail";
}
};
const editArticle = async () => {
if (!validateArticle()) return;
try {
const cleanedContent = cleanCodeContent(editorHtml.value);
console.log("编辑文章:", { cleanedContent });
await new Promise<void>((resolve) => setTimeout(resolve, 400));
resultPhase.value = "success";
} catch (error) {
console.error("保存文章失败:", error);
resultPhase.value = "fail";
}
};
const submit = () => {
if (pageMode.value === PageModeEnum.Edit) {
editArticle();
} else {
addArticle();
}
};
const onSuccess = (response: UploadResponse) => {
cover.value = response.data.url;
ElMessage.success(`图片上传成功 ${EmojiText[200]}`);
};
const onError = () => {
ElMessage.error(`图片上传失败 ${EmojiText[500]}`);
};
const beforeUpload = (file: File): boolean => {
const isImage = file.type.startsWith("image/");
const isLt2M = file.size / 1024 / 1024 < MAX_IMAGE_SIZE;
if (!isImage) {
ElMessage.error("只能上传图片文件");
return false;
}
if (!isLt2M) {
ElMessage.error(`图片大小不能超过 ${MAX_IMAGE_SIZE}MB`);
return false;
}
return true;
};
watch(
() => props.modelValue,
(open) => {
if (open) {
nextTick(() => {
initWhenOpen();
getArticleTypes();
});
}
}
);
</script>
<style scoped lang="scss">
.article-publish-drawer-body {
padding-bottom: env(safe-area-inset-bottom, 0);
}
</style>
@@ -1,271 +0,0 @@
<!-- 文章列表(挂载于 /dashboard/article,原 widgets/article) -->
<template>
<div class="page-content !mb-5">
<ElRow justify="space-between" :gutter="10">
<ElCol :lg="6" :md="6" :sm="14" :xs="16">
<ElInput
v-model="searchVal"
:prefix-icon="Search"
clearable
placeholder="输入文章标题查询"
@keyup.enter="searchArticle"
/>
</ElCol>
<ElCol :lg="12" :md="12" :sm="0" :xs="0">
<div class="custom-segmented">
<ElSegmented v-model="yearVal" :options="YEAR_OPTIONS" @change="searchArticleByYear" />
</div>
</ElCol>
<ElCol
:lg="6"
:md="6"
:sm="10"
:xs="6"
style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: end"
>
<ElButton @click="commentWallOpen = true">
<FaSvgIcon icon="ri:message-3-line" class="mr-1" />
留言墙
</ElButton>
<ElButton type="primary" @click="toAddArticle" v-auth="'add'">新增文章</ElButton>
</ElCol>
</ElRow>
<div class="mt-5">
<div
class="grid grid-cols-5 gap-5 max-2xl:grid-cols-4 max-xl:grid-cols-3 max-lg:grid-cols-2 max-sm:grid-cols-1"
>
<div
class="group c-p overflow-hidden border border-g-300/60 rounded-custom-sm"
v-for="item in articleList"
:key="item.id"
@click="toDetail(item)"
>
<div class="relative aspect-[16/9.5]">
<ElImage
class="flex align-center justify-center w-full h-full object-cover bg-gray-200"
:src="item.home_img"
lazy
fit="cover"
></ElImage>
<span class="absolute top-1 right-1 bg-black/50 rounded text-xs px-1 py-0.5 text-white">
{{ item.type_name }}
</span>
</div>
<div class="px-2 py-1">
<h2 class="text-base text-g-800 font-medium">{{ item.title }}</h2>
<div class="flex-b w-full h-6 mt-1">
<div class="flex-c text-g-500">
<FaSvgIcon icon="ri:time-line" class="mr-1 text-sm" />
<span class="text-sm">{{ useDateFormat(item.create_time, "YYYY-MM-DD") }}</span>
<div class="w-px h-3 bg-g-400 mx-3.5"></div>
<FaSvgIcon icon="ri:eye-line" class="mr-1 text-sm" />
<span class="text-sm">{{ item.count }}</span>
</div>
<ElButton
class="opacity-0 group-hover:opacity-100"
v-auth="'edit'"
size="small"
@click.stop="toEdit(item)"
>
编辑
</ElButton>
</div>
</div>
</div>
</div>
</div>
<div style="margin-top: 16vh" v-if="showEmpty">
<ElEmpty :description="`未找到相关数据 ${EmojiText[0]}`" />
</div>
<div style="display: flex; justify-content: center; margin-top: 20px">
<ElPagination
size="default"
background
v-model:current-page="currentPage"
:page-size="pageSize"
:pager-count="9"
layout="prev, pager, next, total,jumper"
:total="total"
:hide-on-single-page="true"
@current-change="handleCurrentChange"
/>
</div>
<ArticleDetailDrawer
v-model="detailDrawerOpen"
:article-id="detailArticleId"
@edit="onDetailEdit"
@open-comment-wall="onOpenCommentFromDetail"
/>
<ArticleCommentWallDrawer v-model="commentWallOpen" />
<ArticlePublishDrawer
v-model="publishDrawerOpen"
:edit-id="publishEditId"
@success="onPublishSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { Search } from "@element-plus/icons-vue";
import ArticleCommentWallDrawer from "../components/ArticleCommentWallDrawer.vue";
import ArticleDetailDrawer from "../components/ArticleDetailDrawer.vue";
import ArticlePublishDrawer from "../components/ArticlePublishDrawer.vue";
import { useDateFormat } from "@vueuse/core";
import { EmojiText } from "@utils/ui";
import { ArticleList } from "@/mock/temp/articleList";
import { useCommon } from "@/hooks/core/useCommon";
defineOptions({ name: "ArticleList" });
const route = useRoute();
const router = useRouter();
interface Article {
id: number;
home_img: string;
type_name: string;
title: string;
create_time: string;
count: number;
}
interface GetArticleListOptions {
backTop?: boolean;
}
const YEAR_OPTIONS = ["All", "2024", "2023", "2022", "2021", "2020", "2019"];
const PAGE_SIZE = 40;
const yearVal = ref("All");
const searchVal = ref("");
const articleList = ref<Article[]>([]);
const currentPage = ref(1);
const pageSize = ref(PAGE_SIZE);
const total = ref(0);
const isLoading = ref(true);
const showEmpty = computed(() => articleList.value.length === 0 && !isLoading.value);
const detailDrawerOpen = ref(false);
const detailArticleId = ref<number | null>(null);
const publishDrawerOpen = ref(false);
const publishEditId = ref<number | null>(null);
const commentWallOpen = ref(false);
const onPublishSuccess = () => {
getArticleList({ backTop: false });
};
const syncCommentWallQuery = () => {
const q = { ...route.query } as Record<string, string | string[]>;
if (q.commentWall === "1") {
commentWallOpen.value = true;
delete q.commentWall;
router.replace({ path: route.path, query: q });
}
};
const onOpenCommentFromDetail = () => {
detailDrawerOpen.value = false;
nextTick(() => {
commentWallOpen.value = true;
});
};
const onDetailEdit = (id: number) => {
detailDrawerOpen.value = false;
nextTick(() => {
publishEditId.value = id;
publishDrawerOpen.value = true;
});
};
const getArticleList = async ({ backTop = false }: GetArticleListOptions = {}) => {
isLoading.value = true;
try {
if (searchVal.value) {
yearVal.value = "All";
}
// TODO: 替换为真实 API 调用
// const params = {
// page: currentPage.value,
// size: pageSize.value,
// searchVal: searchVal.value,
// year: yearVal.value === 'All' ? '' : yearVal.value
// }
// const res = await ArticleService.getArticleList(params)
articleList.value = ArticleList as Article[];
if (backTop) {
useCommon().scrollToTop();
}
} catch (error) {
console.error("获取文章列表失败:", error);
} finally {
isLoading.value = false;
}
};
const searchArticle = () => {
currentPage.value = 1;
getArticleList({ backTop: true });
};
const searchArticleByYear = () => {
currentPage.value = 1;
getArticleList({ backTop: true });
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
getArticleList({ backTop: true });
};
const toDetail = (item: Article) => {
detailArticleId.value = item.id;
detailDrawerOpen.value = true;
};
const toEdit = (item: Article) => {
publishEditId.value = item.id;
publishDrawerOpen.value = true;
};
const toAddArticle = () => {
publishEditId.value = null;
publishDrawerOpen.value = true;
};
onMounted(() => {
getArticleList();
syncCommentWallQuery();
});
watch(
() => route.query.commentWall,
(v) => {
if (v === "1") syncCommentWallQuery();
}
);
</script>
<style lang="scss">
.custom-segmented .el-segmented {
height: 40px;
padding: 6px;
--el-border-radius-base: 8px;
}
</style>
@@ -1,511 +0,0 @@
<!-- 工作台页面 -->
<template>
<div>
<CardList></CardList>
<ElRow :gutter="20">
<ElCol :sm="24" :md="12" :lg="10">
<ActiveUser />
</ElCol>
<ElCol :sm="24" :md="12" :lg="14">
<SalesOverview />
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :sm="24" :md="24" :lg="12">
<NewUser />
</ElCol>
<ElCol :sm="24" :md="12" :lg="6">
<Dynamic />
</ElCol>
<ElCol :sm="24" :md="12" :lg="6">
<TodoList />
</ElCol>
</ElRow>
<AboutProject />
<!-- —— 卡片组件演示 —— -->
<div class="console-cards-showcase border-g-200">
<div class="py-5">
<h2 class="page-title">统计卡片(文字)</h2>
<ElRow :gutter="20">
<ElCol
v-for="card in statsCards"
:key="`t-${card.id}`"
:xs="24"
:sm="12"
:md="6"
class="mb-5"
>
<FaStatsCard
:icon="card.icon"
:iconStyle="card.iconStyle"
:title="card.title"
:description="card.description"
:showArrow="card.showArrow"
/>
</ElCol>
</ElRow>
<h2 class="page-title">统计卡片(数字滚动)</h2>
<ElRow :gutter="20">
<ElCol
v-for="card in statsCards"
:key="`n-${card.id}`"
:xs="24"
:sm="12"
:md="6"
class="mb-5"
>
<FaStatsCard
:icon="card.icon"
:iconStyle="card.iconStyle"
:count="card.count"
:description="card.description"
:decimals="0"
:showArrow="card.showArrow"
separator=","
/>
</ElCol>
</ElRow>
<h2 class="page-title">统计卡片(自定义样式)</h2>
<ElRow :gutter="20">
<ElCol
v-for="card in statsCards"
:key="`c-${card.id}`"
:xs="24"
:sm="12"
:md="6"
class="mb-5"
>
<FaStatsCard
:icon="card.icon"
:iconStyle="card.customIconStyle"
:boxStyle="card.boxStyle"
:title="card.title"
:description="card.description"
:textColor="card.textColor"
:showArrow="card.showArrow"
/>
</ElCol>
</ElRow>
<h2 class="page-title">进度卡片</h2>
<ElRow :gutter="20">
<ElCol
v-for="card in progressCards"
:key="`p-${card.id}`"
:xs="24"
:sm="12"
:md="6"
class="mb-5"
>
<FaProgressCard :percentage="card.percentage" :title="card.title" :color="card.color" />
</ElCol>
</ElRow>
<h2 class="page-title">进度卡片(icon)</h2>
<ElRow :gutter="20">
<ElCol
v-for="card in progressCards"
:key="`pi-${card.id}`"
:xs="24"
:sm="12"
:md="6"
class="mb-5"
>
<FaProgressCard
:percentage="card.percentage"
:title="card.title"
:color="card.color"
:icon="card.icon"
:iconStyle="card.iconStyle"
/>
</ElCol>
</ElRow>
<h2 class="page-title">图表卡片(小图表)</h2>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6" class="mb-5">
<FaLineChartCard
:isMiniChart="true"
:value="2545"
label="新用户"
date="过去7天"
:percentage="1.2"
:height="9.5"
:chartData="[120, 132, 101, 134, 90, 230, 210]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6" class="mb-5">
<FaBarChartCard
:isMiniChart="true"
:value="15480"
label="浏览量"
date="过去 14 天"
:percentage="-4.15"
:height="9.5"
:barWidth="'45%'"
:chartData="[120, 100, 150, 140, 90, 120, 130]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6" class="mb-5">
<FaLineChartCard
:isMiniChart="true"
:value="2545"
label="粉丝数"
date="过去 30 天"
:percentage="1.2"
:height="9.5"
:showAreaColor="true"
:chartData="[150, 180, 160, 200, 180, 220, 240]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6" class="mb-5">
<FaDonutChartCard
:value="36358"
title="粉丝量"
:percentage="18"
percentageLabel="较去年"
:data="[50, 40]"
:height="9.5"
currentValue="2022"
previousValue="2021"
:radius="['50%', '70%']"
/>
</ElCol>
</ElRow>
<h2 class="page-title">图表卡片(大图表)</h2>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :md="6" class="mb-5">
<FaLineChartCard
:value="2545"
label="新用户"
:percentage="1.2"
:height="11"
:chartData="[120, 132, 101, 134, 90, 230, 210]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6" class="mb-5">
<FaBarChartCard
:value="15480"
label="浏览量"
:percentage="-4.15"
:height="11"
:chartData="[120, 100, 150, 140, 90, 120, 130, 110]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6" class="mb-5">
<FaLineChartCard
:value="2545"
label="粉丝数"
:percentage="1.2"
:height="11"
:showAreaColor="true"
:chartData="[150, 180, 160, 200, 180, 220, 240]"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :md="6" class="mb-5">
<FaDonutChartCard
:value="36358"
title="粉丝量"
:percentage="-18"
percentageLabel="较2021年"
:data="[70, 30]"
:height="11"
currentValue="12月"
previousValue="11月"
/>
</ElCol>
</ElRow>
<h2 class="page-title">数据列表卡片</h2>
<ElRow :gutter="20">
<ElCol :xs="24" :sm="12" :lg="8" class="mb-5">
<FaDataListCard :list="dataList" title="待办事项" subtitle="今日待处理任务" />
</ElCol>
<ElCol :xs="24" :sm="12" :lg="8" class="mb-5">
<FaDataListCard
:maxCount="4"
:list="dataList"
title="最近活动"
subtitle="近期活动列表"
:showMoreButton="true"
@more="handleMore"
/>
</ElCol>
<ElCol :xs="24" :sm="12" :lg="8" class="mb-5">
<FaTimelineListCard :list="timelineData" title="最近交易" subtitle="2024年12月20日" />
</ElCol>
</ElRow>
<h2 class="page-title">图片卡片</h2>
<ElRow :gutter="20">
<ElCol v-for="card in imageCards" :key="card.id" :xs="24" :sm="12" :md="6" class="mb-5">
<FaImageCard
:imageUrl="card.imageUrl"
:title="card.title"
:category="card.category"
:readTime="card.readTime"
:views="card.views"
:comments="card.comments"
:date="card.date"
@click="handleImageCardClick(card)"
/>
</ElCol>
</ElRow>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from "element-plus";
import cover1 from "@imgs/cover/img1.webp";
import cover2 from "@imgs/cover/img2.webp";
import cover3 from "@imgs/cover/img3.webp";
import cover4 from "@imgs/cover/img4.webp";
import CardList from "./modules/card-list.vue";
import ActiveUser from "./modules/active-user.vue";
import SalesOverview from "./modules/sales-overview.vue";
import NewUser from "./modules/new-user.vue";
import Dynamic from "./modules/dynamic-stats.vue";
import TodoList from "./modules/todo-list.vue";
import AboutProject from "./modules/about-project.vue";
defineOptions({ name: "Console" });
const statsCards = [
{
id: 1,
title: "销售产品",
count: 1235,
description: "鞋子、牛仔裤、派对服装、手表",
icon: "ri:bar-chart-box-line",
boxStyle: "!bg-theme/10",
customIconStyle: "!text-theme !text-3xl",
iconStyle: "bg-info",
textColor: "var(--theme-color)",
showArrow: false,
},
{
id: 2,
title: "活跃用户",
count: 5000,
description: "日活跃用户超过5,000+",
icon: "ri:account-box-2-line",
boxStyle: "!bg-warning/10",
customIconStyle: "!text-warning !text-3xl",
iconStyle: "bg-warning",
textColor: "var(--fa-warning)",
showArrow: false,
},
{
id: 3,
title: "总收入",
count: 35000,
description: "月收入超过¥350,000+",
icon: "ri:money-cny-box-line",
boxStyle: "!bg-secondary/10",
customIconStyle: "!text-secondary !text-3xl",
iconStyle: "bg-secondary",
textColor: "var(--fa-secondary)",
showArrow: false,
},
{
id: 4,
title: "客户评价",
count: 4800,
description: "平均评分4.8/5",
icon: "ri:message-3-line",
boxStyle: "!bg-error/10",
customIconStyle: "!text-error !text-3xl",
iconStyle: "bg-error",
textColor: "var(--fa-error)",
showArrow: false,
},
];
const progressCards = [
{
id: 1,
title: "完成进度",
percentage: 75,
color: "var(--fa-success)",
icon: "ri:arrow-up-circle-line",
iconStyle: "bg-success/12 text-success",
},
{
id: 2,
title: "项目进度",
percentage: 65,
color: "var(--theme-color)",
icon: "ri:twitch-line",
iconStyle: "bg-theme/12 text-theme",
},
{
id: 3,
title: "学习进度",
percentage: 45,
color: "var(--fa-error)",
icon: "ri:game-line",
iconStyle: "!bg-error/12 text-error",
},
{
id: 4,
title: "任务进度",
percentage: 90,
color: "var(--fa-secondary)",
icon: "ri:flag-2-line",
iconStyle: "bg-secondary/12 text-secondary",
},
];
const imageCards = [
{
id: 1,
imageUrl: cover1,
title: "AI技术在医疗领域的创新应用与发展前景",
category: "社交",
readTime: "2分钟",
views: 9125,
comments: 3,
date: "12月19日 周一",
},
{
id: 2,
imageUrl: cover2,
title: "大数据分析助力企业决策的实践案例",
category: "技术",
readTime: "3分钟",
views: 7234,
comments: 5,
date: "12月20日 周二",
},
{
id: 3,
imageUrl: cover3,
title: "区块链技术在供应链管理中的应用",
category: "科技",
readTime: "4分钟",
views: 5678,
comments: 8,
date: "12月21日 周三",
},
{
id: 4,
imageUrl: cover4,
title: "云计算技术发展趋势与未来展望",
category: "云技术",
readTime: "5分钟",
views: 4321,
comments: 6,
date: "12月22日 周四",
},
];
const dataList = [
{
title: "新加坡之行",
status: "进行中",
time: "5分钟",
class: "bg-theme/12 text-theme",
icon: "ri:camera-4-line",
},
{
title: "归档数据",
status: "进行中",
time: "10分钟",
class: "bg-secondary/12 text-secondary",
icon: "ri:bar-chart-box-line",
},
{
title: "客户会议",
status: "待处理",
time: "15分钟",
class: "bg-warning/12 text-warning",
icon: "ri:user-3-line",
},
{
title: "筛选任务团队",
status: "进行中",
time: "20分钟",
class: "bg-error/12 text-error",
icon: "ri:account-circle-line",
},
{
title: "发送信封给小王",
status: "已完成",
time: "20分钟",
class: "bg-success/12 text-success",
icon: "ri:message-3-line",
},
];
const timelineData = [
{
time: "上午 09:30",
status: "rgb(73, 190, 255)",
content: "收到 John Doe 支付的 385.90 美元",
},
{
time: "上午 10:00",
status: "rgb(54, 158, 255)",
content: "新销售记录",
code: "ML-3467",
},
{
time: "上午 12:00",
status: "rgb(103, 232, 207)",
content: "向 Michael 支付了 64.95 美元",
},
{
time: "下午 14:30",
status: "rgb(255, 193, 7)",
content: "系统维护通知",
code: "MT-2023",
},
{
time: "下午 15:45",
status: "rgb(255, 105, 105)",
content: "紧急订单取消提醒",
code: "OR-9876",
},
{
time: "下午 17:00",
status: "rgb(103, 232, 207)",
content: "完成每日销售报表",
},
];
function handleMore() {
ElMessage.info("查看更多");
}
interface ImageCard {
id: number;
imageUrl: string;
title: string;
category: string;
readTime: string;
views: number;
comments: number;
date: string;
}
function handleImageCardClick(card: ImageCard) {
console.log("点击卡片:", card);
}
</script>
<style scoped>
@reference "@styles/core/tailwind.css";
.page-title {
@apply my-5 text-xl font-medium first:mt-0;
}
</style>
@@ -1,77 +0,0 @@
<!-- 电子商务页面 -->
<template>
<div class="ecommerce">
<ElRow :gutter="20">
<ElCol :sm="24" :md="24" :lg="16">
<Banner />
</ElCol>
<ElCol :sm="12" :md="12" :lg="4">
<TotalOrderVolume />
</ElCol>
<ElCol :sm="12" :md="12" :lg="4">
<TotalProducts />
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :sm="12" :md="12" :lg="8">
<SalesTrend />
</ElCol>
<ElCol :sm="12" :md="12" :lg="8">
<SalesClassification />
</ElCol>
<ElCol :sm="24" :md="24" :lg="8">
<ElRow :gutter="20">
<ElCol :sm="24" :md="12" :lg="12">
<ProductSales />
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<SalesGrowth />
</ElCol>
<ElCol :span="24" class="no-margin-bottom">
<CartConversionRate />
</ElCol>
</ElRow>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :sm="24" :md="12" :lg="8">
<HotCommodity />
</ElCol>
<ElCol :sm="24" :md="12" :lg="8">
<AnnualSales />
</ElCol>
<ElCol :sm="24" :md="24" :lg="8">
<TransactionList />
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :md="24" :lg="8">
<RecentTransaction />
</ElCol>
<ElCol :md="24" :lg="16" class="no-margin-bottom">
<HotProductsList />
</ElCol>
</ElRow>
</div>
</template>
<script setup lang="ts">
import Banner from "./modules/banner.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 TransactionList from "./modules/transaction-list.vue";
import HotCommodity from "./modules/hot-commodity.vue";
import RecentTransaction from "./modules/recent-transaction.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";
import HotProductsList from "./modules/hot-products-list.vue";
defineOptions({ name: "Ecommerce" });
</script>
@@ -1,17 +0,0 @@
<template>
<div class="page-content mb-5" element-loading-text="加载中...">
<FaMapChart />
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from "vue";
defineOptions({ name: "DashboardMap" });
/**
* 异步加载地图组件
* 减少初始加载体积
*/
const FaMapChart = defineAsyncComponent(() => import("@/components/charts/fa-map-chart/index.vue"));
</script>
@@ -1,149 +0,0 @@
<template>
<div class="pt-24 px-20 max-md:!pt-15 max-sm:!px-5 max-md:!px-8 bg-transparent !border-none">
<div class="mb-10 text-center">
<h1
class="mb-2 text-4xl font-medium max-sm:text-3xl flex flex-wrap justify-center items-baseline gap-x-2 gap-y-1"
>
<span>超过</span>
<FaCountTo
class="text-4xl font-medium max-sm:text-3xl"
:target="53476"
:duration="2000"
separator=","
/>
<span>位信赖的开发者</span>
</h1>
<h2 class="mb-2.5 text-2xl font-normal text-g-600 max-sm:text-2xl">以及众多科技巨头的选择</h2>
<div class="gap-2 flex-cc mt-2.5 max-sm:mt-3.5 max-sm:!block">
<p class="text-sm italic text-g-600 max-sm:text-sm">
本项目基于 MIT 协议开源免费,当前页面为定价模板,仅作演示用途
</p>
<ElTag type="success" size="large" round class="max-sm:mt-3.5">免费商用</ElTag>
</div>
</div>
<div class="mt-20 max-md:mt-0">
<ElRow :gutter="20" justify="center">
<ElCol v-for="plan in pricingPlans" :key="plan.type" :xs="24" :sm="12" :md="6" class="mb-5">
<ElCard class="flex flex-col h-full rounded-xl">
<div class="mb-5">
<h3 class="mb-2.5 text-xl font-medium">{{ plan.title }}</h3>
<p
class="h-10 pb-5 mb-5 overflow-hidden text-sm text-g-600 text-ellipsis border-b border-g-300/80 line-clamp-2"
>
{{ plan.description }}
</p>
<div class="mt-7.5 flex flex-wrap items-baseline gap-x-1">
<FaCountTo
class="text-3xl font-medium"
:target="plan.price"
:duration="1400"
prefix="¥"
:decimals="0"
/>
<span class="ml-2 text-sm text-g-600">/一次性付款</span>
</div>
</div>
<div class="flex-grow mb-5">
<div
v-for="(feature, index) in plan.features"
:key="index"
class="flex-c mb-2.5 text-sm"
>
<ElIcon class="mr-2.5" :class="feature.available ? '!text-theme' : '!text-danger'">
<Check v-if="feature.available" />
<Close v-else />
</ElIcon>
<span>{{ feature.text }}</span>
</div>
</div>
<div class="mt-auto text-center">
<ElButton type="primary" class="w-full h-10" v-ripple>立即购买</ElButton>
</div>
</ElCard>
</ElCol>
</ElRow>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Check, Close } from "@element-plus/icons-vue";
defineOptions({ name: "DashboardPricing" });
interface Feature {
text: string;
available: boolean;
}
interface PricingPlan {
type: string;
title: string;
description: string;
price: number;
features: Feature[];
}
const pricingPlans = ref<PricingPlan[]>([
{
type: "single",
title: "单次使用版",
description: "适用于单个最终产品,最终用户无需付费。",
price: 349,
features: [
{ text: "完整源代码", available: true },
{ text: "技术文档", available: true },
{ text: "SaaS应用授权", available: false },
{ text: "单个项目使用", available: true },
{ text: "一年技术支持", available: true },
{ text: "一年免费更新", available: true },
],
},
{
type: "multiple",
title: "多次使用版",
description: "适用于无限个最终产品,最终用户无需付费。",
price: 629,
features: [
{ text: "完整源代码", available: true },
{ text: "技术文档", available: true },
{ text: "SaaS应用授权", available: false },
{ text: "无限项目使用", available: true },
{ text: "一年技术支持", available: true },
{ text: "一年免费更新", available: true },
],
},
{
type: "extended",
title: "扩展授权版",
description: "适用于单个最终产品,最终用户需要付费。",
price: 2099,
features: [
{ text: "完整源代码", available: true },
{ text: "技术文档", available: true },
{ text: "SaaS应用授权", available: true },
{ text: "单个项目使用", available: true },
{ text: "一年技术支持", available: true },
{ text: "一年免费更新", available: true },
],
},
{
type: "unlimited",
title: "无限授权版",
description: "适用于无限个最终产品,最终用户需要付费。",
price: 3499,
features: [
{ text: "完整源代码", available: true },
{ text: "技术文档", available: true },
{ text: "SaaS应用授权", available: true },
{ text: "无限项目使用", available: true },
{ text: "一年技术支持", available: true },
{ text: "一年免费更新", available: true },
],
},
]);
</script>
File diff suppressed because it is too large Load Diff
@@ -1,574 +0,0 @@
/**
* 功能验收手册:除「用户 / 角色」在 `index.vue` 中详述外,其余页用「完整性验收」结构展示。
*
* **何为完整性**:`notes` 与当前页**已实现**的界面与操作项对应,用于验收时逐条核对**是否漏项**(不评价交互好坏)。
* **与后端菜单对齐**:标题与 `sys_menu.json` 中菜单页 `component_path` 一致;壳层/静态页见各条说明。
* **`path` 字段**:默认可视为 `src/views/` 下相对路径;`../layouts` 与 `locales` 等见各条。
*/
export type CompactManualPage = {
anchor: string;
title: string;
path: string;
notes: string[];
};
export type ManualModuleSection = {
anchor: string;
heading: string;
pkgTag?: string;
pages: CompactManualPage[];
};
/** 系统管理模块内、接在「角色管理」后的页面 */
export const MANUAL_SYSTEM_TAIL_PAGES: CompactManualPage[] = [
{
anchor: "page-menu",
title: "菜单管理",
path: "module_system/menu/index.vue",
notes: [
"检索:FaSearchBar(菜单名称等)。",
"分区:Tab「PC 桌面菜单管理 / APP 移动端菜单管理」两套树数据。",
"主区:树形表格(勾选、行点击);展开/收起工具按钮。",
"工具栏:新增、批量删除、批量更多(patch)、刷新、列配置、显示搜索栏。",
"弹层:详情 Descriptions;新增/编辑 Drawer(目录 / 菜单 / 按钮 / 外链等类型与路由、图标、权限标识)。",
"权限:`module_system:menu:create|delete|patch` + 行级 detail/update/delete。",
],
},
{
anchor: "page-dept",
title: "部门管理",
path: "module_system/dept/index.vue",
notes: [
"检索:FaSearchBar。",
"主区:树形表格(勾选);展开/收起。",
"工具栏:新增、批量删除、批量更多(patch)、刷新、列配置、搜索栏切换。",
"弹层:详情;新增/编辑 Dialog(名称、编码、上级、状态等)。",
"权限:`module_system:dept:create|delete|patch` + 行 detail/update/delete。",
],
},
{
anchor: "page-position",
title: "岗位管理",
path: "module_system/position/index.vue",
notes: [
"检索:FaSearchBar(含创建人 UserTableSelect 槽)。",
"主区:分页表格。",
"工具栏:新增、导出、批量删除、批量更多(patch)、刷新、列配置。",
"弹层:详情;新增/编辑 Dialog;导出 FaExportDialog。",
"权限:`module_system:position:create|export|delete|patch` + 行 detail/update/delete。",
],
},
{
anchor: "page-dict",
title: "字典管理",
path: "module_system/dict/index.vue",
notes: [
"检索:字典类型 FaSearchBar + 分页表。",
"工具栏:新增类型、导出类型、批量删除、批量更多(patch)、刷新、列配置。",
"类型弹层:详情 / 新增编辑 Dialog。",
"子层:`DataDrawer` 维护某字典类型下的字典数据项(二级)。",
"权限:`module_system:dict_type:*` 与字典数据接口权限(见行内按钮)。",
],
},
{
anchor: "page-param",
title: "参数配置",
path: "module_system/param/index.vue",
notes: [
"检索:FaSearchBar + 分页表。",
"工具栏:新增、导出、批量删除、刷新、列配置(无 patch 批量入口)。",
"弹层:详情;新增/编辑 Dialog;内置参数标识与表单校验。",
"权限:`module_system:param:create|export|delete` + 行级权限。",
],
},
{
anchor: "page-notice",
title: "通知公告",
path: "module_system/notice/index.vue",
notes: [
"检索:FaSearchBar(含创建人 UserTableSelect)。",
"工具栏:新增、导出、批量删除、批量更多(patch)、刷新、列配置。",
"弹层:详情;新增/编辑 Dialog;正文为富文本 `FaWangEditor`;HTML 预览。",
"权限:`module_system:notice:create|export|delete|patch` + 行级操作。",
],
},
{
anchor: "page-tenant",
title: "租户管理",
path: "module_system/tenant/index.vue",
notes: [
"检索:FaSearchBar + 分页表。",
"工具栏:新增、批量删除、刷新、列配置(无导出/更多 patch 于顶栏)。",
"弹层:详情;新增/编辑 Dialog(名称、编码、起止时间、状态、描述等)。",
"权限:`module_system:tenant:create|delete` + 行级 update/detail/delete。",
],
},
{
anchor: "page-log",
title: "操作日志",
path: "module_system/log/index.vue",
notes: [
"检索:FaSearchBar(含创建人 UserTableSelect);登录/操作日志类型在列或详情中区分。",
"工具栏:导出、批量删除、刷新、列配置(无新增)。",
"弹层:详情 Dialog(请求路径/方法、IP、状态码、耗时、入参出参等大字段)。",
"权限:`module_system:log:export|delete` + 行级查看/删除。",
],
},
{
anchor: "page-login",
title: "登录页",
path: "module_system/auth/login/index.vue",
notes: [
"面板切换:登录 / 注册 / 忘记密码(`setAuthPanel`)。",
"登录:账号表单;验证码;记住我;登录成功后 `redirect` 解析跳转。",
"静态路由:`/login`;与路由守卫白名单一致。",
"其他:注册与忘记密码提交接口;左侧背景与主题组件(视模板)。",
],
},
];
export const MANUAL_MODULES_AFTER_SYSTEM: ManualModuleSection[] = [
{
anchor: "mod-monitor",
heading: "二、监控管理",
pkgTag: "module_monitor",
pages: [
{
anchor: "page-online",
title: "在线用户",
path: "module_monitor/online/index.vue",
notes: [
"检索:FaSearchBar(IP、用户名、登录地点等)。",
"工具栏:「强退所有」(权限 `module_monitor:online:delete`);刷新、列配置、搜索栏。",
"行操作:单条会话「强退」。",
"主区:分页表格(session_id 等列);无批量勾选删除入口(仅有单条强退)。",
],
},
{
anchor: "page-cache",
title: "缓存管理",
path: "module_monitor/cache/index.vue",
notes: [
"Tab「监控信息」:Redis 监控指标 Descriptions(版本、内存、CPU、Key 数量等)。",
"Tab「缓存管理」:缓存列表/键值查看与按规则清理(与后端接口一致)。",
"完整性:两 Tab 均需点开核对是否加载成功、清理是否有二次确认。",
],
},
{
anchor: "page-resource",
title: "文件管理",
path: "module_monitor/resource/index.vue",
notes: [
"检索:FaSearchBar;路径面包屑导航进入子目录。",
"工具栏:上传、新建文件夹、下载、删除等(`module_monitor:resource:*` 按钮权限);刷新、列配置。",
"主区:文件表格;与后端资源浏览一致(菜单名「文件管理」,component `resource`)。",
],
},
{
anchor: "page-server",
title: "服务监控",
path: "module_monitor/server/index.vue",
notes: [
"布局:多卡片 Dashboard(CPU、内存、磁盘、JVM/服务器信息等,依接口返回)。",
"完整性:各卡片指标是否有数值;加载失败时占位或提示。",
],
},
],
},
{
anchor: "mod-task",
heading: "三、任务管理",
pkgTag: "module_task",
pages: [
{
anchor: "page-cronjob",
title: "调度器监控",
path: "module_task/cronjob/job/index.vue",
notes: [
"顶部状态区:调度器运行状态、任务数量;按钮:启动/暂停/恢复/关闭/清空任务/控制台/同步/刷新(各 `module_task:cronjob:job:*`)。",
"主区:任务卡片列表 — `getSchedulerJobs` 全量返回,名称与任务状态为前端筛选;单卡触发类型(Cron/间隔等)与暂停、恢复、调试、移除等行操作。",
"抽屉:当前任务的执行日志 — `useTable` + `getJobLogList` 分页,日志检索栏,批量删日志等(见页面)。",
],
},
{
anchor: "page-cronnode",
title: "节点管理",
path: "module_task/cronjob/node/index.vue",
notes: [
"检索:FaSearchBar + 分页表。",
"工具栏:新增、批量删除、刷新、列配置。",
"弹层:FaDialog + 分栏(表单 + 扩展配置区);编辑节点连接与执行参数。",
"权限:`module_task:cronjob:node:create|delete` + 行级 update/detail/delete。",
],
},
{
anchor: "page-workflow",
title: "流程编排",
path: "module_task/workflow/definition/index.vue",
notes: [
"检索:FaSearchBar(可展开)。",
"工具栏:新增、批量删除、刷新、列配置。",
"行操作:草稿「发布」;已发布「执行」下拉;「编辑」打开 `WorkflowDesignDrawer` 画布;删除。",
"权限:`module_task:workflow:definition:create|delete|update|execute` 等(见行内 v-hasPerm)。",
],
},
{
anchor: "page-nodetype",
title: "编排节点类型",
path: "module_task/workflow/node-type/index.vue",
notes: [
"检索:FaSearchBar(可展开)。",
"工具栏:新增、批量删除、刷新、列配置。",
"行操作:编辑(打开表单/脚本配置)、删除等(见 `node-type-operation` 槽)。",
"权限:`module_task:workflow:node-type:create|delete|update` + 行级操作。",
],
},
],
},
{
anchor: "mod-ai",
heading: "四、AI 模块",
pkgTag: "module_ai",
pages: [
{
anchor: "page-ai-chat",
title: "AI智能助手",
path: "module_ai/chat/index.vue",
notes: [
"布局:左侧 `Sidebar`(会话列表、新建会话);右侧 `ChatNavbar` + `ChatMessages` + `ChatInput`。",
"连接:`toggleConnection`、连接状态展示;清空对话、侧栏折叠。",
"完整性:发消息、收消息列表滚动、错误条展示与关闭;流式与否依赖后端/WebSocket 对接。",
],
},
{
anchor: "page-ai-fachat",
title: "会话聊天",
path: "module_ai/fachat/index.vue",
notes: [
"布局:左侧联系人列表(搜索、排序下拉、在线点);右侧头部「Art Bot」状态与语音/视频等图标按钮;中部消息气泡列表;底部输入框与发送。",
"完整性:选人切换会话、发送消息、滚动消息区;当前多为演示数据与本地发送逻辑。",
],
},
{
anchor: "page-ai-memory",
title: "会话记忆",
path: "module_ai/memory/index.vue",
notes: [
"检索:FaSearchBar + 分页表。",
"工具栏:新增、批量删除、刷新、列配置;权限锚点 `module_ai:chat:create|delete`(与后端标识一致)。",
"弹层:详情与表单 Drawer(会话记忆字段);完整性:CRUD 与分页均需验证。",
],
},
],
},
{
anchor: "mod-generator",
heading: "五、代码生成器",
pkgTag: "module_generator",
pages: [
{
anchor: "page-gencode",
title: "代码生成",
path: "module_generator/gencode/index.vue",
notes: [
"检索:FaSearchBar + 库表分页列表;勾选表格行。",
"工具栏:创建表、导入库表、批量删除、批量生成(`module_generator:gencode:*`)。",
"弹层:`CreateTableDialog`、导入 DB、`GenCodeDrawer` 多步(导入字段、生成配置、预览代码)。",
"完整性:预览、下载 zip、生成到磁盘等按钮是否随权限显示且可点。",
],
},
],
},
{
anchor: "mod-app",
heading: "六、应用管理",
pkgTag: "module_application",
pages: [
{
anchor: "page-portal",
title: "插件市场",
path: "module_application/portal/index.vue",
notes: [
"检索:FaSearchBar(含创建人、更新人 UserTableSelect)。",
"工具栏:标题「应用市场」+「创建应用」(`module_application:portal:create`);刷新、搜索栏。",
"主区:卡片网格(图标、名称、状态、描述);卡片脚编辑/删除(update/delete);底部分页器。",
"完整性:空列表 ElEmpty;卡片点击进入内部打开逻辑(见 `openAppInternal`)。",
],
},
],
},
{
anchor: "mod-example",
heading: "七、示例模块",
pkgTag: "module_example",
pages: [
{
anchor: "page-demo",
title: "示例管理",
path: "module_example/demo/index.vue",
notes: [
"标准列表 CRUD 示例页:检索、分页表、工具栏、行操作、弹窗表单(与本项目其它模块同一套 Art 组件)。",
"完整性:按页面可见按钮与表单字段逐项点验即可。",
],
},
{
anchor: "page-demo01",
title: "三级菜单",
path: "module_example/demo01/index.vue",
notes: [
"占位或简单展示的叶子页,用于验证多级菜单路由与 KeepAlive。",
"完整性:侧栏进入后路由正确、页面无报错即可。",
],
},
],
},
{
anchor: "mod-dashboard",
heading: "八、仪表盘",
pkgTag: "dashboard",
pages: [
{
anchor: "page-home",
title: "首页",
path: "dashboard/index.vue",
notes: [
"路由 `/home`:样式类 `workplace-page` — 问候与用户信息卡片、模块入口网格(按权限灰显)、其它工作台分区(与文件内组件一致)。",
"完整性:模块卡片点击跳转、无权限入口禁用或提示;接口失败时有容错。",
],
},
{
anchor: "page-profile",
title: "个人中心",
path: "current/profile.vue",
notes: [
"路由 `/profile`;左侧头像上传(ElUpload)、资料展示;右侧「基本设置」表单与「更改密码」三块。",
"完整性:保存基本信息、修改密码、头像上传回调与校验均已接线。",
],
},
{
anchor: "page-changelog",
title: "更新日志",
path: "changelog/index.vue",
notes: [
"静态路由 `/changelog`;列表遍历 mock `upgradeLogList`(版本、日期、明细、备注、需重登标签)。",
"另有 `module_system/changelog` 视图未挂路由;完整性以本页为准。",
],
},
{
anchor: "page-db-workplace",
title: "工作台",
path: "dashboard/workplace/index.vue",
notes: [
"仪表盘子路由 `dashboard/workplace`:独立 `dashboard-container` 布局(含 Github 角标、问候区、天气文案、统计卡片与图表等,以页面为准)。",
"与 `/home` 所用 `dashboard/index.vue` 不是同一文件;完整性需单独打开该路由逐项看区块是否渲染。",
],
},
{
anchor: "page-db-console",
title: "控制台",
path: "dashboard/console/index.vue",
notes: [
"控制台仪表盘分区(卡片/图表);meta.fixedTab 等影响标签钉扎行为。",
"完整性:各区块渲染与路由切换无报错。",
],
},
{
anchor: "page-db-analysis",
title: "分析页",
path: "dashboard/analysis/index.vue",
notes: ["图表与卡片布局演示页;完整性:各图表组件 mount 成功、无控制台报错。"],
},
{
anchor: "page-db-ecommerce",
title: "电子商务",
path: "dashboard/ecommerce/index.vue",
notes: ["电商风格区块演示;完整性:列表/卡片区块均可滚动与展示。"],
},
{
anchor: "page-db-map",
title: "地图",
path: "dashboard/map/index.vue",
notes: ["地图 SDK 示例;完整性:地图实例初始化、标记与交互事件可用。"],
},
{
anchor: "page-db-pricing",
title: "定价",
path: "dashboard/pricing/index.vue",
notes: ["定价页模板;完整性:价格表、按钮或跳转链接展示齐全。"],
},
{
anchor: "page-db-article",
title: "文章管理",
path: "dashboard/article/list/index.vue",
notes: [
"文章列表(当前多为 mock `ArticleList`);路由 path `dashboard/article/article-list`。",
"meta.authList:新增/编辑类权限标记;完整性:列表、分页、抽屉或编辑入口覆盖文章 CRUD 流程。",
],
},
{
anchor: "page-db-tutorial",
title: "教程",
path: "dashboard/tutorial/index.vue",
notes: [
"视频 Tab + 功能验收 Tab;正文含用户/角色详述与其余模块完整性清单。",
"完整性:目录筛选、锚点滚动、手册内链跳转可用。",
],
},
],
},
{
anchor: "mod-layout",
heading: "九、布局与通用功能",
pkgTag: "layouts",
pages: [
{
anchor: "layout-main",
title: "主布局",
path: "../layouts/index.vue",
notes: [
"RootLayout:嵌套 RouterView;组合侧栏、顶栏、工作区、设置抽屉等子布局组件。",
"完整性:登录后进主壳、子路由 outlet 渲染、响应式收缩侧栏。",
],
},
{
anchor: "layout-sidebar",
title: "侧栏菜单",
path: "../layouts/art-menus/**",
notes: ["菜单折叠、搜索、递归子菜单;与路由 meta.icon/title 绑定。"],
},
{
anchor: "layout-header",
title: "顶栏",
path: "../layouts/art-header-bar/**",
notes: ["面包屑、快速入口、全屏、通知铃铛区域。"],
},
{
anchor: "layout-worktab",
title: "标签页",
path: "../layouts/art-work-tab/**",
notes: ["多标签打开、固定钉住、关闭其他;与 keep-alive 协同。"],
},
{
anchor: "layout-settings",
title: "设置面板",
path: "../layouts/art-settings-panel/**",
notes: ["主题、色弱、标签栏、菜单布局等本地偏好持久化。"],
},
{
anchor: "layout-notification",
title: "通知",
path: "../layouts/art-notification/**",
notes: ["通知下拉列表;已读状态;路由跳转。"],
},
{
anchor: "layout-search",
title: "全局搜索",
path: "../layouts/art-global-search/**",
notes: ["Ctrl/⌘+K 打开全局搜索(`art-global-search` 监听 keydown)。"],
},
{
anchor: "layout-lock",
title: "锁屏",
path: "../layouts/art-screen-lock/index.vue",
notes: [
"锁屏全屏页;背景随明暗主题切换(`@imgs/lock` → `src/assets/images/lock`);顶栏用户菜单进入。",
],
},
{
anchor: "layout-user",
title: "用户菜单",
path: "../layouts/art-header-bar/**",
notes: ["头像下拉:文档、教程、锁屏、退出等。"],
},
{
anchor: "layout-theme",
title: "主题切换",
path: "../layouts/art-settings-panel/**",
notes: ["明暗色、主题色;CSS 变量与 Element Plus 深色。"],
},
{
anchor: "layout-lang",
title: "语言切换",
path: "../locales/langs/**",
notes: ["`src/locales/langs/*.json` 与 i18n 实例;顶栏/设置内切换语言并持久化到本地存储。"],
},
],
},
{
anchor: "mod-exception",
heading: "十、异常页",
pkgTag: "exception",
pages: [
{
anchor: "page-401",
title: "401",
path: "exception/401/index.vue",
notes: [
"静态路由 `/401`;页面含返回/登录入口(见模板)。",
"完整性:守卫或 axios 拦截跳转至此页时按钮可用。",
],
},
{
anchor: "page-403",
title: "403",
path: "exception/403/index.vue",
notes: [
"静态路由 `/403`;无权限提示与返回首页等操作。",
"完整性:从路由 meta 或业务手动跳转后页面可退出。",
],
},
{
anchor: "page-404",
title: "404",
path: "exception/404/index.vue",
notes: [
"独立 `/404` 与 CatchAll `CatchAll404` 共用组件文件(路由 name 不同)。",
"完整性:非法路径落入 CatchAll、手动跳转 `/404` 均能展示。",
],
},
{
anchor: "page-500",
title: "500",
path: "exception/500/index.vue",
notes: [
"静态路由 `/500`;服务端错误提示页。",
"完整性:错误文案、返回或重试按钮(若有)可点击。",
],
},
],
},
{
anchor: "mod-swagger",
heading: "十一、接口文档(API)",
pkgTag: "module_swagger",
pages: [
{
anchor: "page-swagger",
title: "Swagger文档",
path: "module_swagger/docs/index.vue",
notes: [
"父级菜单「接口管理」;默认 iframe `VITE_APP_BASE_API + /docs`,可由 `IframeRouteManager` 覆盖 link。",
"完整性:iframe 加载、loading 消失;需后端文档地址可达。",
],
},
{
anchor: "page-redoc",
title: "Redoc文档",
path: "module_swagger/redoc/index.vue",
notes: [
"Redoc 文档 iframe 页;链接来自 env 或 iframe 路由 meta(与同模块 docs 一致范式)。",
"完整性:页面全高 iframe、加载完成。",
],
},
{
anchor: "page-ljdoc",
title: "LangJin文档",
path: "module_swagger/ljdoc/index.vue",
notes: [
"LangJin 文档 iframe;sys_menu 中节点类型可为 4(菜单页)。",
"完整性:同上,依赖后端或 meta 配置的文档 URL。",
],
},
],
},
];
@@ -1,63 +0,0 @@
/**
* 功能验收手册 — 侧栏「筛选目录」扩展词
* 含与 sys_menu 对齐前的俗称、简称,避免改名后搜不到。
*/
/** 模块级补充检索词 */
export const MANUAL_MODULE_SEARCH_ALIASES: Readonly<Record<string, readonly string[]>> = {
"mod-system": ["平台管理", "系统管理", "用户", "权限"],
"mod-monitor": ["监控"],
"mod-task": ["任务", "调度", "工作流", "cron"],
"mod-ai": ["AI", "智能", "会话", "记忆"],
"mod-generator": ["代码生成", "生成器"],
"mod-app": ["应用", "门户", "插件"],
"mod-example": ["示例", "案例", "demo"],
"mod-dashboard": ["仪表盘", "首页", "工作台", "教程"],
"mod-layout": ["布局", "顶栏", "侧栏", "主题", "锁屏"],
"mod-exception": ["错误页", "404", "403"],
"mod-swagger": ["接口", "API", "文档", "swagger", "redoc"],
};
/** 页面级补充检索词(anchor 与 MANUAL_TOC 一致) */
export const MANUAL_PAGE_SEARCH_ALIASES: Readonly<Record<string, readonly string[]>> = {
"page-resource": ["资源监控", "文件", "上传"],
"page-cronjob": ["定时任务", "调度器", "job", "cron"],
"page-cronnode": ["任务节点", "节点"],
"page-workflow": ["工作流", "流程", "编排"],
"page-nodetype": ["节点类型", "编排"],
"page-ai-chat": ["AI 聊天", "聊天", "chat"],
"page-ai-fachat": ["FA 聊天", "fachat"],
"page-ai-memory": ["记忆管理", "memory"],
"page-portal": ["应用门户", "portal"],
"page-demo": ["示例一", "demo"],
"page-demo01": ["示例二", "demo01", "三级"],
"page-swagger": ["Swagger", "swagger"],
"page-redoc": ["Redoc", "redoc"],
"page-ljdoc": ["LJDoc", "ljdoc", "LangJin"],
"page-db-article": ["文章", "article"],
"page-db-tutorial": ["操作手册", "手册", "教程"],
"page-login": ["登录", "auth"],
"page-changelog": ["日志", "版本", "release"],
"page-home": ["home", "入口"],
"page-profile": ["个人中心", "profile", "账号"],
};
function includesLoose(text: string, q: string): boolean {
return text.toLowerCase().includes(q.toLowerCase());
}
/** 模块是否在筛选下保留(标题、别名或任一子页命中) */
export function manualModuleMatchesQuery(modTitle: string, modAnchor: string, q: string): boolean {
if (includesLoose(modTitle, q)) return true;
const ma = MANUAL_MODULE_SEARCH_ALIASES[modAnchor];
if (ma?.some((a) => includesLoose(a, q))) return true;
return false;
}
/** 子页是否在筛选下保留 */
export function manualPageMatchesQuery(pageTitle: string, pageAnchor: string, q: string): boolean {
if (includesLoose(pageTitle, q)) return true;
const pa = MANUAL_PAGE_SEARCH_ALIASES[pageAnchor];
if (pa?.some((a) => includesLoose(a, q))) return true;
return false;
}
+490 -597
View File
File diff suppressed because it is too large Load Diff