集成 payment_order 表相关接口到订单列表页面

This commit is contained in:
wangzhiwei 2026-04-24 11:55:21 +08:00
commit 3395e0a110
34 changed files with 6685 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>admin-frontend</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1876
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "admin-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.7",
"crypto-js": "^4.2.0",
"element-plus": "^2.8.4",
"pinia": "^2.2.5",
"vue": "^3.5.25",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.2",
"vite": "^7.3.1"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
skills-api.json Normal file

File diff suppressed because one or more lines are too long

15
src/App.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<router-view />
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
}
</style>

16
src/api/account.js Normal file
View File

@ -0,0 +1,16 @@
import api from './index'
export const accountApi = {
// 获取账户列表
getPageList: (data) => api.post('/account/pageList', data),
// 根据ID查询账户
queryById: (id) => api.post(`/account/queryById/${id}`),
// 更新账户
update: (data) => api.post('/account/update', data),
// 冻结/解冻账户
freeze: (data) => api.post('/account/freeze', data),
// 获取交易记录
getTransactions: (data) => api.post('/account/getTransactions', data),
// 给用户赠送金额(不可提现)
addGiftBalance: (data) => api.post('/account/addGiftBalance', data)
}

29
src/api/content.js Normal file
View File

@ -0,0 +1,29 @@
import api from './index'
export const contentApi = {
// 获取内容列表
getPageList: (data) => api.post('/cmsContent/getPageList', data),
// 根据ID查询内容
queryById: (id) => api.post(`/cmsContent/queryById/${id}`),
// 新增内容
insert: (data) => api.post('/cmsContent/insert', data),
// 更新内容
update: (data) => api.post('/cmsContent/update', data),
// 删除内容
deleteById: (id) => api.post(`/cmsContent/deleteById/${id}`),
// 审核内容
audit: (data) => api.post('/cmsContent/audit', data),
// 发布内容
publish: (data) => api.post('/cmsContent/publish', data),
// 从ZIP文件导入
importFromZip: (formData) => api.post('/cmsContent/importFromZip', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export const tagApi = {
// 获取标签列表
getList: () => api.post('/cmsTag/getList', {})
}

16
src/api/dict.js Normal file
View File

@ -0,0 +1,16 @@
import api from './index'
export const dictApi = {
// 获取字典列表
getPageList: (data) => api.post('/sysDict/getPageList', data),
// 根据ID查询字典
queryById: (id) => api.post(`/sysDict/queryById/${id}`),
// 新增字典
insert: (data) => api.post('/sysDict/insert', data),
// 更新字典
update: (data) => api.post('/sysDict/update', data),
// 删除字典
deleteById: (id) => api.post(`/sysDict/deleteById/${id}`),
// 获取字典类型
getDictTypes: () => api.post('/sysDict/getDictTypes')
}

57
src/api/index.js Normal file
View File

@ -0,0 +1,57 @@
import axios from 'axios'
import router from '../router'
import { config } from '../config'
const api = axios.create({
baseURL: config.apiBaseUrl,
timeout: 10000
})
// 请求拦截器
api.interceptors.request.use(
requestConfig => {
console.log('请求拦截器执行', requestConfig.url)
const token = localStorage.getItem('token')
console.log('token:', token)
if (token) {
requestConfig.headers.Authorization = token
console.log('添加Authorization头部:', token)
}
return requestConfig
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
response => {
const res = response.data
if (res.status !== 1000) {
// 处理token过期状态码为-1000且提示中包含token相关字眼
if (res.status === -1000 && res.message && (res.message.includes('token') || res.message.includes('Token'))) {
// 清除本地存储的token和用户信息
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
// 跳转到登录页面
router.push('/login')
}
return Promise.reject(new Error(res.message || 'Error'))
}
return res
},
error => {
// 处理token过期
if (error.response && error.response.status === 401) {
// 清除本地存储的token和用户信息
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
// 跳转到登录页面
router.push('/login')
}
return Promise.reject(error)
}
)
export default api

10
src/api/order.js Normal file
View File

@ -0,0 +1,10 @@
import api from './index'
export const orderApi = {
// 获取订单列表
getPageList: (data) => api.post('/paymentOrder/pageList', data),
// 根据ID查询订单
queryById: (id) => api.post(`/paymentOrder/queryById/${id}`),
// 通过订单号查询订单
queryByOrderNo: (orderNo) => api.post('/paymentOrder/queryByOrderNo', { orderNo })
}

18
src/api/role.js Normal file
View File

@ -0,0 +1,18 @@
import api from './index'
export const roleApi = {
// 获取角色列表
getPageList: (data) => api.post('/sysRole/getPageList', data),
// 根据ID查询角色
queryById: (id) => api.post(`/sysRole/queryById/${id}`),
// 新增角色
insert: (data) => api.post('/sysRole/insert', data),
// 更新角色
update: (data) => api.post('/sysRole/update', data),
// 删除角色
deleteById: (id) => api.post(`/sysRole/deleteById/${id}`),
// 获取角色权限
getRolePermissions: (roleId) => api.post(`/sysRole/getPermissions/${roleId}`),
// 分配权限
assignPermissions: (data) => api.post('/sysRole/assignPermissions', data)
}

28
src/api/user.js Normal file
View File

@ -0,0 +1,28 @@
import api from './index'
export const userApi = {
// 登录
login: async (data) => {
console.log('发送登录请求:', data);
try {
const response = await api.post('/login/accountLogin', data);
console.log('登录响应:', response);
return response;
} catch (error) {
console.error('登录错误:', error);
throw error;
}
},
// 获取用户列表
getPageList: (data) => api.post('/sysUser/getPageList', data),
// 根据ID查询用户
queryById: (id) => api.post(`/sysUser/queryById/${id}`),
// 新增用户
insert: (data) => api.post('/sysUser/insert', data),
// 更新用户
update: (data) => api.post('/sysUser/update', data),
// 删除用户
deleteById: (id) => api.post(`/sysUser/deleteById/${id}`),
// 重置密码
resetPassword: (data) => api.post('/sysUser/resetPassword', data)
}

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,43 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

8
src/config/index.js Normal file
View File

@ -0,0 +1,8 @@
// 配置文件
export const config = {
// 后端API地址
apiBaseUrl: 'http://localhost:19001/api',
// apiBaseUrl: 'https://skills.xueai.art/api',
// 前端基础路径
baseUrl: '/'
}

16
src/main.js Normal file
View File

@ -0,0 +1,16 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import 'element-plus/dist/index.css'
import router from './router'
import App from './App.vue'
import './style.css'
const app = createApp(App)
app.use(createPinia())
app.use(ElementPlus, {
locale: zhCn
})
app.use(router)
app.mount('#app')

73
src/router/index.js Normal file
View File

@ -0,0 +1,73 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue'),
meta: { requiresAuth: true },
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue')
},
{
path: 'user',
name: 'User',
component: () => import('../views/user/List.vue')
},
{
path: 'role',
name: 'Role',
component: () => import('../views/role/List.vue')
},
{
path: 'content',
name: 'Content',
component: () => import('../views/content/List.vue')
},
{
path: 'order',
name: 'Order',
component: () => import('../views/order/List.vue')
},
{
path: 'account',
name: 'Account',
component: () => import('../views/account/List.vue')
},
{
path: 'dict',
name: 'Dict',
component: () => import('../views/dict/List.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token')
if (token) {
next()
} else {
next({ name: 'Login' })
}
} else {
next()
}
})
export default router

27
src/store/user.js Normal file
View File

@ -0,0 +1,27 @@
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null')
}),
getters: {
isLoggedIn: (state) => !!state.token
},
actions: {
setToken(token) {
this.token = token
localStorage.setItem('token', token)
},
setUserInfo(userInfo) {
this.userInfo = userInfo
localStorage.setItem('userInfo', JSON.stringify(userInfo))
},
logout() {
this.token = ''
this.userInfo = null
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
}
})

79
src/style.css Normal file
View File

@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background-color: #f5f5f5;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
text-align: left;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

233
src/views/Dashboard.vue Normal file
View File

@ -0,0 +1,233 @@
<template>
<div class="dashboard-container">
<h2 class="page-title">控制台</h2>
<!-- 统计卡片 -->
<el-row :gutter="20" class="stat-row">
<el-col :xs="12" :sm="12" :md="6" :lg="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-number">{{ userCount }}</div>
<div class="stat-label">用户总数</div>
</div>
<div class="stat-icon">
<el-icon class="icon-large"><i-ep-user /></el-icon>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="12" :md="6" :lg="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-number">{{ contentCount }}</div>
<div class="stat-label">内容总数</div>
</div>
<div class="stat-icon">
<el-icon class="icon-large"><i-ep-document /></el-icon>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="12" :md="6" :lg="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-number">{{ orderCount }}</div>
<div class="stat-label">订单总数</div>
</div>
<div class="stat-icon">
<el-icon class="icon-large"><i-ep-s-order /></el-icon>
</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="12" :md="6" :lg="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-number">{{ revenue }}</div>
<div class="stat-label">总交易额</div>
</div>
<div class="stat-icon">
<el-icon class="icon-large"><i-ep-wallet /></el-icon>
</div>
</el-card>
</el-col>
</el-row>
<!-- 最近活动 -->
<el-card class="mt-20">
<template #header>
<div class="card-header">
<span>最近活动</span>
</div>
</template>
<el-table :data="recentActivities" style="width: 100%" class="activity-table">
<el-table-column prop="time" label="时间" width="180"></el-table-column>
<el-table-column prop="user" label="用户" width="120"></el-table-column>
<el-table-column prop="action" label="操作"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === 'success' ? 'success' : 'info'">
{{ scope.row.status === 'success' ? '成功' : '处理中' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
//
const userCount = ref(1234)
const contentCount = ref(5678)
const orderCount = ref(9012)
const revenue = ref('¥123,456')
const recentActivities = ref([
{
time: '2026-03-12 10:00',
user: 'admin',
action: '登录系统',
status: 'success'
},
{
time: '2026-03-12 09:30',
user: 'user1',
action: '购买内容',
status: 'success'
},
{
time: '2026-03-12 09:00',
user: 'admin',
action: '发布新内容',
status: 'success'
},
{
time: '2026-03-11 18:00',
user: 'user2',
action: '充值账户',
status: 'success'
}
])
onMounted(() => {
// API
console.log('Dashboard mounted')
})
</script>
<style scoped>
.dashboard-container {
padding: 0;
}
.page-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
.stat-row {
margin-bottom: 20px;
}
.stat-card {
position: relative;
overflow: hidden;
height: 120px;
transition: all 0.3s;
}
.stat-content {
position: relative;
z-index: 1;
}
.stat-number {
font-size: 32px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #666;
}
.stat-icon {
position: absolute;
top: 10px;
right: 10px;
color: #1890ff;
opacity: 0.2;
}
.icon-large {
font-size: 48px;
}
.mt-20 {
margin-top: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.activity-table {
width: 100%;
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-title {
font-size: 20px;
margin-bottom: 15px;
}
.stat-card {
height: 100px;
}
.stat-number {
font-size: 24px;
}
.stat-label {
font-size: 12px;
}
.icon-large {
font-size: 36px;
}
.activity-table {
font-size: 14px;
}
.activity-table .el-table-column {
width: auto !important;
}
}
@media (max-width: 480px) {
.stat-card {
height: 90px;
}
.stat-number {
font-size: 20px;
}
.stat-label {
font-size: 11px;
}
.icon-large {
font-size: 30px;
}
}
</style>

322
src/views/Home.vue Normal file
View File

@ -0,0 +1,322 @@
<template>
<div class="home-container">
<!-- 侧边栏 -->
<el-aside :width="sidebarCollapsed ? '64px' : '200px'" class="sidebar" :class="{ 'collapsed': sidebarCollapsed }">
<div class="logo" :class="{ 'collapsed': sidebarCollapsed }">
<h3 v-if="!sidebarCollapsed">Skill后台管理</h3>
<div v-else class="logo-collapsed">S</div>
</div>
<el-menu
:default-active="activeMenu"
class="sidebar-menu"
router
background-color="#001529"
text-color="#fff"
active-text-color="#409EFF"
:collapse="sidebarCollapsed"
:collapse-transition="false"
>
<el-menu-item index="/dashboard">
<el-icon><i-ep-home /></el-icon>
<template #title>
<span>控制台</span>
</template>
</el-menu-item>
<el-sub-menu index="user">
<template #title>
<el-icon><i-ep-user /></el-icon>
<span>用户管理</span>
</template>
<el-menu-item index="/user">
<template #title>
<span>用户列表</span>
</template>
</el-menu-item>
<el-menu-item index="/role">
<template #title>
<span>角色管理</span>
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="content">
<template #title>
<el-icon><i-ep-document /></el-icon>
<span>内容管理</span>
</template>
<el-menu-item index="/content">
<template #title>
<span>内容列表</span>
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="order">
<template #title>
<el-icon><i-ep-s-order /></el-icon>
<span>订单管理</span>
</template>
<el-menu-item index="/order">
<template #title>
<span>订单列表</span>
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="account">
<template #title>
<el-icon><i-ep-wallet /></el-icon>
<span>账户管理</span>
</template>
<el-menu-item index="/account">
<template #title>
<span>账户列表</span>
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="system">
<template #title>
<el-icon><i-ep-setting /></el-icon>
<span>系统管理</span>
</template>
<el-menu-item index="/dict">
<template #title>
<span>字典管理</span>
</template>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<!-- 主内容区 -->
<el-container class="main-container">
<!-- 顶部导航栏 -->
<el-header class="top-header">
<div class="header-left">
<el-button type="text" class="menu-toggle" @click="toggleSidebar">
<el-icon><i-ep-menu /></el-icon>
</el-button>
</div>
<div class="header-right">
<el-dropdown>
<span class="user-info">
<el-avatar :size="32" :src="userStore.userInfo?.avatar || ''"></el-avatar>
<span class="username">{{ userStore.userInfo?.username || '管理员' }}</span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<!-- 内容区域 -->
<el-main class="content-area">
<router-view />
</el-main>
</el-container>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '../store/user'
import { ElMessage } from 'element-plus'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const sidebarCollapsed = ref(false)
const activeMenu = computed(() => {
return route.path
})
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value
}
const handleLogout = () => {
userStore.logout()
ElMessage.success('退出成功')
router.push('/login')
}
onMounted(() => {
//
if (!userStore.isLoggedIn) {
router.push('/login')
}
//
const handleResize = () => {
if (window.innerWidth < 768) {
sidebarCollapsed.value = true
}
}
window.addEventListener('resize', handleResize)
handleResize() //
//
return () => {
window.removeEventListener('resize', handleResize)
}
})
</script>
<style scoped>
.home-container {
display: flex;
height: 100vh;
overflow: hidden;
}
.sidebar {
background-color: #001529;
height: 100%;
transition: width 0.3s;
flex-shrink: 0;
}
.sidebar.collapsed {
width: 64px !important;
}
.logo {
padding: 20px;
text-align: center;
border-bottom: 1px solid #1890ff;
transition: all 0.3s;
}
.logo.collapsed {
padding: 15px 0;
}
.logo h3 {
color: white;
margin: 0;
font-size: 18px;
transition: all 0.3s;
}
.logo-collapsed {
color: white;
font-size: 20px;
font-weight: bold;
text-align: center;
}
.sidebar-menu {
height: calc(100% - 70px);
overflow-y: auto;
}
/* 调整菜单缩进:一级菜单不缩进,二级菜单缩进 */
:deep(.el-sub-menu > .el-sub-menu__title) {
padding-left: 0 !important;
}
:deep(.el-sub-menu .el-menu-item) {
padding-left: 40px !important;
}
/* 折叠状态下的样式 */
.sidebar.collapsed :deep(.el-sub-menu > .el-sub-menu__title) {
padding-left: 0 !important;
}
.sidebar.collapsed :deep(.el-sub-menu .el-menu-item) {
padding-left: 10px !important;
}
.main-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.top-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
background-color: white;
border-bottom: 1px solid #e8e8e8;
height: 60px;
}
.menu-toggle {
color: #666;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.username {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 120px;
}
.content-area {
flex: 1;
overflow-y: auto;
padding: 20px 10px;
background-color: #f5f5f5;
width: 100%;
max-width: none;
}
/* 响应式设计 */
@media (min-width: 1200px) {
.content-area {
padding: 20px;
}
}
/* 移除卡片默认边距,使内容更贴近边缘 */
:deep(.el-card) {
margin-bottom: 20px;
}
/* 确保卡片内容充满宽度 */
:deep(.el-card__body) {
padding: 20px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
left: 0;
top: 0;
z-index: 1000;
height: 100vh;
}
.sidebar.collapsed {
transform: translateX(-100%);
}
.main-container {
margin-left: 0;
}
.content-area {
padding: 10px;
}
.username {
display: none;
}
}
</style>

231
src/views/Login.vue Normal file
View File

@ -0,0 +1,231 @@
<template>
<div class="login-container">
<el-card class="login-card">
<template #header>
<div class="login-header">
<h2>可学后台管理</h2>
<p>请登录系统</p>
</div>
</template>
<el-form :model="loginForm" :rules="rules" ref="loginFormRef" label-width="80px" class="login-form">
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username" placeholder="请输入用户名" class="full-width"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" class="full-width"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="captchaValue">
<div class="captcha-container">
<el-input v-model="loginForm.captchaValue" placeholder="请输入验证码" class="captcha-input"></el-input>
<div class="captcha-img" @click="refreshCaptcha">
<img :src="captchaUrl" alt="验证码">
</div>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin" class="login-button">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { userApi } from '../api/user'
import { useUserStore } from '../store/user'
import { ElMessage } from 'element-plus'
import { config } from '../config'
import CryptoJS from 'crypto-js'
// 使crypto-jsmd5
const md5 = (str) => {
return CryptoJS.MD5(str).toString();
}
const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref(null)
const captchaId = ref('')
const captchaUrl = ref('')
const loginForm = reactive({
username: '',
password: '',
captchaId: '',
captchaValue: ''
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
],
captchaValue: [
{ required: true, message: '请输入验证码', trigger: 'blur' }
]
}
const refreshCaptcha = async () => {
try {
//
console.log('开始获取验证码...')
const response = await fetch(`${config.apiBaseUrl}/captcha/generate?t=${Date.now()}`)
console.log('验证码接口响应:', response)
const data = await response.json()
console.log('验证码接口返回数据:', data)
if (data.status === 1000) {
captchaId.value = data.data.captchaId
captchaUrl.value = data.data.captchaImage
loginForm.captchaId = captchaId.value
console.log('验证码ID:', captchaId.value)
console.log('登录表单中的验证码ID:', loginForm.captchaId)
console.log('验证码图片URL:', captchaUrl.value)
} else {
ElMessage.error('获取验证码失败')
console.error('获取验证码失败:', data.message)
}
} catch (error) {
ElMessage.error('获取验证码失败')
console.error('获取验证码异常:', error)
}
}
const handleLogin = async () => {
if (!loginFormRef.value) return
console.log('开始登录...')
console.log('登录表单:', loginForm)
await loginFormRef.value.validate(async (valid) => {
if (valid) {
try {
// MD5
const passwordMD5 = md5(loginForm.password)
console.log('密码MD5:', passwordMD5)
const loginData = {
username: loginForm.username,
password: passwordMD5,
captchaId: loginForm.captchaId,
captchaValue: loginForm.captchaValue
}
console.log('登录数据:', loginData)
console.log('API地址:', config.apiBaseUrl + '/login/accountLogin')
const response = await userApi.login(loginData)
console.log('登录响应:', response)
if (response.status === 1000) {
userStore.setToken(response.data.token)
userStore.setUserInfo(response.data.userInfo)
ElMessage.success('登录成功')
router.push('/dashboard')
} else {
ElMessage.error(response.message)
console.error('登录失败:', response.message)
}
} catch (error) {
console.error('登录错误:', error)
console.error('错误详情:', error.response ? error.response.data : error.message)
ElMessage.error('登录失败,请检查用户名和密码')
}
} else {
console.error('表单验证失败')
}
})
}
onMounted(() => {
refreshCaptcha()
})
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 0 20px;
}
.login-card {
width: 100%;
max-width: 400px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 20px;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h2 {
color: #333;
margin-bottom: 10px;
font-size: 24px;
}
.login-header p {
color: #666;
font-size: 14px;
}
.login-form {
width: 100%;
}
.full-width {
width: 100%;
}
.captcha-container {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.captcha-input {
flex: 1;
}
.captcha-img {
width: 120px;
cursor: pointer;
}
.captcha-img img {
width: 100%;
height: 40px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #dcdfe6;
}
.login-button {
width: 100%;
height: 40px;
font-size: 16px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.login-card {
max-width: 100%;
margin: 0 20px;
}
.captcha-img {
width: 100px;
}
}
</style>

527
src/views/account/List.vue Normal file
View File

@ -0,0 +1,527 @@
<template>
<div class="account-list-container">
<h2 class="page-title">账户管理</h2>
<!-- 搜索和操作栏 -->
<el-card class="mb-20">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="用户ID" class="search-form-item">
<el-input v-model="searchForm.userId" placeholder="请输入用户ID"></el-input>
</el-form-item>
<el-form-item label="用户名" class="search-form-item">
<el-input v-model="searchForm.userName" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item class="search-form-item">
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 账户列表 -->
<el-card>
<div class="table-container">
<el-table :data="accountList" style="width: 100%" class="responsive-table">
<el-table-column prop="accountId" label="账户ID" width="100"></el-table-column>
<el-table-column prop="userId" label="用户ID" width="100"></el-table-column>
<el-table-column prop="userName" label="用户名">
<template #default="scope">
<el-tooltip :content="scope.row.userName" placement="top" :disabled="!scope.row.userName || scope.row.userName.length <= 10">
<span class="ellipsis">{{ scope.row.userName || '-' }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="balance" label="余额" width="120">
<template #default="scope">
¥{{ scope.row.balance.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="frozenAmount" label="冻结金额" width="120">
<template #default="scope">
¥{{ scope.row.frozenAmount.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="1"
inactive-value="0"
@change="handleStatusChange(scope.row)"
></el-switch>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<div class="action-buttons">
<el-button type="primary" size="small" @click="handleDetail(scope.row)">详情</el-button>
<el-button type="warning" size="small" @click="handleFreeze(scope.row)">冻结/解冻</el-button>
<el-button type="info" size="small" @click="handleTransactions(scope.row.userId)">交易记录</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.pageNum"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
<!-- 账户详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="账户详情"
:width="dialogWidth"
>
<el-descriptions :column="1" border>
<el-descriptions-item label="账户ID">{{ currentAccount.accountId }}</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ currentAccount.userId }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ currentAccount.userName }}</el-descriptions-item>
<el-descriptions-item label="余额">¥{{ currentAccount.balance?.toFixed(2) || '0.00' }}</el-descriptions-item>
<el-descriptions-item label="冻结金额">¥{{ currentAccount.frozenAmount?.toFixed(2) || '0.00' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ currentAccount.status === 1 ? '正常' : '冻结' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ currentAccount.createTime }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ currentAccount.updateTime }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
<!-- 冻结/解冻弹窗 -->
<el-dialog
v-model="freezeDialogVisible"
:title="currentAccount.status === 1 ? '冻结账户' : '解冻账户'"
:width="smallDialogWidth"
>
<el-form :model="freezeForm" :rules="freezeRules" ref="freezeFormRef" label-width="80px" class="full-width-form">
<el-form-item label="用户ID">{{ currentAccount.userId }}</el-form-item>
<el-form-item label="用户名">{{ currentAccount.userName }}</el-form-item>
<el-form-item label="操作类型">{{ currentAccount.status === 1 ? '冻结' : '解冻' }}</el-form-item>
<el-form-item label="冻结金额" v-if="currentAccount.status === 1" prop="frozenAmount">
<el-input v-model.number="freezeForm.frozenAmount" placeholder="请输入冻结金额" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="操作原因" prop="reason">
<el-input v-model="freezeForm.reason" type="textarea" placeholder="请输入操作原因" class="full-width-input"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="freezeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmitFreeze">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 交易记录弹窗 -->
<el-dialog
v-model="transactionDialogVisible"
title="交易记录"
:width="dialogWidth"
>
<div class="table-container">
<el-table :data="transactionList" style="width: 100%" class="responsive-table">
<el-table-column prop="transactionId" label="交易ID" width="120"></el-table-column>
<el-table-column prop="transactionType" label="交易类型" width="120">
<template #default="scope">
<el-tag :type="getTransactionType(scope.row.transactionType)">
{{ getTransactionTypeText(scope.row.transactionType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="100">
<template #default="scope">
{{ scope.row.transactionType === 1 ? '+' : '-' }}¥{{ scope.row.amount.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="balance" label="余额" width="120">
<template #default="scope">
¥{{ scope.row.balance.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="transactionTime" label="交易时间" width="180"></el-table-column>
<el-table-column prop="remark" label="备注">
<template #default="scope">
<el-tooltip :content="scope.row.remark" placement="top" :disabled="!scope.row.remark || scope.row.remark.length <= 10">
<span class="ellipsis">{{ scope.row.remark || '-' }}</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="transactionDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, onUnmounted } from 'vue'
import { accountApi } from '../../api/account'
import { ElMessage, ElMessageBox } from 'element-plus'
const accountList = ref([])
const total = ref(0)
const pageInfo = reactive({
pageNum: 1,
pageSize: 10
})
const searchForm = reactive({
userId: '',
userName: ''
})
const detailDialogVisible = ref(false)
const freezeDialogVisible = ref(false)
const transactionDialogVisible = ref(false)
const currentAccount = ref({})
const freezeFormRef = ref(null)
const transactionList = ref([])
const freezeForm = reactive({
frozenAmount: 0,
reason: ''
})
const freezeRules = {
frozenAmount: [
{ required: true, message: '请输入冻结金额', trigger: 'blur' },
{ type: 'number', message: '请输入正确的金额', trigger: 'blur' }
],
reason: [
{ required: true, message: '请输入操作原因', trigger: 'blur' }
]
}
//
const dialogWidth = computed(() => {
return window.innerWidth < 768 ? '90%' : '600px'
})
const smallDialogWidth = computed(() => {
return window.innerWidth < 768 ? '90%' : '400px'
})
const getTransactionTypeText = (type) => {
switch (type) {
case 1: return '充值'
case 2: return '消费'
case 3: return '退款'
case 4: return '冻结'
case 5: return '解冻'
default: return '未知'
}
}
const getTransactionType = (type) => {
switch (type) {
case 1: return 'success'
case 2: return 'danger'
case 3: return 'warning'
case 4: return 'info'
case 5: return 'info'
default: return ''
}
}
const loadAccountList = async () => {
try {
const params = {
...searchForm,
...pageInfo
}
const response = await accountApi.getPageList(params)
if (response && response.data) {
accountList.value = response.data.list || []
total.value = response.data.total || 0
} else {
accountList.value = []
total.value = 0
}
} catch (error) {
ElMessage.error('获取账户列表失败')
accountList.value = []
total.value = 0
}
}
const handleSearch = () => {
pageInfo.pageNum = 1
loadAccountList()
}
const resetSearch = () => {
searchForm.userId = ''
searchForm.userName = ''
pageInfo.pageNum = 1
loadAccountList()
}
const handleSizeChange = (size) => {
pageInfo.pageSize = size
loadAccountList()
}
const handleCurrentChange = (current) => {
pageInfo.pageNum = current
loadAccountList()
}
const handleDetail = async (row) => {
try {
const response = await accountApi.queryById(row.accountId)
currentAccount.value = response.data
detailDialogVisible.value = true
} catch (error) {
ElMessage.error('获取账户详情失败')
}
}
const handleStatusChange = async (row) => {
try {
await accountApi.update(row)
} catch (error) {
ElMessage.error('更新状态失败')
row.status = row.status === 1 ? 0 : 1
}
}
const handleFreeze = (row) => {
currentAccount.value = row
freezeForm.frozenAmount = 0
freezeForm.reason = ''
freezeDialogVisible.value = true
}
const handleSubmitFreeze = async () => {
if (!freezeFormRef.value) return
await freezeFormRef.value.validate(async (valid) => {
if (valid) {
try {
await accountApi.freeze({
accountId: currentAccount.value.accountId,
frozenAmount: currentAccount.value.status === 1 ? freezeForm.frozenAmount : 0,
status: currentAccount.value.status === 1 ? 0 : 1,
reason: freezeForm.reason
})
ElMessage.success(currentAccount.value.status === 1 ? '冻结成功' : '解冻成功')
freezeDialogVisible.value = false
loadAccountList()
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
const handleTransactions = async (userId) => {
try {
const response = await accountApi.getTransactions({ userId })
transactionList.value = response.data
transactionDialogVisible.value = true
} catch (error) {
ElMessage.error('获取交易记录失败')
}
}
//
const handleResize = () => {
//
}
onMounted(() => {
loadAccountList()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.account-list-container {
padding: 0;
}
.page-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
.mb-20 {
margin-bottom: 20px;
}
.search-form {
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.search-form-item {
margin-bottom: 10px;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
.responsive-table {
min-width: 800px;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
}
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.full-width-form {
width: 100%;
}
.full-width-input {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.page-title {
font-size: 20px;
margin-bottom: 15px;
}
.search-form {
flex-direction: column;
align-items: stretch;
}
.search-form-item {
width: 100%;
}
.search-form-item .el-input {
width: 100%;
}
.action-buttons {
flex-direction: column;
align-items: stretch;
}
.action-buttons .el-button {
width: 100%;
margin-bottom: 5px;
}
.pagination-container {
justify-content: center;
}
.el-pagination {
flex-wrap: wrap;
}
.el-table {
font-size: 14px;
}
.el-table th,
.el-table td {
padding: 8px;
}
.el-form-item {
margin-bottom: 15px;
}
.el-form-item__label {
font-size: 14px;
}
.el-input,
.el-textarea {
font-size: 14px;
}
.dialog-footer {
flex-direction: column;
}
.dialog-footer .el-button {
width: 100%;
}
}
@media screen and (max-width: 480px) {
.page-title {
font-size: 18px;
}
.el-table {
font-size: 12px;
}
.el-table th,
.el-table td {
padding: 6px;
}
.el-form-item__label {
font-size: 12px;
}
.el-input,
.el-textarea {
font-size: 12px;
}
.el-button {
font-size: 12px;
padding: 6px 12px;
}
}
</style>

822
src/views/content/List.vue Normal file
View File

@ -0,0 +1,822 @@
<template>
<div class="content-list-container">
<h2 class="page-title">内容管理</h2>
<!-- 搜索和操作栏 -->
<el-card class="mb-20">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="标题" class="search-form-item">
<el-input v-model="searchForm.title" placeholder="请输入内容标题"></el-input>
</el-form-item>
<el-form-item label="作者" class="search-form-item">
<el-input v-model="searchForm.authorName" placeholder="请输入作者名称"></el-input>
</el-form-item>
<el-form-item label="状态" class="search-form-item">
<el-select v-model="searchForm.auditStatus" placeholder="请选择审核状态">
<el-option label="全部" value=""></el-option>
<el-option label="待审核" value="0"></el-option>
<el-option label="审核通过" value="1"></el-option>
<el-option label="审核拒绝" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item class="search-form-item">
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">新增内容</el-button>
<el-button type="success" @click="handleImportDialog">导入ZIP</el-button>
</div>
</el-card>
<!-- 内容列表 -->
<el-card>
<div class="table-container">
<el-table :data="contentList" style="width: 100%" class="responsive-table">
<el-table-column prop="contentId" label="内容ID" width="100"></el-table-column>
<el-table-column prop="title" label="标题">
<template #default="scope">
<el-tooltip :content="scope.row.title" placement="top" :disabled="scope.row.title.length <= 20">
<span class="ellipsis">{{ scope.row.title }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="标签">
<template #default="scope">
<div class="tags-container">
<el-tag size="small" v-for="tag in scope.row.tagNames" :key="tag" class="mr-1">{{ tag }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="description" label="描述">
<template #default="scope">
<el-tooltip :content="scope.row.description" placement="top" :disabled="scope.row.description && scope.row.description.length <= 50">
<span class="ellipsis">{{ scope.row.description || '-' }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="isOfficial" label="标识" width="150">
<template #default="scope">
<div class="badges-container">
<el-tag v-if="scope.row.isOfficial" type="primary" size="small" class="mr-1">官方认证</el-tag>
<el-tag v-if="scope.row.isFree" type="success" size="small">免费</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="stats" label="统计" width="150">
<template #default="scope">
<div class="stats-container">
<span class="stat-item">使用: {{ scope.row.usageCount || 0 }}</span>
<span class="stat-item">收藏: {{ scope.row.favoriteCount || 0 }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="authorName" label="作者" width="120"></el-table-column>
<el-table-column prop="contentType" label="类型" width="100">
<template #default="scope">
<el-tag>{{ scope.row.contentType === 1 ? '文章' : '视频' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="auditStatus" label="审核状态" width="120">
<template #default="scope">
<el-tag :type="getAuditStatusType(scope.row.auditStatus)">
{{ getAuditStatusText(scope.row.auditStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="publishStatus" label="发布状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.publishStatus === 1 ? 'success' : 'info'">{{ scope.row.publishStatus === 1 ? '已发布' : '未发布' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="250">
<template #default="scope">
<div class="action-buttons">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="success" size="small" v-if="scope.row.auditStatus === 0" @click="handleAudit(scope.row, 1)">审核通过</el-button>
<el-button type="danger" size="small" v-if="scope.row.auditStatus === 0" @click="handleAudit(scope.row, 2)">审核拒绝</el-button>
<el-button type="warning" size="small" v-if="scope.row.auditStatus === 1 && scope.row.publishStatus === 0" @click="handlePublish(scope.row)">发布</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row.contentId)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.pageNum"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
<!-- 新增/编辑内容弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
:width="dialogWidth"
>
<el-form :model="contentForm" :rules="rules" ref="contentFormRef" label-width="100px" class="full-width-form">
<el-form-item label="标题" prop="title">
<el-input v-model="contentForm.title" placeholder="请输入内容标题" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="类型" prop="contentType">
<el-select v-model="contentForm.contentType" placeholder="请选择内容类型" class="full-width-input">
<el-option label="文章" value="1"></el-option>
<el-option label="视频" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item label="分类" prop="categoryId">
<el-select v-model="contentForm.categoryId" placeholder="请选择分类" class="full-width-input">
<el-option v-for="category in categories" :key="category.categoryId" :label="category.categoryName" :value="category.categoryId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="标签" prop="tagIdList">
<el-select v-model="contentForm.tagIdList" multiple placeholder="请选择标签" class="full-width-input">
<el-option v-for="tag in tags" :key="tag.tagId" :label="tag.tagName" :value="tag.tagId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="封面图" prop="coverImage">
<el-upload
class="upload-demo"
action="http://localhost:19001/api/upload"
:on-success="handleUploadSuccess"
:file-list="fileList"
list-type="picture"
>
<el-button type="primary">上传封面</el-button>
</el-upload>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="contentForm.description" type="textarea" :rows="3" placeholder="请输入内容描述" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input v-model="contentForm.content" type="textarea" :rows="10" placeholder="请输入内容" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="是否官方">
<el-switch v-model="contentForm.isOfficial"></el-switch>
</el-form-item>
<el-form-item label="是否免费">
<el-switch v-model="contentForm.isFree"></el-switch>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 导入ZIP弹窗 -->
<el-dialog
v-model="importDialogVisible"
title="导入ZIP文件"
:width="dialogWidth"
>
<el-alert
title="请上传包含Excel文件的ZIP压缩包"
type="info"
:closable="false"
show-icon
class="mb-20"
>
<template #default>
<p>系统将从ZIP文件中提取所有Excel文件并批量导入到内容管理系统</p>
</template>
</el-alert>
<el-form label-width="100px">
<el-form-item label="创建人" required>
<el-input v-model="importForm.createBy" placeholder="请输入创建人姓名" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="ZIP文件" required>
<el-upload
ref="uploadRef"
class="upload-demo"
:auto-upload="false"
:limit="1"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
accept=".zip"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖拽文件到此处或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
只能上传zip格式文件
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="importDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleImportSubmit" :loading="importLoading">开始导入</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, onUnmounted } from 'vue'
import { contentApi, tagApi } from '../../api/content'
import { ElMessage, ElMessageBox } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
const contentList = ref([])
const total = ref(0)
const pageInfo = reactive({
pageNum: 1,
pageSize: 10
})
const searchForm = reactive({
title: '',
authorName: '',
auditStatus: ''
})
//
const allTags = ref([])
const tagMap = ref(new Map())
const dialogVisible = ref(false)
const dialogTitle = ref('新增内容')
const isEdit = ref(false)
const contentFormRef = ref(null)
const contentForm = reactive({
contentId: '',
title: '',
content: '',
contentType: '1',
categoryId: '',
tagIdList: [],
tags: [],
description: '',
coverImage: '',
isOfficial: false,
isFree: false,
usageCount: 0,
favoriteCount: 0
})
const fileList = ref([])
const categories = ref([
{ categoryId: 1, categoryName: '技术文章' },
{ categoryId: 2, categoryName: '教程视频' },
{ categoryId: 3, categoryName: '行业资讯' }
])
const tags = ref([
{ tagId: 1, tagName: 'Java' },
{ tagId: 2, tagName: 'Vue' },
{ tagId: 3, tagName: 'Spring Boot' },
{ tagId: 4, tagName: '前端' },
{ tagId: 5, tagName: '后端' }
])
//
const importDialogVisible = ref(false)
const importLoading = ref(false)
const uploadRef = ref(null)
const selectedFile = ref(null)
const importForm = reactive({
createBy: ''
})
const rules = {
title: [
{ required: true, message: '请输入内容标题', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入内容', trigger: 'blur' }
],
contentType: [
{ required: true, message: '请选择内容类型', trigger: 'change' }
],
categoryId: [
{ required: true, message: '请选择分类', trigger: 'change' }
]
}
//
const dialogWidth = computed(() => {
return window.innerWidth < 768 ? '90%' : '800px'
})
const getAuditStatusText = (status) => {
switch (status) {
case 0: return '待审核'
case 1: return '审核通过'
case 2: return '审核拒绝'
default: return '未知'
}
}
const getAuditStatusType = (status) => {
switch (status) {
case 0: return 'warning'
case 1: return 'success'
case 2: return 'danger'
default: return ''
}
}
//
const loadTags = async () => {
try {
const response = await tagApi.getList()
allTags.value = response.data
// tagId
tagMap.value = new Map()
response.data.forEach(tag => {
tagMap.value.set(tag.tagId.toString(), tag.tagName)
})
} catch (error) {
ElMessage.error('获取标签列表失败')
}
}
const loadContentList = async () => {
try {
const params = {
...searchForm,
...pageInfo
}
const response = await contentApi.getPageList(params)
//
const list = response.data.list.map(item => {
//
if (item.tags) {
const tagIds = item.tags.split(',').map(id => id.trim()).filter(id => id)
item.tagNames = tagIds.map(tagId => {
// IDtagMap
return tagMap.value.get(tagId.toString()) || tagId
})
} else {
item.tagNames = []
}
return item
})
contentList.value = list
total.value = response.data.total
} catch (error) {
ElMessage.error('获取内容列表失败')
}
}
const handleSearch = () => {
pageInfo.pageNum = 1
loadContentList()
}
const resetSearch = () => {
searchForm.title = ''
searchForm.authorName = ''
searchForm.auditStatus = ''
pageInfo.pageNum = 1
loadContentList()
}
const handleSizeChange = (size) => {
pageInfo.pageSize = size
loadContentList()
}
const handleCurrentChange = (current) => {
pageInfo.pageNum = current
loadContentList()
}
const handleAdd = () => {
isEdit.value = false
dialogTitle.value = '新增内容'
Object.assign(contentForm, {
contentId: '',
title: '',
content: '',
contentType: '1',
categoryId: '',
tagIdList: [],
tags: [],
description: '',
coverImage: '',
isOfficial: false,
isFree: false,
usageCount: 0,
favoriteCount: 0
})
fileList.value = []
dialogVisible.value = true
}
const handleEdit = (row) => {
isEdit.value = true
dialogTitle.value = '编辑内容'
Object.assign(contentForm, row)
if (row.coverImage) {
fileList.value = [{
name: 'cover.jpg',
url: row.coverImage
}]
} else {
fileList.value = []
}
dialogVisible.value = true
}
const handleUploadSuccess = (response) => {
if (response.status === 1000) {
contentForm.coverImage = response.data.fileUrl
fileList.value = [{
name: response.data.fileName,
url: response.data.fileUrl
}]
ElMessage.success('上传成功')
} else {
ElMessage.error('上传失败')
}
}
const handleSubmit = async () => {
if (!contentFormRef.value) return
await contentFormRef.value.validate(async (valid) => {
if (valid) {
try {
let response
if (isEdit.value) {
response = await contentApi.update(contentForm)
} else {
response = await contentApi.insert(contentForm)
}
ElMessage.success(isEdit.value ? '更新成功' : '新增成功')
dialogVisible.value = false
loadContentList()
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
const handleDelete = async (contentId) => {
try {
await ElMessageBox.confirm('确定要删除该内容吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await contentApi.deleteById(contentId)
ElMessage.success('删除成功')
loadContentList()
} catch (error) {
//
}
}
const handleAudit = async (row, status) => {
try {
const response = await contentApi.audit({
contentId: row.contentId,
auditStatus: status
})
ElMessage.success(status === 1 ? '审核通过' : '审核拒绝')
loadContentList()
} catch (error) {
ElMessage.error('审核失败')
}
}
const handlePublish = async (row) => {
try {
await contentApi.publish({
contentId: row.contentId,
publishStatus: 1
})
ElMessage.success('发布成功')
loadContentList()
} catch (error) {
ElMessage.error('发布失败')
}
}
//
const handleImportDialog = () => {
importForm.createBy = ''
selectedFile.value = null
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
importDialogVisible.value = true
}
//
const handleFileChange = (file, fileList) => {
//
const fileName = file.name.toLowerCase()
if (!fileName.endsWith('.zip')) {
ElMessage.error('只能上传ZIP格式的文件')
//
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
selectedFile.value = null
return
}
selectedFile.value = file.raw
}
//
const handleFileRemove = () => {
selectedFile.value = null
}
//
const handleImportSubmit = async () => {
//
if (!importForm.createBy) {
ElMessage.warning('请输入创建人姓名')
return
}
if (!selectedFile.value) {
ElMessage.warning('请选择ZIP文件')
return
}
try {
importLoading.value = true
// FormData
const formData = new FormData()
formData.append('file', selectedFile.value)
formData.append('createBy', importForm.createBy)
const response = await contentApi.importFromZip(formData)
if (response.status === 1000 || response.code === 200) {
const successCount = response.data
ElMessage.success(`导入成功!共导入 ${successCount} 条记录`)
importDialogVisible.value = false
loadContentList()
} else {
ElMessage.error(response.message || '导入失败')
}
} catch (error) {
console.error('导入错误:', error)
ElMessage.error('导入失败:' + (error.message || '未知错误'))
} finally {
importLoading.value = false
}
}
//
const handleResize = () => {
//
}
onMounted(async () => {
await loadTags() //
loadContentList() //
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.content-list-container {
padding: 0;
}
.page-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
.mb-20 {
margin-bottom: 20px;
}
.search-form {
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.search-form-item {
margin-bottom: 10px;
}
.action-bar {
margin-top: 10px;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
.responsive-table {
min-width: 900px;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
}
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.badges-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.stats-container {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-item {
font-size: 12px;
color: #666;
}
.mr-1 {
margin-right: 5px;
}
.full-width-form {
width: 100%;
}
.full-width-input {
width: 100%;
}
.upload-demo {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.page-title {
font-size: 20px;
margin-bottom: 15px;
}
.search-form {
flex-direction: column;
align-items: stretch;
}
.search-form-item {
width: 100%;
}
.search-form-item .el-input,
.search-form-item .el-select {
width: 100%;
}
.action-bar {
margin-top: 15px;
}
.action-buttons {
flex-direction: column;
align-items: stretch;
}
.action-buttons .el-button {
width: 100%;
margin-bottom: 5px;
}
.pagination-container {
justify-content: center;
}
.el-pagination {
flex-wrap: wrap;
}
.el-table {
font-size: 14px;
}
.el-table th,
.el-table td {
padding: 8px;
}
.el-form-item {
margin-bottom: 15px;
}
.el-form-item__label {
font-size: 14px;
}
.el-input,
.el-select,
.el-textarea {
font-size: 14px;
}
.upload-demo {
margin-bottom: 15px;
}
.dialog-footer {
flex-direction: column;
}
.dialog-footer .el-button {
width: 100%;
}
}
@media screen and (max-width: 480px) {
.page-title {
font-size: 18px;
}
.el-table {
font-size: 12px;
}
.el-table th,
.el-table td {
padding: 6px;
}
.el-form-item__label {
font-size: 12px;
}
.el-input,
.el-select,
.el-textarea {
font-size: 12px;
}
.el-button {
font-size: 12px;
padding: 6px 12px;
}
}
</style>

484
src/views/dict/List.vue Normal file
View File

@ -0,0 +1,484 @@
<template>
<div class="dict-list-container">
<h2 class="page-title">字典管理</h2>
<!-- 搜索和操作栏 -->
<el-card class="mb-20">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="字典类型" class="search-form-item">
<el-select v-model="searchForm.dictType" placeholder="请选择字典类型">
<el-option label="全部" value=""></el-option>
<el-option v-for="type in dictTypes" :key="type" :label="type" :value="type"></el-option>
</el-select>
</el-form-item>
<el-form-item label="字典键值" class="search-form-item">
<el-input v-model="searchForm.dictKey" placeholder="请输入字典键值"></el-input>
</el-form-item>
<el-form-item class="search-form-item">
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">新增字典</el-button>
</div>
</el-card>
<!-- 字典列表 -->
<el-card>
<div class="table-container">
<el-table :data="dictList" style="width: 100%" class="responsive-table">
<el-table-column prop="dictId" label="字典ID" width="100"></el-table-column>
<el-table-column prop="dictType" label="字典类型" width="150"></el-table-column>
<el-table-column prop="dictKey" label="字典键值" width="150"></el-table-column>
<el-table-column prop="dictValue" label="字典值">
<template #default="scope">
<el-tooltip :content="scope.row.dictValue" placement="top" :disabled="scope.row.dictValue.length <= 20">
<span class="ellipsis">{{ scope.row.dictValue }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="80"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="1"
inactive-value="0"
@change="handleStatusChange(scope.row)"
></el-switch>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<div class="action-buttons">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row.dictId)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.pageNum"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
<!-- 新增/编辑字典弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
:width="dialogWidth"
>
<el-form :model="dictForm" :rules="rules" ref="dictFormRef" label-width="80px" class="full-width-form">
<el-form-item label="字典类型" prop="dictType">
<el-select v-model="dictForm.dictType" placeholder="请选择字典类型" class="full-width-input">
<el-option v-for="type in dictTypes" :key="type" :label="type" :value="type"></el-option>
</el-select>
</el-form-item>
<el-form-item label="字典键值" prop="dictKey">
<el-input v-model="dictForm.dictKey" placeholder="请输入字典键值" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="字典值" prop="dictValue">
<el-input v-model="dictForm.dictValue" placeholder="请输入字典值" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model.number="dictForm.sort" placeholder="请输入排序值" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="dictForm.status" active-value="1" inactive-value="0"></el-switch>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="dictForm.remark" type="textarea" placeholder="请输入备注" class="full-width-input"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, onUnmounted } from 'vue'
import { dictApi } from '../../api/dict'
import { ElMessage, ElMessageBox } from 'element-plus'
const dictList = ref([])
const total = ref(0)
const pageInfo = reactive({
pageNum: 1,
pageSize: 10
})
const searchForm = reactive({
dictType: '',
dictKey: ''
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增字典')
const isEdit = ref(false)
const dictFormRef = ref(null)
const dictTypes = ref(['user_status', 'order_status', 'content_type', 'pay_type'])
const dictForm = reactive({
dictId: '',
dictType: '',
dictKey: '',
dictValue: '',
sort: 0,
status: '1',
remark: ''
})
const rules = {
dictType: [
{ required: true, message: '请选择字典类型', trigger: 'change' }
],
dictKey: [
{ required: true, message: '请输入字典键值', trigger: 'blur' }
],
dictValue: [
{ required: true, message: '请输入字典值', trigger: 'blur' }
],
sort: [
{ required: true, message: '请输入排序值', trigger: 'blur' },
{ type: 'number', message: '请输入正确的排序值', trigger: 'blur' }
]
}
//
const dialogWidth = computed(() => {
return window.innerWidth < 768 ? '90%' : '500px'
})
const loadDictList = async () => {
try {
const params = {
...searchForm,
...pageInfo
}
const response = await dictApi.getPageList(params)
dictList.value = response.data.list
total.value = response.data.total
} catch (error) {
ElMessage.error('获取字典列表失败')
}
}
const loadDictTypes = async () => {
try {
const response = await dictApi.getDictTypes()
dictTypes.value = response.data
} catch (error) {
console.error('获取字典类型失败')
}
}
const handleSearch = () => {
pageInfo.pageNum = 1
loadDictList()
}
const resetSearch = () => {
searchForm.dictType = ''
searchForm.dictKey = ''
pageInfo.pageNum = 1
loadDictList()
}
const handleSizeChange = (size) => {
pageInfo.pageSize = size
loadDictList()
}
const handleCurrentChange = (current) => {
pageInfo.pageNum = current
loadDictList()
}
const handleAdd = () => {
isEdit.value = false
dialogTitle.value = '新增字典'
Object.assign(dictForm, {
dictId: '',
dictType: '',
dictKey: '',
dictValue: '',
sort: 0,
status: '1',
remark: ''
})
dialogVisible.value = true
}
const handleEdit = (row) => {
isEdit.value = true
dialogTitle.value = '编辑字典'
Object.assign(dictForm, row)
dialogVisible.value = true
}
const handleSubmit = async () => {
if (!dictFormRef.value) return
await dictFormRef.value.validate(async (valid) => {
if (valid) {
try {
let response
if (isEdit.value) {
response = await dictApi.update(dictForm)
} else {
response = await dictApi.insert(dictForm)
}
ElMessage.success(isEdit.value ? '更新成功' : '新增成功')
dialogVisible.value = false
loadDictList()
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
const handleDelete = async (dictId) => {
try {
await ElMessageBox.confirm('确定要删除该字典吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await dictApi.deleteById(dictId)
ElMessage.success('删除成功')
loadDictList()
} catch (error) {
//
}
}
const handleStatusChange = async (row) => {
try {
const response = await dictApi.update(row)
if (response.code !== 200) {
ElMessage.error('更新状态失败')
//
row.status = row.status === '1' ? '0' : '1'
}
} catch (error) {
ElMessage.error('更新状态失败')
row.status = row.status === '1' ? '0' : '1'
}
}
//
const handleResize = () => {
//
}
onMounted(() => {
loadDictTypes()
loadDictList()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.dict-list-container {
padding: 0;
}
.page-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
.mb-20 {
margin-bottom: 20px;
}
.search-form {
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.search-form-item {
margin-bottom: 10px;
}
.action-bar {
margin-top: 10px;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
.responsive-table {
min-width: 800px;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
}
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.full-width-form {
width: 100%;
}
.full-width-input {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.page-title {
font-size: 20px;
margin-bottom: 15px;
}
.search-form {
flex-direction: column;
align-items: stretch;
}
.search-form-item {
width: 100%;
}
.search-form-item .el-input,
.search-form-item .el-select {
width: 100%;
}
.action-bar {
margin-top: 15px;
}
.action-buttons {
flex-direction: column;
align-items: stretch;
}
.action-buttons .el-button {
width: 100%;
margin-bottom: 5px;
}
.pagination-container {
justify-content: center;
}
.el-pagination {
flex-wrap: wrap;
}
.el-table {
font-size: 14px;
}
.el-table th,
.el-table td {
padding: 8px;
}
.el-form-item {
margin-bottom: 15px;
}
.el-form-item__label {
font-size: 14px;
}
.el-input,
.el-select,
.el-textarea {
font-size: 14px;
}
.dialog-footer {
flex-direction: column;
}
.dialog-footer .el-button {
width: 100%;
}
}
@media screen and (max-width: 480px) {
.page-title {
font-size: 18px;
}
.el-table {
font-size: 12px;
}
.el-table th,
.el-table td {
padding: 6px;
}
.el-form-item__label {
font-size: 12px;
}
.el-input,
.el-select,
.el-textarea {
font-size: 12px;
}
.el-button {
font-size: 12px;
padding: 6px 12px;
}
}
</style>

415
src/views/order/List.vue Normal file
View File

@ -0,0 +1,415 @@
<template>
<div class="order-list-container">
<h2 class="page-title">订单管理</h2>
<!-- 搜索和操作栏 -->
<el-card class="mb-20">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="订单号" class="search-form-item">
<el-input v-model="searchForm.orderNo" placeholder="请输入订单号"></el-input>
</el-form-item>
<el-form-item label="用户ID" class="search-form-item">
<el-input v-model="searchForm.userId" placeholder="请输入用户ID"></el-input>
</el-form-item>
<el-form-item label="订单状态" class="search-form-item">
<el-select v-model="searchForm.status" placeholder="请选择订单状态">
<el-option label="全部" value=""></el-option>
<el-option label="待支付" value="1"></el-option>
<el-option label="已支付" value="2"></el-option>
<el-option label="支付失败" value="3"></el-option>
<el-option label="已取消" value="4"></el-option>
<el-option label="已退款" value="5"></el-option>
</el-select>
</el-form-item>
<el-form-item class="search-form-item">
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 订单列表 -->
<el-card>
<div class="table-container">
<el-table :data="orderList" style="width: 100%" class="responsive-table">
<el-table-column prop="orderId" label="订单ID" width="120"></el-table-column>
<el-table-column prop="orderNo" label="订单号">
<template #default="scope">
<el-tooltip :content="scope.row.orderNo" placement="top" :disabled="scope.row.orderNo.length <= 15">
<span class="ellipsis">{{ scope.row.orderNo }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="userId" label="用户ID" width="100"></el-table-column>
<el-table-column prop="userName" label="用户名" width="120"></el-table-column>
<el-table-column prop="amount" label="金额" width="100">
<template #default="scope">
¥{{ scope.row.amount.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="payType" label="支付方式" width="120">
<template #default="scope">
<el-tag>{{ scope.row.payType === 1 ? '微信' : scope.row.payType === 2 ? '支付宝' : '其他' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="订单状态" width="120">
<template #default="scope">
<el-tag :type="getOrderStatusType(scope.row.status)">
{{ getOrderStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="productName" label="商品名称" width="150">
<template #default="scope">
<el-tooltip :content="scope.row.productName" placement="top" :disabled="scope.row.productName?.length <= 20">
<span class="ellipsis">{{ scope.row.productName || '无' }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="productDesc" label="商品描述" width="200">
<template #default="scope">
<el-tooltip :content="scope.row.productDesc" placement="top" :disabled="scope.row.productDesc?.length <= 30">
<span class="ellipsis">{{ scope.row.productDesc || '无' }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<div class="action-buttons">
<el-button type="primary" size="small" @click="handleDetail(scope.row)">详情</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.pageNum"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
<!-- 订单详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="订单详情"
:width="dialogWidth"
>
<el-descriptions :column="1" border>
<el-descriptions-item label="订单ID">{{ currentOrder.orderId }}</el-descriptions-item>
<el-descriptions-item label="订单号">{{ currentOrder.orderNo }}</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ currentOrder.userId }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ currentOrder.userName }}</el-descriptions-item>
<el-descriptions-item label="金额">¥{{ currentOrder.amount?.toFixed(2) || '0.00' }}</el-descriptions-item>
<el-descriptions-item label="支付方式">{{ currentOrder.payType === 1 ? '微信' : currentOrder.payType === 2 ? '支付宝' : '其他' }}</el-descriptions-item>
<el-descriptions-item label="订单状态">{{ getOrderStatusText(currentOrder.status) }}</el-descriptions-item>
<el-descriptions-item label="商品名称">{{ currentOrder.productName || '无' }}</el-descriptions-item>
<el-descriptions-item label="商品描述">{{ currentOrder.productDesc || '无' }}</el-descriptions-item>
<el-descriptions-item label="业务类型">{{ currentOrder.businessType || '无' }}</el-descriptions-item>
<el-descriptions-item label="业务ID">{{ currentOrder.businessId || '无' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ currentOrder.createTime }}</el-descriptions-item>
<el-descriptions-item label="支付时间">{{ currentOrder.payTime || '未支付' }}</el-descriptions-item>
<el-descriptions-item label="过期时间">{{ currentOrder.expireTime || '无' }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ currentOrder.remark || '无' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<span class="dialog-footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, onUnmounted } from 'vue'
import { orderApi } from '../../api/order'
import { ElMessage, ElMessageBox } from 'element-plus'
const orderList = ref([])
const total = ref(0)
const pageInfo = reactive({
pageNum: 1,
pageSize: 10
})
const searchForm = reactive({
orderNo: '',
userId: '',
status: ''
})
const detailDialogVisible = ref(false)
const currentOrder = ref({})
//
const dialogWidth = computed(() => {
return window.innerWidth < 768 ? '90%' : '600px'
})
const smallDialogWidth = computed(() => {
return window.innerWidth < 768 ? '90%' : '400px'
})
const getOrderStatusText = (status) => {
switch (status) {
case 1: return '待支付'
case 2: return '已支付'
case 3: return '支付失败'
case 4: return '已取消'
case 5: return '已退款'
default: return '未知'
}
}
const getOrderStatusType = (status) => {
switch (status) {
case 1: return 'warning'
case 2: return 'success'
case 3: return 'danger'
case 4: return 'info'
case 5: return 'info'
default: return ''
}
}
const loadOrderList = async () => {
try {
const params = {
...searchForm,
...pageInfo
}
const response = await orderApi.getPageList(params)
orderList.value = response.data.list
total.value = response.data.total
} catch (error) {
ElMessage.error('获取订单列表失败')
}
}
const handleSearch = () => {
pageInfo.pageNum = 1
loadOrderList()
}
const resetSearch = () => {
searchForm.orderNo = ''
searchForm.userId = ''
searchForm.status = ''
pageInfo.pageNum = 1
loadOrderList()
}
const handleSizeChange = (size) => {
pageInfo.pageSize = size
loadOrderList()
}
const handleCurrentChange = (current) => {
pageInfo.pageNum = current
loadOrderList()
}
const handleDetail = async (row) => {
try {
const response = await orderApi.queryById(row.orderId)
currentOrder.value = response.data
detailDialogVisible.value = true
} catch (error) {
ElMessage.error('获取订单详情失败')
}
}
//
const handleResize = () => {
//
}
onMounted(() => {
loadOrderList()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.order-list-container {
padding: 0;
}
.page-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
.mb-20 {
margin-bottom: 20px;
}
.search-form {
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.search-form-item {
margin-bottom: 10px;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
.responsive-table {
min-width: 900px;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
}
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.full-width-form {
width: 100%;
}
.full-width-input {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.page-title {
font-size: 20px;
margin-bottom: 15px;
}
.search-form {
flex-direction: column;
align-items: stretch;
}
.search-form-item {
width: 100%;
}
.search-form-item .el-input,
.search-form-item .el-select {
width: 100%;
}
.action-buttons {
flex-direction: column;
align-items: stretch;
}
.action-buttons .el-button {
width: 100%;
margin-bottom: 5px;
}
.pagination-container {
justify-content: center;
}
.el-pagination {
flex-wrap: wrap;
}
.el-table {
font-size: 14px;
}
.el-table th,
.el-table td {
padding: 8px;
}
.el-form-item {
margin-bottom: 15px;
}
.el-form-item__label {
font-size: 14px;
}
.el-input,
.el-textarea {
font-size: 14px;
}
.dialog-footer {
flex-direction: column;
}
.dialog-footer .el-button {
width: 100%;
}
}
@media screen and (max-width: 480px) {
.page-title {
font-size: 18px;
}
.el-table {
font-size: 12px;
}
.el-table th,
.el-table td {
padding: 6px;
}
.el-form-item__label {
font-size: 12px;
}
.el-input,
.el-textarea {
font-size: 12px;
}
.el-button {
font-size: 12px;
padding: 6px 12px;
}
}
</style>

547
src/views/role/List.vue Normal file
View File

@ -0,0 +1,547 @@
<template>
<div class="role-list-container">
<h2 class="page-title">角色管理</h2>
<!-- 搜索和操作栏 -->
<el-card class="mb-20">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="角色名称" class="search-form-item">
<el-input v-model="searchForm.roleName" placeholder="请输入角色名称"></el-input>
</el-form-item>
<el-form-item class="search-form-item">
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<div class="action-bar">
<el-button type="primary" @click="handleAdd">新增角色</el-button>
</div>
</el-card>
<!-- 角色列表 -->
<el-card>
<div class="table-container">
<el-table :data="roleList" style="width: 100%" class="responsive-table">
<el-table-column prop="roleId" label="角色ID" width="100"></el-table-column>
<el-table-column prop="roleName" label="角色名称" width="150"></el-table-column>
<el-table-column prop="description" label="角色描述">
<template #default="scope">
<el-tooltip :content="scope.row.description" placement="top" :disabled="scope.row.description.length <= 20">
<span class="ellipsis">{{ scope.row.description }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="1"
inactive-value="0"
@change="handleStatusChange(scope.row)"
></el-switch>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<div class="action-buttons">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="success" size="small" @click="handleAssignPermissions(scope.row.roleId)">分配权限</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row.roleId)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.pageNum"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</el-card>
<!-- 新增/编辑角色弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
:width="dialogWidth"
>
<el-form :model="roleForm" :rules="rules" ref="roleFormRef" label-width="80px" class="full-width-form">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="roleForm.roleName" placeholder="请输入角色名称" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="角色描述" prop="description">
<el-input v-model="roleForm.description" placeholder="请输入角色描述" type="textarea" class="full-width-input"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="roleForm.status" active-value="1" inactive-value="0"></el-switch>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 权限分配弹窗 -->
<el-dialog
v-model="permissionDialogVisible"
title="分配权限"
:width="dialogWidth"
>
<div class="permission-tree-container">
<el-tree
:data="permissionTree"
show-checkbox
node-key="permissionId"
ref="permissionTreeRef"
default-expand-all
></el-tree>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="permissionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSavePermissions">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, onUnmounted } from 'vue'
import { roleApi } from '../../api/role'
import { ElMessage, ElMessageBox } from 'element-plus'
const roleList = ref([])
const total = ref(0)
const pageInfo = reactive({
pageNum: 1,
pageSize: 10
})
const searchForm = reactive({
roleName: ''
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增角色')
const isEdit = ref(false)
const roleFormRef = ref(null)
const roleForm = reactive({
roleId: '',
roleName: '',
description: '',
status: '1'
})
const permissionDialogVisible = ref(false)
const currentRoleId = ref('')
const permissionTreeRef = ref(null)
const permissionTree = ref([])
const rules = {
roleName: [
{ required: true, message: '请输入角色名称', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入角色描述', trigger: 'blur' }
]
}
//
const dialogWidth = computed(() => {
return window.innerWidth < 768 ? '90%' : '500px'
})
const loadRoleList = async () => {
try {
const params = {
...searchForm,
...pageInfo
}
const response = await roleApi.getPageList(params)
roleList.value = response.data.list
total.value = response.data.total
} catch (error) {
ElMessage.error('获取角色列表失败')
}
}
const handleSearch = () => {
pageInfo.pageNum = 1
loadRoleList()
}
const resetSearch = () => {
searchForm.roleName = ''
pageInfo.pageNum = 1
loadRoleList()
}
const handleSizeChange = (size) => {
pageInfo.pageSize = size
loadRoleList()
}
const handleCurrentChange = (current) => {
pageInfo.pageNum = current
loadRoleList()
}
const handleAdd = () => {
isEdit.value = false
dialogTitle.value = '新增角色'
Object.assign(roleForm, {
roleId: '',
roleName: '',
description: '',
status: '1'
})
dialogVisible.value = true
}
const handleEdit = (row) => {
isEdit.value = true
dialogTitle.value = '编辑角色'
Object.assign(roleForm, row)
dialogVisible.value = true
}
const handleSubmit = async () => {
if (!roleFormRef.value) return
await roleFormRef.value.validate(async (valid) => {
if (valid) {
try {
let response
if (isEdit.value) {
response = await roleApi.update(roleForm)
} else {
response = await roleApi.insert(roleForm)
}
ElMessage.success(isEdit.value ? '更新成功' : '新增成功')
dialogVisible.value = false
loadRoleList()
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
const handleDelete = async (roleId) => {
try {
await ElMessageBox.confirm('确定要删除该角色吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await roleApi.deleteById(roleId)
ElMessage.success('删除成功')
loadRoleList()
} catch (error) {
//
}
}
const handleStatusChange = async (row) => {
try {
await roleApi.update(row)
} catch (error) {
ElMessage.error('更新状态失败')
row.status = row.status === '1' ? '0' : '1'
}
}
const handleAssignPermissions = async (roleId) => {
currentRoleId.value = roleId
try {
//
// API使
permissionTree.value = [
{
permissionId: 1,
permissionName: '系统管理',
children: [
{ permissionId: 11, permissionName: '用户管理' },
{ permissionId: 12, permissionName: '角色管理' },
{ permissionId: 13, permissionName: '字典管理' }
]
},
{
permissionId: 2,
permissionName: '内容管理',
children: [
{ permissionId: 21, permissionName: '内容列表' },
{ permissionId: 22, permissionName: '分类管理' },
{ permissionId: 23, permissionName: '标签管理' }
]
},
{
permissionId: 3,
permissionName: '订单管理',
children: [
{ permissionId: 31, permissionName: '订单列表' },
{ permissionId: 32, permissionName: '退款管理' }
]
},
{
permissionId: 4,
permissionName: '账户管理',
children: [
{ permissionId: 41, permissionName: '账户列表' },
{ permissionId: 42, permissionName: '交易记录' }
]
}
]
//
const response = await roleApi.getRolePermissions(roleId)
if (response.code === 200) {
const permissionIds = response.data
//
if (permissionTreeRef.value) {
permissionTreeRef.value.setCheckedKeys(permissionIds)
}
}
permissionDialogVisible.value = true
} catch (error) {
ElMessage.error('获取权限失败')
}
}
const handleSavePermissions = async () => {
if (!permissionTreeRef.value) return
const checkedKeys = permissionTreeRef.value.getCheckedKeys()
try {
await roleApi.assignPermissions({
roleId: currentRoleId.value,
permissionIds: checkedKeys
})
ElMessage.success('权限分配成功')
permissionDialogVisible.value = false
} catch (error) {
ElMessage.error('权限分配失败')
}
}
//
const handleResize = () => {
//
}
onMounted(() => {
loadRoleList()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.role-list-container {
padding: 0;
}
.page-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
.mb-20 {
margin-bottom: 20px;
}
.search-form {
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.search-form-item {
margin-bottom: 10px;
}
.action-bar {
margin-top: 10px;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
.responsive-table {
min-width: 800px;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
}
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.full-width-form {
width: 100%;
}
.full-width-input {
width: 100%;
}
.permission-tree-container {
max-height: 400px;
overflow-y: auto;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 响应式设计 */
@media screen and (max-width: 768px) {
.page-title {
font-size: 20px;
margin-bottom: 15px;
}
.search-form {
flex-direction: column;
align-items: stretch;
}
.search-form-item {
width: 100%;
}
.search-form-item .el-input {
width: 100%;
}
.action-bar {
margin-top: 15px;
}
.action-buttons {
flex-direction: column;
align-items: stretch;
}
.action-buttons .el-button {
width: 100%;
margin-bottom: 5px;
}
.pagination-container {
justify-content: center;
}
.el-pagination {
flex-wrap: wrap;
}
.el-table {
font-size: 14px;
}
.el-table th,
.el-table td {
padding: 8px;
}
.el-form-item {
margin-bottom: 15px;
}
.el-form-item__label {
font-size: 14px;
}
.el-input,
.el-textarea {
font-size: 14px;
}
.permission-tree-container {
max-height: 300px;
}
.dialog-footer {
flex-direction: column;
}
.dialog-footer .el-button {
width: 100%;
}
}
@media screen and (max-width: 480px) {
.page-title {
font-size: 18px;
}
.el-table {
font-size: 12px;
}
.el-table th,
.el-table td {
padding: 6px;
}
.el-form-item__label {
font-size: 12px;
}
.el-input,
.el-textarea {
font-size: 12px;
}
.el-button {
font-size: 12px;
padding: 6px 12px;
}
.permission-tree-container {
max-height: 250px;
}
}
</style>

678
src/views/user/List.vue Normal file
View File

@ -0,0 +1,678 @@
<template>
<div class="user-list-container">
<h2 class="page-title">用户管理</h2>
<!-- 搜索和操作栏 -->
<el-card class="mb-20">
<el-form :model="searchForm" :inline="true" class="search-form" :label-position="'top'">
<el-row :gutter="10">
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="用户名">
<el-input v-model="searchForm.userName" placeholder="请输入用户名" class="full-width"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="手机号">
<el-input v-model="searchForm.tel" placeholder="请输入手机号" class="full-width"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="8" :lg="12">
<el-form-item>
<el-button type="primary" @click="handleSearch" class="mr-2">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-button type="primary" @click="handleAdd" class="ml-auto">新增用户</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 用户列表 -->
<el-card>
<div class="table-container">
<el-table :data="userList" style="width: 100%" class="user-table">
<el-table-column prop="userId" label="用户ID" width="100" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="userName" label="用户名" width="150" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="tel" label="手机号" width="150" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="email" label="邮箱" width="200" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="realName" label="昵称" width="120" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-switch
v-model="scope.row.status"
active-value="1"
inactive-value="0"
@change="handleStatusChange(scope.row)"
size="small"
></el-switch>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" :show-overflow-tooltip="true"></el-table-column>
<el-table-column label="操作" width="450" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.row)" class="mr-1">编辑</el-button>
<el-button type="warning" size="small" @click="handleResetPassword(scope.row)" class="mr-1">重置密码</el-button>
<el-button type="success" size="small" @click="handleGiftBalance(scope.row)" class="mr-1">赠送资金</el-button>
<el-button type="info" size="small" @click="handleAccountDetail(scope.row)" class="mr-1">账户详情</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row.userId)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.pageNum"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
class="pagination"
></el-pagination>
</div>
</el-card>
<!-- 新增/编辑用户弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
:width="dialogWidth"
>
<el-form :model="userForm" :rules="rules" ref="userFormRef" label-width="80px">
<el-form-item label="用户名" prop="userName">
<el-input v-model="userForm.userName" placeholder="请输入用户名" class="full-width"></el-input>
</el-form-item>
<el-form-item label="密码" v-if="!isEdit">
<el-input v-model="userForm.password" type="password" placeholder="请输入密码" class="full-width"></el-input>
</el-form-item>
<el-form-item label="手机号" prop="tel">
<el-input v-model="userForm.tel" placeholder="请输入手机号" class="full-width"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userForm.email" placeholder="请输入邮箱" class="full-width"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="realName">
<el-input v-model="userForm.realName" placeholder="请输入昵称" class="full-width"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="userForm.status" active-value="1" inactive-value="0"></el-switch>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 重置密码弹窗 -->
<el-dialog
v-model="resetPasswordVisible"
title="重置密码"
width="400px"
>
<el-form :model="resetPasswordForm" :rules="resetPasswordRules" ref="resetPasswordFormRef" label-width="80px">
<el-form-item label="新密码" prop="password">
<el-input v-model="resetPasswordForm.password" type="password" placeholder="请输入新密码" class="full-width"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="resetPasswordForm.confirmPassword" type="password" placeholder="请确认新密码" class="full-width"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="resetPasswordVisible = false">取消</el-button>
<el-button type="primary" @click="handleResetPasswordSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 赠送资金弹窗 -->
<el-dialog
v-model="giftBalanceVisible"
title="赠送资金"
width="500px"
>
<el-form :model="giftBalanceForm" :rules="giftBalanceRules" ref="giftBalanceFormRef" label-width="100px">
<el-form-item label="用户名称" prop="userName">
<el-input v-model="giftBalanceForm.userName" disabled class="full-width"></el-input>
</el-form-item>
<el-form-item label="赠送金额" prop="amount">
<el-input v-model="giftBalanceForm.amount" type="number" placeholder="请输入赠送金额" class="full-width"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="giftBalanceVisible = false">取消</el-button>
<el-button type="primary" @click="handleGiftBalanceSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 账户详情弹窗 -->
<el-dialog
v-model="accountDetailVisible"
title="账户详情"
:width="dialogWidth"
>
<el-descriptions :column="1" border v-if="currentAccount.accountId">
<el-descriptions-item label="账户ID">{{ currentAccount.accountId }}</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ currentAccount.userId }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ currentAccount.userName }}</el-descriptions-item>
<el-descriptions-item label="余额">¥{{ currentAccount.balance?.toFixed(2) || '0.00' }}</el-descriptions-item>
<el-descriptions-item label="冻结金额">¥{{ currentAccount.frozenAmount?.toFixed(2) || '0.00' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ currentAccount.status === 1 ? '正常' : '冻结' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ currentAccount.createTime }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ currentAccount.updateTime }}</el-descriptions-item>
</el-descriptions>
<div v-else class="text-center">
<el-skeleton :rows="8" animated></el-skeleton>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="accountDetailVisible = false">关闭</el-button>
<el-button type="primary" @click="handleViewTransactions">查看交易记录</el-button>
</span>
</template>
</el-dialog>
<!-- 交易记录弹窗 -->
<el-dialog
v-model="transactionDialogVisible"
title="交易记录"
:width="dialogWidth"
>
<div class="table-container">
<el-table :data="transactionList" style="width: 100%" class="responsive-table">
<el-table-column prop="transactionId" label="交易ID" width="120"></el-table-column>
<el-table-column prop="transactionType" label="交易类型" width="120">
<template #default="scope">
<el-tag :type="getTransactionType(scope.row.transactionType)">
{{ getTransactionTypeText(scope.row.transactionType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="100">
<template #default="scope">
{{ scope.row.transactionType === 1 ? '+' : '-' }}¥{{ scope.row.amount.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="balance" label="余额" width="120">
<template #default="scope">
¥{{ scope.row.balance.toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="transactionTime" label="交易时间" width="180"></el-table-column>
<el-table-column prop="remark" label="备注">
<template #default="scope">
<el-tooltip :content="scope.row.remark" placement="top" :disabled="!scope.row.remark || scope.row.remark.length <= 10">
<span class="ellipsis">{{ scope.row.remark || '-' }}</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="transactionDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { userApi } from '../../api/user'
import { accountApi } from '../../api/account'
import { ElMessage, ElMessageBox } from 'element-plus'
import CryptoJS from 'crypto-js'
// 使crypto-jsmd5
const md5 = (str) => {
return CryptoJS.MD5(str).toString();
}
const userList = ref([])
const total = ref(0)
const pageInfo = reactive({
pageNum: 1,
pageSize: 10
})
const searchForm = reactive({
userName: '',
tel: ''
})
const dialogVisible = ref(false)
const dialogTitle = ref('新增用户')
const isEdit = ref(false)
const userFormRef = ref(null)
//
const resetPasswordVisible = ref(false)
const currentResetUser = ref(null)
const resetPasswordForm = reactive({
password: '',
confirmPassword: ''
})
const resetPasswordRules = {
password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{ validator: (rule, value, callback) => {
if (value !== resetPasswordForm.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}, trigger: 'blur' }
]
}
const resetPasswordFormRef = ref(null)
//
const giftBalanceVisible = ref(false)
const currentGiftUser = ref(null)
const giftBalanceForm = reactive({
userName: '',
amount: '',
transactionNo: '',
businessId: '',
businessType: '',
remark: ''
})
const giftBalanceRules = {
amount: [
{ required: true, message: '请输入赠送金额', trigger: 'blur' },
{ validator: (rule, value, callback) => {
if (isNaN(Number(value)) || Number(value) <= 0) {
callback(new Error('请输入有效的金额'))
} else {
callback()
}
}, trigger: 'blur' }
]
}
const giftBalanceFormRef = ref(null)
//
const accountDetailVisible = ref(false)
const transactionDialogVisible = ref(false)
const currentAccount = ref({})
const transactionList = ref([])
const userForm = reactive({
userId: '',
userName: '',
password: '',
tel: '',
email: '',
realName: '',
status: '1'
})
const dialogWidth = computed(() => {
return window.innerWidth < 768 ? '90%' : '500px'
})
const rules = {
userName: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
tel: [
{ required: true, message: '请输入手机号', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
realName: [
{ required: true, message: '请输入昵称', trigger: 'blur' }
]
}
const handleSearch = () => {
pageInfo.pageNum = 1
loadUserList()
}
const resetSearch = () => {
searchForm.userName = ''
searchForm.tel = ''
pageInfo.pageNum = 1
loadUserList()
}
const handleSizeChange = (size) => {
pageInfo.pageSize = size
loadUserList()
}
const handleCurrentChange = (current) => {
pageInfo.pageNum = current
loadUserList()
}
const handleAdd = () => {
isEdit.value = false
dialogTitle.value = '新增用户'
Object.assign(userForm, {
userId: '',
userName: '',
password: '',
tel: '',
email: '',
realName: '',
status: '1'
})
dialogVisible.value = true
}
const handleEdit = (row) => {
isEdit.value = true
dialogTitle.value = '编辑用户'
Object.assign(userForm, row)
dialogVisible.value = true
}
const handleSubmit = async () => {
if (!userFormRef.value) return
await userFormRef.value.validate(async (valid) => {
if (valid) {
try {
let response
if (isEdit.value) {
response = await userApi.update(userForm)
} else {
response = await userApi.insert(userForm)
}
ElMessage.success(isEdit.value ? '更新成功' : '新增成功')
dialogVisible.value = false
loadUserList()
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
const handleDelete = async (userId) => {
try {
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await userApi.deleteById(userId)
ElMessage.success('删除成功')
loadUserList()
} catch (error) {
//
}
}
//
const isInitialLoading = ref(true)
const handleStatusChange = async (row) => {
//
if (isInitialLoading.value) {
return
}
try {
await userApi.update(row)
} catch (error) {
ElMessage.error('更新状态失败')
row.status = row.status === '1' ? '0' : '1'
}
}
const loadUserList = async () => {
try {
const params = {
...searchForm,
...pageInfo
}
const response = await userApi.getPageList(params)
userList.value = response.data.list
total.value = response.data.total
// isInitialLoadingfalse
setTimeout(() => {
isInitialLoading.value = false
}, 100)
} catch (error) {
ElMessage.error('获取用户列表失败')
}
}
const handleResetPassword = (row) => {
currentResetUser.value = row
resetPasswordForm.password = ''
resetPasswordForm.confirmPassword = ''
resetPasswordVisible.value = true
}
const handleResetPasswordSubmit = async () => {
if (!resetPasswordFormRef.value) return
await resetPasswordFormRef.value.validate(async (valid) => {
if (valid) {
try {
// MD5
const encryptedPassword = md5(resetPasswordForm.password)
await userApi.resetPassword({
userId: currentResetUser.value.userId,
newPassword: encryptedPassword
})
ElMessage.success('密码重置成功')
resetPasswordVisible.value = false
} catch (error) {
ElMessage.error('密码重置失败')
}
}
})
}
const handleGiftBalance = (row) => {
currentGiftUser.value = row
giftBalanceForm.userName = row.userName
giftBalanceForm.amount = ''
giftBalanceForm.transactionNo = ''
giftBalanceForm.businessId = ''
giftBalanceForm.businessType = ''
giftBalanceForm.remark = ''
giftBalanceVisible.value = true
}
const handleGiftBalanceSubmit = async () => {
if (!giftBalanceFormRef.value) return
await giftBalanceFormRef.value.validate(async (valid) => {
if (valid) {
try {
await accountApi.addGiftBalance({
userId: currentGiftUser.value.userId,
amount: Number(giftBalanceForm.amount)
})
ElMessage.success('资金赠送成功')
giftBalanceVisible.value = false
} catch (error) {
ElMessage.error('资金赠送失败')
}
}
})
}
//
const handleAccountDetail = async (row) => {
try {
// userId
const response = await accountApi.getPageList({ userId: row.userId })
if (response && response.data && response.data.list && response.data.list.length > 0) {
currentAccount.value = response.data.list[0]
} else {
currentAccount.value = {}
ElMessage.warning('未找到账户信息')
}
accountDetailVisible.value = true
} catch (error) {
ElMessage.error('获取账户详情失败')
currentAccount.value = {}
}
}
//
const handleViewTransactions = async () => {
if (!currentAccount.value.userId) return
try {
const response = await accountApi.getTransactions({ userId: currentAccount.value.userId })
transactionList.value = response.data || []
transactionDialogVisible.value = true
} catch (error) {
ElMessage.error('获取交易记录失败')
}
}
//
const getTransactionTypeText = (type) => {
switch (type) {
case 1: return '充值'
case 2: return '消费'
case 3: return '退款'
case 4: return '冻结'
case 5: return '解冻'
default: return '未知'
}
}
//
const getTransactionType = (type) => {
switch (type) {
case 1: return 'success'
case 2: return 'danger'
case 3: return 'warning'
case 4: return 'info'
case 5: return 'info'
default: return ''
}
}
onMounted(() => {
loadUserList()
})
</script>
<style scoped>
.user-list-container {
padding: 0;
}
.page-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
.mb-20 {
margin-bottom: 20px;
}
.search-form {
margin-bottom: 10px;
}
.full-width {
width: 100%;
}
.mr-2 {
margin-right: 8px;
}
.mr-1 {
margin-right: 4px;
}
.ml-auto {
margin-left: auto;
}
.table-container {
overflow-x: auto;
}
.user-table {
min-width: 800px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.pagination {
flex-shrink: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-title {
font-size: 20px;
margin-bottom: 15px;
}
.search-form {
margin-bottom: 15px;
}
.user-table {
font-size: 14px;
}
.user-table .el-table-column {
width: auto !important;
}
.pagination-container {
justify-content: center;
}
.pagination {
font-size: 12px;
}
}
@media (max-width: 480px) {
.page-title {
font-size: 18px;
}
.user-table {
font-size: 12px;
}
.el-button {
font-size: 12px;
padding: 6px 12px;
}
}
</style>

37
test-captcha.py Normal file
View File

@ -0,0 +1,37 @@
from playwright.sync_api import sync_playwright
import time
with sync_playwright() as p:
browser = p.chromium.launch(headless=False) # 非无头模式,以便查看
page = browser.new_page()
# 访问登录页面
page.goto('http://localhost:5176/')
page.wait_for_load_state('networkidle')
# 等待验证码加载
time.sleep(2)
# 检查验证码图片是否存在
captcha_image = page.locator('img[alt="验证码"]')
if captcha_image.is_visible():
print("验证码图片已显示")
# 截图保存
page.screenshot(path='captcha-test.png')
print("已保存验证码截图到 captcha-test.png")
else:
print("验证码图片未显示")
# 检查控制台日志
print("\n控制台日志:")
for entry in page.context.logs():
print(f"{entry.type}: {entry.text}")
# 检查网络请求
print("\n网络请求:")
for request in page.context.requests():
if "captcha" in request.url:
print(f"验证码请求: {request.url}")
print(f"状态码: {request.response().status if request.response() else '无响应'}")
browser.close()

7
vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})