Lufaneng 11 months ago
parent 12f80d3310
commit 3c7863a90a

@ -22,6 +22,7 @@ declare module 'vue' {
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMain: typeof import('element-plus/es')['ElMain']
@ -35,6 +36,7 @@ declare module 'vue' {
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElTree: typeof import('element-plus/es')['ElTree']
MapSelector: typeof import('./components/MapSelector.vue')['default']
Pagination: typeof import('./components/Pagination/index.vue')['default']

@ -18,6 +18,12 @@ export const constantRoutes = [
// 动态路由配置(需要权限)
export const asyncRoutes = [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard/index.vue'),
meta: { title: '仪表盘', permissions: ['dashboard'] },
},
{
path: 'SystemManage',
meta: { title: '系统管理', permissions: ['system'] },

@ -34,3 +34,23 @@ body {
.pointer {
cursor: pointer;
}
*::-webkit-scrollbar {
width: 12px;
height: 12px;
}
*::-webkit-scrollbar-button {
width: 0px;
height: 0px;
display: none;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
height: 6px;
border-radius: 25px;
background-clip: padding-box;
background-color: rgba(0, 0, 0, 0.3);
}

@ -46,7 +46,6 @@ instance.interceptors.request.use(
// HTTP response拦截
instance.interceptors.response.use(
(response) => {
console.log(response)
if (response.data.error.code === 200) {
return Promise.resolve(response.data.result)
} else if (response.data.error.code === 401) {

@ -0,0 +1,128 @@
<template>
<div class="order-type-chart">
<div class="chart-header">
<h3>订单类型分布</h3>
</div>
<v-chart class="chart" :option="chartOption" autoresize />
<!-- <div class="legend">
<div class="legend-item" v-for="item in legendData" :key="item.name">
<span class="legend-color" :style="{ backgroundColor: item.color }"></span>
<span class="legend-text">{{ item.name }}</span>
</div>
</div> -->
</div>
</template>
<script setup>
import { ref } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import RPC from '@/utils/rpc'
use([CanvasRenderer, PieChart, TitleComponent, TooltipComponent, LegendComponent])
const legendData = ref([
{ name: '充值', color: '#409eff' },
{ name: '包月', color: '#67c23a' },
{ name: '包季', color: '#e6a23c' },
{ name: '包年', color: '#f56c6c' },
])
RPC.post('/dashboard/order_type_distribution').then((res) => {
console.log('order_type_distribution', res)
chartData.value = res.items.map((item) => {
return {
name: item.type,
value: item.value_cents / 100,
}
})
})
const chartData = ref([
// { value: 35, name: '', itemStyle: { color: '#409eff' } },
// { value: 25, name: '', itemStyle: { color: '#67c23a' } },
// { value: 25, name: '', itemStyle: { color: '#e6a23c' } },
// { value: 15, name: '', itemStyle: { color: '#f56c6c' } },
])
const chartOption = ref({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c}% ({d}%)',
},
legend: {
bottom: '0%',
left: 'center',
},
series: [
{
name: '订单类型',
type: 'pie',
radius: ['50%', '80%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
label: {
show: false,
},
emphasis: {
label: {
show: false,
},
},
labelLine: {
show: false,
},
data: chartData,
},
],
})
</script>
<style lang="scss" scoped>
.order-type-chart {
padding: 20px;
.chart-header {
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.chart {
height: 300px;
width: 100%;
}
.legend {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 20px;
.legend-item {
display: flex;
align-items: center;
gap: 8px;
.legend-color {
width: 12px;
height: 12px;
border-radius: 50%;
}
.legend-text {
font-size: 14px;
color: #606266;
}
}
}
}
</style>

@ -0,0 +1,145 @@
<template>
<div class="recent-drawings">
<div class="card-header">
<h3>最近图纸生成</h3>
<el-button type="primary" size="small" plain @click="goAll"></el-button>
</div>
<div class="drawings-list">
<div v-for="item in drawingData" :key="item.id" class="drawing-item">
<div class="drawing-info">
<div class="drawing-title">{{ item.file_name }}</div>
<div class="drawing-details">
<span class="drawing-id">生成编号{{ item.order_no }}</span>
<span class="drawing-user">用户{{ item.username }}({{ item.user_id }})</span>
</div>
<div class="drawing-time">
生成时间{{
item.CreatedAt ? dayjs(item.CreatedAt).format('YYYY-MM-DD HH:mm:ss') : ''
}}
</div>
</div>
<div class="drawing-status">
<el-tag :type="getStatusTagType(item.status)" size="small">
<template v-if="item.status == 0"></template>
<template v-if="item.status == 1"></template>
<template v-if="item.status == 2"></template>
<template v-if="item.status == 3">()</template>
<template v-if="item.status == 4"></template>
<template v-if="item.status == 5"></template>
</el-tag>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import RPC from '@/utils/rpc'
import dayjs from 'dayjs'
const router = useRouter()
const goAll = () => router.push('/GenerateRecord')
const loading = ref(false)
const getList = () => {
loading.value = true
RPC.post('/planned_list', { page: 1, page_size: 10 })
.then((res) => {
drawingData.value = res.list || []
})
.finally(() => {
loading.value = false
})
}
getList()
const drawingData = ref([])
const getStatusTagType = (status) => {
// const statusMap = {
// : 'success',
// : 'warning',
// : 'danger',
// : 'info',
// }
const statusMap = ['info', 'info', 'info', 'warning', 'success', 'danger']
return statusMap[status] || ''
}
</script>
<style lang="scss" scoped>
.recent-drawings {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.drawings-list {
height: 280px;
overflow-y: auto;
.drawing-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f2f5;
&:last-child {
border-bottom: none;
}
&:hover {
background-color: #f8f9fa;
margin: 0 -12px;
padding: 16px 12px;
border-radius: 6px;
}
.drawing-info {
flex: 1;
.drawing-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 6px;
}
.drawing-details {
display: flex;
gap: 16px;
margin-bottom: 4px;
.drawing-id,
.drawing-user {
font-size: 12px;
color: #909399;
}
}
.drawing-time {
font-size: 12px;
color: #c0c4cc;
}
}
.drawing-status {
flex-shrink: 0;
}
}
}
}
</style>

@ -0,0 +1,120 @@
<template>
<div class="recent-orders">
<div class="card-header">
<h3>最近订单</h3>
<el-button type="primary" size="small" plain @click="goAll"></el-button>
</div>
<div class="table-container">
<el-table :data="orderData" style="width: 100%" size="small" height="270">
<el-table-column align="center" prop="order_no" label="订单编号" show-overflow-tooltip />
<el-table-column align="center" prop="user_name" label="用户" show-overflow-tooltip />
<el-table-column align="center" prop="amount" label="金额" width="100">
<template #default="scope">
<span class="amount">{{ scope.row.amount }}</span>
</template>
</el-table-column>
<el-table-column align="center" prop="order_type" label="类型" width="100">
</el-table-column>
<el-table-column align="center" prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag v-if="row.status === 'created'" type="warning" size="small"></el-tag>
<el-tag v-if="row.status === 'pending'" type="warning" size="small"></el-tag>
<el-tag v-if="row.status === 'paid'" type="success" size="small"></el-tag>
<el-tag v-if="row.status === 'failed'" type="danger" size="small"></el-tag>
<el-tag v-if="row.status === 'canceled'" type="info" size="small"></el-tag>
<el-tag v-if="row.status === 'refunded'" type="info" size="small">退</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import RPC from '@/utils/rpc'
import dayjs from 'dayjs'
const router = useRouter()
const goAll = () => router.push('/OrderManage')
const orderData = ref([])
const loading = ref(false)
const getList = () => {
loading.value = true
RPC.post('/membership/order/list', { page: 1, size: 10 })
.then((res) => {
orderData.value = res.list
})
.finally(() => {
loading.value = false
})
}
getList()
const getTypeTagType = (type) => {
const typeMap = {
包年: '',
包季: 'success',
充值: 'info',
包月: 'warning',
}
return typeMap[type] || ''
}
const getStatusTagType = (status) => {
const statusMap = {
已支付: 'success',
待支付: 'warning',
已取消: 'danger',
}
return statusMap[status] || ''
}
</script>
<style lang="scss" scoped>
.recent-orders {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 100%;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.table-container {
.amount {
font-weight: 600;
color: #303133;
}
}
}
:deep(.el-table) {
.el-table__header {
th {
background-color: #fafafa;
color: #606266;
font-weight: 500;
}
}
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
</style>

@ -0,0 +1,181 @@
<template>
<div class="revenue-chart">
<div class="chart-header">
<h3>收入趋势</h3>
<div class="chart-tabs">
<el-button
size="small"
:class="{ active: activeTab === 'month' }"
@click="switchTab('month')"
>
月度
</el-button>
<el-button
size="small"
:class="{ active: activeTab === 'quarter' }"
@click="switchTab('quarter')"
>
季度
</el-button>
<el-button
size="small"
:class="{ active: activeTab === 'year' }"
@click="switchTab('year')"
>
年度
</el-button>
</div>
</div>
<v-chart class="chart" :option="chartOption" autoresize />
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
} from 'echarts/components'
import VChart from 'vue-echarts'
import RPC from '@/utils/rpc'
use([CanvasRenderer, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
const activeTab = ref('month')
const switchTab = (tab) => {
activeTab.value = tab
console.log(activeTab.value)
getData()
}
const data = ref([])
const getData = () => {
RPC.post('/dashboard/revenue_trend', { granularity: activeTab.value }).then((res) => {
console.log(activeTab.value, res)
data.value = res.items || []
})
}
getData()
const chartOption = computed(() => {
return {
tooltip: {
trigger: 'axis',
formatter: '{b}: ¥{c}',
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: data.value.map((item) => item.label),
axisLine: {
lineStyle: {
color: '#e4e7ed',
},
},
axisLabel: {
color: '#606266',
},
},
yAxis: {
type: 'value',
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: '#606266',
formatter: '{value}',
},
splitLine: {
lineStyle: {
color: '#f5f7fa',
},
},
},
series: [
{
type: 'line',
data: data.value.map((item) => item.value_cents / 100),
smooth: true,
lineStyle: {
color: '#409eff',
width: 3,
},
itemStyle: {
color: '#409eff',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(64, 158, 255, 0.3)',
},
{
offset: 1,
color: 'rgba(64, 158, 255, 0.1)',
},
],
},
},
},
],
}
})
</script>
<style lang="scss" scoped>
.revenue-chart {
padding: 20px;
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.chart-tabs {
.el-button {
margin-left: 8px;
&.active {
background-color: #157ee8;
border-color: #177de3;
color: white;
}
}
}
}
.chart {
height: 300px;
width: 100%;
}
}
</style>

@ -0,0 +1,269 @@
<template>
<div class="dashboard">
<!-- 统计卡片 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-label">总用户数</div>
<div class="stat-value">{{ data.total_users }}</div>
<div
class="stat-trend"
:class="{
positive: data.total_users_mom_ratio?.includes('+'),
negative:
data.total_users_mom_ratio?.includes('-') &&
data.total_users_mom_ratio.length > 1,
}"
>
<el-icon v-if="total_users_mom_delta > 0"><ArrowUp /></el-icon>
<el-icon v-if="total_users_mom_delta < 0"><ArrowDown /></el-icon>
{{ data.total_users_mom_ratio }} 较上月
</div>
</div>
<div class="stat-icon user">
<el-icon><User /></el-icon>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-label">今日活跃</div>
<div class="stat-value">{{ data.active_today }}</div>
<div
class="stat-trend"
:class="{
positive: data.active_today_dod_ratio?.includes('+'),
negative:
data.active_today_dod_ratio?.includes('-') &&
data.active_today_dod_ratio.length > 1,
}"
>
<el-icon><ArrowDown /></el-icon>
{{ data.active_today_dod_ratio }} 较昨日
</div>
</div>
<div class="stat-icon active">
<el-icon><TrendCharts /></el-icon>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-label">商品生成量</div>
<div class="stat-value">{{ data.drawings_generated }}</div>
<div
class="stat-trend"
:class="{
positive: data.drawings_generated_mom_ratio?.includes('+'),
negative:
data.drawings_generated_mom_ratio?.includes('-') &&
data.drawings_generated_mom_ratio?.length > 1,
}"
>
<el-icon><ArrowUp /></el-icon>
{{ data.drawings_generated_mom_ratio }} 较上月
</div>
</div>
<div class="stat-icon generate">
<el-icon><Document /></el-icon>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-label">本月收入</div>
<div class="stat-value">¥{{ data.month_income_cents / 100 }}</div>
<div
class="stat-trend"
:class="{
positive: data.month_income_cents_mom_ratio?.includes('+'),
negative:
data.month_income_cents_mom_ratio?.includes('-') &&
data.month_income_cents_mom_ratio?.length > 1,
}"
>
<el-icon><ArrowUp /></el-icon>
{{ data.month_income_cents_mom_ratio }} 较上月
</div>
</div>
<div class="stat-icon revenue">
<el-icon><Money /></el-icon>
</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="charts-section">
<div class="chart-container">
<RevenueChart />
</div>
<div class="chart-container">
<OrderTypeChart />
</div>
</div>
<!-- 最近数据区域 -->
<div class="recent-section">
<div class="recent-container">
<RecentOrders />
</div>
<div class="recent-container">
<RecentDrawings />
</div>
</div>
</div>
</template>
<script setup>
import { ArrowUp, ArrowDown, User, TrendCharts, Document, Money } from '@element-plus/icons-vue'
import RevenueChart from './RevenueChart.vue'
import OrderTypeChart from './OrderTypeChart.vue'
import RecentOrders from './RecentOrders.vue'
import RecentDrawings from './RecentDrawings.vue'
import RPC from '@/utils/rpc'
const data = ref({})
const getData = () => {
RPC.post('/dashboard/summary').then((res) => {
data.value = res
})
}
getData()
</script>
<style lang="scss" scoped>
.dashboard {
padding: 20px;
// background-color: #f5f7fa;
min-height: 100vh;
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
.stat-card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
&:hover {
transform: translateY(-2px);
}
.stat-content {
display: flex;
justify-content: space-between;
align-items: center;
.stat-info {
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.stat-trend {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
&.positive {
color: #67c23a;
}
&.negative {
color: #f56c6c;
}
.el-icon {
font-size: 12px;
}
}
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
.el-icon {
font-size: 24px;
color: white;
}
&.user {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.active {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
&.generate {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
&.revenue {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
}
}
}
}
.charts-section {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 30px;
@media (max-width: 1200px) {
grid-template-columns: 1fr;
}
.chart-container {
min-height: 400px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
}
.recent-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
@media (max-width: 1200px) {
grid-template-columns: 1fr;
}
.recent-container {
height: 300px;
}
}
}
</style>

@ -35,6 +35,14 @@
header-row-class-name="table-header"
>
<el-table-column align="center" label="文件名" prop="file_name" show-overflow-tooltip />
<el-table-column
align="center"
label="用户名"
prop="username"
width="100"
show-overflow-tooltip
/>
<el-table-column align="center" label="用户ID" prop="user_id" width="100" />
<el-table-column align="center" label="状态" prop="status" width="100">
<template #default="{ row }">
<span v-if="row.status == 0"></span>

Loading…
Cancel
Save