管理后台
This commit is contained in:
parent
741b4457bd
commit
7fb22198c7
4
admin/components.d.ts
vendored
4
admin/components.d.ts
vendored
|
|
@ -10,6 +10,9 @@ declare module 'vue' {
|
|||
export interface GlobalComponents {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
|
|
@ -36,6 +39,7 @@ declare module 'vue' {
|
|||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
|
|
|
|||
26
admin/package-lock.json
generated
26
admin/package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
|||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.9.8",
|
||||
"pinia": "^3.0.2",
|
||||
"sortablejs": "^1.15.7",
|
||||
|
|
@ -1829,6 +1830,16 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
|
||||
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.13.6",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.6.tgz",
|
||||
|
|
@ -2757,6 +2768,12 @@
|
|||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
|
|
@ -3130,6 +3147,15 @@
|
|||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
|
||||
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.9.8",
|
||||
"pinia": "^3.0.2",
|
||||
"sortablejs": "^1.15.7",
|
||||
|
|
|
|||
6
admin/src/api/auth.ts
Normal file
6
admin/src/api/auth.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 管理员认证 API
|
||||
export function login(data: { username: string; password: string }) {
|
||||
return request.post('/auth/login', data)
|
||||
}
|
||||
14
admin/src/api/dashboard.ts
Normal file
14
admin/src/api/dashboard.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 仪表盘 API
|
||||
export function getDashboardStats() {
|
||||
return request.get('/dashboard/stats')
|
||||
}
|
||||
|
||||
export function getUserTrend() {
|
||||
return request.get('/dashboard/user-trend')
|
||||
}
|
||||
|
||||
export function getCouponDistribution() {
|
||||
return request.get('/dashboard/coupon-distribution')
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
// 管理后台 API 模块统一导出
|
||||
export * from './auth'
|
||||
export * from './banner'
|
||||
export * from './coupon'
|
||||
export * from './content'
|
||||
export * from './dashboard'
|
||||
export * from './entry'
|
||||
export * from './membership'
|
||||
export * from './points'
|
||||
|
|
|
|||
|
|
@ -1,43 +1,59 @@
|
|||
<template>
|
||||
<el-container class="admin-layout">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :width="isCollapse ? '64px' : '220px'" class="aside">
|
||||
<el-aside :width="isCollapse ? '64px' : '240px'" class="aside">
|
||||
<div class="logo">
|
||||
<span v-if="!isCollapse">贩卖机管理后台</span>
|
||||
<span v-else>VM</span>
|
||||
<img src="/favicon.svg" alt="logo" class="logo-img" />
|
||||
<span v-if="!isCollapse" class="logo-text">贩卖机管理后台</span>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="currentRoute"
|
||||
:collapse="isCollapse"
|
||||
router
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409eff"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.path"
|
||||
:index="item.path"
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:default-active="currentRoute"
|
||||
:collapse="isCollapse"
|
||||
router
|
||||
background-color="#1d1e1f"
|
||||
text-color="rgba(255,255,255,0.65)"
|
||||
active-text-color="#ffffff"
|
||||
:collapse-transition="false"
|
||||
class="side-menu"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<template #title>{{ item.title }}</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<el-menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.path"
|
||||
:index="item.path"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<template #title>{{ item.title }}</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<el-container class="main-container">
|
||||
<!-- 顶部栏 -->
|
||||
<el-header class="header">
|
||||
<el-icon class="collapse-btn" @click="isCollapse = !isCollapse">
|
||||
<Fold v-if="!isCollapse" />
|
||||
<Expand v-else />
|
||||
</el-icon>
|
||||
<div class="header-left">
|
||||
<el-icon class="collapse-btn" @click="isCollapse = !isCollapse">
|
||||
<Fold v-if="!isCollapse" />
|
||||
<Expand v-else />
|
||||
</el-icon>
|
||||
<el-breadcrumb separator="/" class="breadcrumb">
|
||||
<el-breadcrumb-item :to="{ path: '/dashboard' }">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-if="currentTitle">{{ currentTitle }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="admin-user">管理员</span>
|
||||
<el-dropdown @command="handleCommand" trigger="click">
|
||||
<span class="admin-user">
|
||||
<el-avatar :size="28" style="background:#409eff;margin-right:8px;">管</el-avatar>
|
||||
管理员
|
||||
<el-icon style="margin-left:4px"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
<el-dropdown-item command="logout">
|
||||
<el-icon><SwitchButton /></el-icon>退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
|
@ -45,7 +61,7 @@
|
|||
</el-header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-main>
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
|
@ -55,7 +71,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Fold, Expand } from '@element-plus/icons-vue'
|
||||
import { Fold, Expand, ArrowDown, SwitchButton } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
|
||||
const route = useRoute()
|
||||
|
|
@ -64,6 +80,10 @@ const authStore = useAuthStore()
|
|||
const isCollapse = ref(false)
|
||||
|
||||
const currentRoute = computed(() => route.path)
|
||||
const currentTitle = computed(() => {
|
||||
const item = menuItems.find(m => m.path === route.path)
|
||||
return item?.title || ''
|
||||
})
|
||||
|
||||
// 侧边栏菜单项
|
||||
const menuItems = [
|
||||
|
|
@ -85,3 +105,118 @@ function handleCommand(command: string) {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.aside {
|
||||
background: #1d1e1f;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.side-menu .el-menu-item {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
margin: 2px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.side-menu .el-menu-item.is-active {
|
||||
background: #409eff !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.side-menu .el-menu-item:hover:not(.is-active) {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
height: 56px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.admin-user:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,37 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2>仪表盘</h2>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<template #header>用户总数</template>
|
||||
<div class="stat-value">--</div>
|
||||
<div class="dashboard">
|
||||
<h2 class="page-title">仪表盘</h2>
|
||||
<p class="page-desc">欢迎回来,管理员。以下是系统概览数据。</p>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :xs="12" :sm="6" v-for="item in statCards" :key="item.label">
|
||||
<div class="stat-card" :style="{ borderTop: `3px solid ${item.color}` }">
|
||||
<div class="stat-header">
|
||||
<span class="stat-label">{{ item.label }}</span>
|
||||
</div>
|
||||
<div class="stat-value" :style="{ color: item.color }">{{ item.value }}</div>
|
||||
<div class="stat-footer">{{ item.desc }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="16" class="chart-row">
|
||||
<el-col :xs="24" :sm="14">
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<span class="section-title">近7天用户增长趋势</span>
|
||||
</template>
|
||||
<div ref="lineChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<template #header>会员总数</template>
|
||||
<div class="stat-value">--</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<template #header>今日积分发放</template>
|
||||
<div class="stat-value">--</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<template #header>优惠券兑换次数</template>
|
||||
<div class="stat-value">--</div>
|
||||
<el-col :xs="24" :sm="10">
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<span class="section-title">优惠券类型分布</span>
|
||||
</template>
|
||||
<div ref="pieChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
|
@ -31,14 +39,167 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// TODO: 对接后端统计接口
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { getDashboardStats, getUserTrend, getCouponDistribution } from '@/api/dashboard'
|
||||
|
||||
const lineChartRef = ref<HTMLElement>()
|
||||
const pieChartRef = ref<HTMLElement>()
|
||||
let lineChart: echarts.ECharts | null = null
|
||||
let pieChart: echarts.ECharts | null = null
|
||||
|
||||
const statCards = ref([
|
||||
{ label: '用户总数', value: '--', icon: 'User', color: '#409eff', bg: '#ecf5ff', desc: '注册用户数量' },
|
||||
{ label: '会员总数', value: '--', icon: 'UserFilled', color: '#67c23a', bg: '#f0f9eb', desc: '活跃会员数量' },
|
||||
{ label: '今日积分发放', value: '--', icon: 'Coin', color: '#e6a23c', bg: '#fdf6ec', desc: '今日新增积分' },
|
||||
{ label: '优惠券兑换', value: '--', icon: 'Ticket', color: '#f56c6c', bg: '#fef0f0', desc: '累计兑换次数' },
|
||||
])
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
// 加载统计数据
|
||||
try {
|
||||
const res: any = await getDashboardStats()
|
||||
if (res?.data) {
|
||||
statCards.value[0].value = String(res.data.totalUsers)
|
||||
statCards.value[1].value = String(res.data.totalMembers)
|
||||
statCards.value[2].value = String(res.data.todayPoints)
|
||||
statCards.value[3].value = String(res.data.totalRedeems)
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
|
||||
// 加载用户趋势图表
|
||||
try {
|
||||
const res: any = await getUserTrend()
|
||||
if (res?.data) {
|
||||
const dates = res.data.map((d: any) => d.date)
|
||||
const users = res.data.map((d: any) => d.newUsers)
|
||||
const members = res.data.map((d: any) => d.newMembers)
|
||||
initLineChart(dates, users, members)
|
||||
}
|
||||
} catch {
|
||||
initLineChart([], [], [])
|
||||
}
|
||||
|
||||
// 加载优惠券分布图表
|
||||
try {
|
||||
const res: any = await getCouponDistribution()
|
||||
if (res?.data) {
|
||||
initPieChart(res.data)
|
||||
}
|
||||
} catch {
|
||||
initPieChart([])
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
lineChart?.dispose()
|
||||
pieChart?.dispose()
|
||||
})
|
||||
|
||||
function handleResize() {
|
||||
lineChart?.resize()
|
||||
pieChart?.resize()
|
||||
}
|
||||
|
||||
function initLineChart(dates: string[], users: number[], members: number[]) {
|
||||
if (!lineChartRef.value) return
|
||||
lineChart = echarts.init(lineChartRef.value)
|
||||
lineChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['新增用户', '新增会员'], bottom: 0, textStyle: { color: '#86909c' } },
|
||||
grid: { left: 40, right: 20, top: 16, bottom: 36 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLine: { lineStyle: { color: '#e5e6eb' } },
|
||||
axisLabel: { color: '#86909c' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
splitLine: { lineStyle: { color: '#f0f0f0' } },
|
||||
axisLabel: { color: '#86909c' },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '新增用户',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: users,
|
||||
itemStyle: { color: '#409eff' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(64,158,255,0.3)' },
|
||||
{ offset: 1, color: 'rgba(64,158,255,0.02)' },
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '新增会员',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: members,
|
||||
itemStyle: { color: '#67c23a' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(103,194,58,0.3)' },
|
||||
{ offset: 1, color: 'rgba(103,194,58,0.02)' },
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const pieColors = ['#409eff', '#67c23a', '#e6a23c', '#c0c4cc']
|
||||
|
||||
function initPieChart(data: any[]) {
|
||||
if (!pieChartRef.value) return
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
const chartData = data.map((d: any, i: number) => ({
|
||||
...d,
|
||||
itemStyle: { color: pieColors[i] || '#909399' },
|
||||
}))
|
||||
pieChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
legend: { bottom: 0, textStyle: { color: '#86909c' } },
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['40%', '65%'],
|
||||
center: ['50%', '45%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false },
|
||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
|
||||
data: chartData,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: #409eff;
|
||||
.dashboard { width: 100%; }
|
||||
.page-title { font-size: 22px; font-weight: 600; color: #1d2129; margin: 0 0 4px 0; }
|
||||
.page-desc { color: #86909c; font-size: 14px; margin: 0 0 20px 0; }
|
||||
.stat-card {
|
||||
background: #fff; border-radius: 8px; padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06); transition: all 0.25s; margin-bottom: 16px;
|
||||
}
|
||||
.stat-card:hover { box-shadow: 0 6px 16px rgba(0,0,0,0.1); transform: translateY(-2px); }
|
||||
.stat-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||
.stat-label { font-size: 14px; color: #86909c; }
|
||||
.stat-icon-wrap {
|
||||
width: 40px; height: 40px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.stat-value { font-size: 32px; font-weight: 700; margin-bottom: 8px; }
|
||||
.stat-footer { font-size: 12px; color: #c0c4cc; }
|
||||
.section-card { border-radius: 8px; width: 100%; margin-bottom: 16px; }
|
||||
.section-title { font-weight: 600; font-size: 15px; }
|
||||
.chart-container { width: 100%; height: 300px; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { reactive } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { login } from '@/api/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
|
@ -39,15 +40,27 @@ const form = reactive({
|
|||
password: '',
|
||||
})
|
||||
|
||||
function handleLogin() {
|
||||
async function handleLogin() {
|
||||
if (!form.username || !form.password) {
|
||||
ElMessage.warning('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
// TODO: 对接后端管理员登录接口,当前使用本地模拟
|
||||
authStore.setToken('admin-token-placeholder')
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/dashboard')
|
||||
try {
|
||||
const res: any = await login({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
})
|
||||
authStore.setToken(res.data.token)
|
||||
ElMessage.success('登录成功')
|
||||
await router.push('/dashboard')
|
||||
// 如果 push 没生效,强制刷新
|
||||
if (router.currentRoute.value.path !== '/dashboard') {
|
||||
window.location.href = '/dashboard'
|
||||
}
|
||||
} catch (err: any) {
|
||||
const msg = err.response?.data?.message || '登录失败'
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using VendingMachine.Application.Common;
|
||||
using VendingMachine.Domain.Entities;
|
||||
using VendingMachine.Infrastructure.Data;
|
||||
|
||||
namespace VendingMachine.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/auth")]
|
||||
public class AdminAuthController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public AdminAuthController(AppDbContext db, IConfiguration config)
|
||||
{
|
||||
_db = db;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录
|
||||
/// </summary>
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] AdminLoginRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
|
||||
return BadRequest(ApiResponse.Fail("请输入用户名和密码"));
|
||||
|
||||
var admin = await _db.AdminUsers.FirstOrDefaultAsync(a => a.Username == request.Username);
|
||||
if (admin == null || admin.PasswordHash != HashPassword(request.Password))
|
||||
return Unauthorized(ApiResponse.Fail("用户名或密码错误"));
|
||||
|
||||
var token = GenerateAdminToken(admin);
|
||||
return Ok(ApiResponse<object>.Ok(new { token, username = admin.Username }));
|
||||
}
|
||||
|
||||
private string GenerateAdminToken(AdminUser admin)
|
||||
{
|
||||
var jwtSection = _config.GetSection("Jwt");
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSection["Secret"]!));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, admin.Id.ToString()),
|
||||
new Claim(ClaimTypes.Name, admin.Username),
|
||||
new Claim(ClaimTypes.Role, "Admin")
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: jwtSection["Issuer"],
|
||||
audience: jwtSection["Audience"],
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddHours(24),
|
||||
signingCredentials: credentials);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
// SHA256 哈希密码
|
||||
internal static string HashPassword(string password)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(password));
|
||||
return Convert.ToHexStringLower(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public class AdminLoginRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using VendingMachine.Application.Common;
|
||||
using VendingMachine.Domain.Entities;
|
||||
using VendingMachine.Infrastructure.Data;
|
||||
|
||||
namespace VendingMachine.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/dashboard")]
|
||||
public class AdminDashboardController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public AdminDashboardController(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仪表盘统计数据
|
||||
/// </summary>
|
||||
[HttpGet("stats")]
|
||||
public async Task<IActionResult> GetStats()
|
||||
{
|
||||
var totalUsers = await _db.Users.CountAsync();
|
||||
var totalMembers = await _db.Users.CountAsync(u => u.IsMember);
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var todayPoints = await _db.PointRecords
|
||||
.Where(p => p.Type == PointRecordType.Earn && p.CreatedAt >= today)
|
||||
.SumAsync(p => p.Amount);
|
||||
var totalRedeems = await _db.UserCoupons.CountAsync();
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(new
|
||||
{
|
||||
totalUsers,
|
||||
totalMembers,
|
||||
todayPoints,
|
||||
totalRedeems
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 近7天用户增长趋势
|
||||
/// </summary>
|
||||
[HttpGet("user-trend")]
|
||||
public async Task<IActionResult> GetUserTrend()
|
||||
{
|
||||
var days = new List<object>();
|
||||
for (int i = 6; i >= 0; i--)
|
||||
{
|
||||
var date = DateTime.UtcNow.Date.AddDays(-i);
|
||||
var nextDate = date.AddDays(1);
|
||||
var newUsers = await _db.Users.CountAsync(u => u.CreatedAt >= date && u.CreatedAt < nextDate);
|
||||
var newMembers = await _db.Users.CountAsync(u => u.IsMember && u.CreatedAt >= date && u.CreatedAt < nextDate);
|
||||
days.Add(new
|
||||
{
|
||||
date = date.ToString("M/d"),
|
||||
newUsers,
|
||||
newMembers
|
||||
});
|
||||
}
|
||||
return Ok(ApiResponse<object>.Ok(days));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 优惠券类型分布
|
||||
/// </summary>
|
||||
[HttpGet("coupon-distribution")]
|
||||
public async Task<IActionResult> GetCouponDistribution()
|
||||
{
|
||||
var threshold = await _db.CouponTemplates.CountAsync(c => c.Type == CouponType.ThresholdDiscount && !c.IsStamp && c.IsActive);
|
||||
var direct = await _db.CouponTemplates.CountAsync(c => c.Type == CouponType.DirectDiscount && !c.IsStamp && c.IsActive);
|
||||
var stamp = await _db.CouponTemplates.CountAsync(c => c.IsStamp && c.IsActive);
|
||||
var expired = await _db.CouponTemplates.CountAsync(c => !c.IsActive);
|
||||
|
||||
return Ok(ApiResponse<object>.Ok(new[]
|
||||
{
|
||||
new { name = "满减券", value = threshold },
|
||||
new { name = "抵扣券", value = direct },
|
||||
new { name = "节日印花", value = stamp },
|
||||
new { name = "已下架", value = expired }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -84,4 +84,20 @@ app.UseAuthentication();
|
|||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
// 初始化默认管理员账号
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
db.Database.Migrate();
|
||||
if (!db.AdminUsers.Any())
|
||||
{
|
||||
db.AdminUsers.Add(new VendingMachine.Domain.Entities.AdminUser
|
||||
{
|
||||
Username = "admin",
|
||||
PasswordHash = VendingMachine.Api.Controllers.AdminAuthController.HashPassword("admin123")
|
||||
});
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Database=VendingMachineDb;Trusted_Connection=True;TrustServerCertificate=True;",
|
||||
"Redis": "localhost:6379"
|
||||
"DefaultConnection": "Server=tcp:192.168.195.15,1433;Database=VendingMachineDb;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=True;Encrypt=False;",
|
||||
"Redis": "192.168.195.15:6379,defaultDatabase=1"
|
||||
},
|
||||
"Jwt": {
|
||||
"Secret": "YourSuperSecretKeyHere_ChangeInProduction_AtLeast32Chars!",
|
||||
|
|
|
|||
10
backend/src/VendingMachine.Domain/Entities/AdminUser.cs
Normal file
10
backend/src/VendingMachine.Domain/Entities/AdminUser.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace VendingMachine.Domain.Entities;
|
||||
|
||||
// 管理员用户实体
|
||||
public class AdminUser
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ public class AppDbContext : DbContext
|
|||
public DbSet<HomeEntry> HomeEntries => Set<HomeEntry>();
|
||||
public DbSet<ContentConfig> ContentConfigs => Set<ContentConfig>();
|
||||
public DbSet<VendingPaymentRecord> VendingPaymentRecords => Set<VendingPaymentRecord>();
|
||||
public DbSet<AdminUser> AdminUsers => Set<AdminUser>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
|
@ -106,5 +107,13 @@ public class AppDbContext : DbContext
|
|||
e.Property(v => v.TransactionId).HasMaxLength(100);
|
||||
e.HasIndex(v => v.UserId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AdminUser>(e =>
|
||||
{
|
||||
e.HasKey(a => a.Id);
|
||||
e.Property(a => a.Username).HasMaxLength(50);
|
||||
e.Property(a => a.PasswordHash).HasMaxLength(200);
|
||||
e.HasIndex(a => a.Username).IsUnique();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
432
backend/src/VendingMachine.Infrastructure/Data/Migrations/20260403081502_AddAdminUser.Designer.cs
generated
Normal file
432
backend/src/VendingMachine.Infrastructure/Data/Migrations/20260403081502_AddAdminUser.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using VendingMachine.Infrastructure.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace VendingMachine.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260403081502_AddAdminUser")]
|
||||
partial class AddAdminUser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.AdminUser", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AdminUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.Banner", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ImageUrlEn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ImageUrlZhCn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ImageUrlZhTw")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("LinkType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<string>("LinkUrl")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Banners");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.ContentConfig", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ContentEn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ContentZhCn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ContentZhTw")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ContentConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.CouponTemplate", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<decimal>("DiscountAmount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime>("ExpireAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsStamp")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("NameEn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("NameZhCn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("NameZhTw")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("PointsCost")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal?>("ThresholdAmount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("CouponTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.HomeEntry", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("ImageUrlEn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ImageUrlZhCn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ImageUrlZhTw")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("HomeEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.MembershipProduct", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("AppleProductId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<string>("DescriptionEn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DescriptionZhCn")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DescriptionZhTw")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("DurationDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("GoogleProductId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MembershipProducts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.PointRecord", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int>("Amount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("nvarchar(12)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("PointRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.PointsConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<decimal>("ConversionRate")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PointsConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.User", b =>
|
||||
{
|
||||
b.Property<string>("Uid")
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("nvarchar(12)");
|
||||
|
||||
b.Property<string>("AreaCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsMember")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<DateTime?>("MembershipExpireAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("MembershipType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Nickname")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<int>("PointsBalance")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("PointsExpireAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.HasKey("Uid");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.UserCoupon", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("CouponTemplateId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("ExpireAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("UsedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("nvarchar(12)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CouponTemplateId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserCoupons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.VendingPaymentRecord", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("MachineId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<decimal>("PaymentAmount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("PaymentStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<string>("TransactionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("UsedCouponId")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(12)
|
||||
.HasColumnType("nvarchar(12)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("VendingPaymentRecords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.UserCoupon", b =>
|
||||
{
|
||||
b.HasOne("VendingMachine.Domain.Entities.CouponTemplate", "CouponTemplate")
|
||||
.WithMany()
|
||||
.HasForeignKey("CouponTemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CouponTemplate");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace VendingMachine.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAdminUser : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AdminUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Username = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AdminUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AdminUsers_Username",
|
||||
table: "AdminUsers",
|
||||
column: "Username",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AdminUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,35 @@ namespace VendingMachine.Infrastructure.Data.Migrations
|
|||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.AdminUser", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AdminUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("VendingMachine.Domain.Entities.Banner", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/home/index",
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "首页",
|
||||
"navigationStyle": "custom"
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/home/index",
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ async function handleDeleteAccount() {
|
|||
showDeleteConfirm.value = false
|
||||
uni.showToast({ title: t('about.deleteSuccess'), icon: 'none' })
|
||||
// 返回首页
|
||||
uni.switchTab({ url: '/pages/home/index' })
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
} catch {
|
||||
// 错误已由 request 层处理
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ async function handleLogin() {
|
|||
const info = await getUserInfo()
|
||||
userStore.setUserInfo(info)
|
||||
// 跳转首页
|
||||
uni.switchTab({ url: '/pages/home/index' })
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
} catch {
|
||||
// 错误已由 request 层处理
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user