mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 12:49:30 +00:00
refactor: 重构路由与部署脚本,更新静态资源路径
This commit is contained in:
@@ -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>
|
||||
+669
-246
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
Regular → Executable
+490
-597
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user