集成 payment_order 表相关接口到订单列表页面
This commit is contained in:
commit
3395e0a110
|
|
@ -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?
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -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>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 |
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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', {})
|
||||||
|
}
|
||||||
|
|
@ -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')
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 })
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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 |
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
// 配置文件
|
||||||
|
export const config = {
|
||||||
|
// 后端API地址
|
||||||
|
apiBaseUrl: 'http://localhost:19001/api',
|
||||||
|
// apiBaseUrl: 'https://skills.xueai.art/api',
|
||||||
|
// 前端基础路径
|
||||||
|
baseUrl: '/'
|
||||||
|
}
|
||||||
|
|
@ -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')
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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-js的md5对密码进行加密
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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 => {
|
||||||
|
// 将标签ID转换为字符串以匹配tagMap中的键
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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-js的md5对密码进行加密
|
||||||
|
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
|
||||||
|
|
||||||
|
// 延迟设置isInitialLoading为false,确保组件初始化完成
|
||||||
|
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>
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue