Compare commits
33 Commits
f7dbaf1b71
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 427039e7c3 | |||
| 24f416d6c8 | |||
| 873c9ba125 | |||
| 5dc508eb99 | |||
| 35f62a6b3e | |||
| a083dcfbd4 | |||
| 15f06c2ebe | |||
| c8b3fe0d04 | |||
| 2e14978455 | |||
| f3645ce610 | |||
| 307f5be716 | |||
| 9da9ec72fa | |||
| 6d4b1ba1eb | |||
| a97aaedda3 | |||
| cc42cda34d | |||
| dd465d1ff1 | |||
| 58b7c0f98e | |||
| faf61bfa6c | |||
| b933a46fb8 | |||
| a1328ab8ff | |||
| 5ce6d005ed | |||
| aa2de05831 | |||
| 2a85c782ae | |||
| 8cb66058ae | |||
| 383f6d63c2 | |||
| c1397a38c3 | |||
| d6baee4da9 | |||
| 54cb92d52c | |||
| 8b40dcceca | |||
| 2d7d81e1b6 | |||
| 61511e1fc6 | |||
| bcefd2ba71 | |||
| 73456f6ecf |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "admin-system",
|
||||
"name": "agent-admin-system",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "admin-system",
|
||||
"name": "agent-admin-system",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
|
||||
@@ -15,12 +15,18 @@
|
||||
router
|
||||
class="sidebar-menu"
|
||||
>
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-menu-item index="/dashboard_agent" v-if="userType === 'agent'">
|
||||
<el-icon><Odometer /></el-icon>
|
||||
<template #title>仪表盘</template>
|
||||
<template #title>仪表盘(代理)</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/dashboard_directly" v-if="userType === 'agent_directly'">
|
||||
<!-- <el-menu-item index="/dashboard_directly"> -->
|
||||
<el-icon><Odometer /></el-icon>
|
||||
<template #title>仪表盘(直营代理)</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/users">
|
||||
<el-menu-item index="/users" v-if="userType === 'agent'">
|
||||
<el-icon><User /></el-icon>
|
||||
<template #title>用户管理</template>
|
||||
</el-menu-item>
|
||||
@@ -52,10 +58,15 @@
|
||||
|
||||
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/transfers">
|
||||
<el-menu-item index="/transfers" v-if="userType === 'agent'">
|
||||
<el-icon><Money /></el-icon>
|
||||
<template #title>转账管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/direct-sale" v-if="userType === 'agent_directly'">
|
||||
<el-icon><Coin /></el-icon>
|
||||
<template #title>直营列表</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/daily-transfer-stats">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
@@ -87,7 +98,7 @@
|
||||
<template #title>数据库监控</template>
|
||||
</el-menu-item> -->
|
||||
|
||||
<el-menu-item index="/profile">
|
||||
<el-menu-item v-if="false" index="/profile">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<template #title>个人资料</template>
|
||||
</el-menu-item>
|
||||
@@ -141,7 +152,7 @@
|
||||
<!-- 用户菜单 -->
|
||||
<el-dropdown @command="handleCommand" class="user-dropdown">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="32" :src="userStore.user?.avatar">
|
||||
<el-avatar :size="32" :src="userStore.user?getImageUrl(userStore.user.avatar):userStore.user">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
</el-avatar>
|
||||
<span class="username">{{ userStore.user?.username }}</span>
|
||||
@@ -149,15 +160,15 @@
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">
|
||||
<el-dropdown-item v-if="false" command="profile">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
个人资料
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="changePassword">
|
||||
<el-dropdown-item v-if="false" command="changePassword">
|
||||
<el-icon><Lock /></el-icon>
|
||||
修改密码
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
<el-dropdown-item command="logout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
@@ -228,6 +239,8 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getImageUrl } from '@/utils/config'
|
||||
|
||||
import {
|
||||
Odometer,
|
||||
User,
|
||||
@@ -257,12 +270,24 @@ import {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const userType = computed(() => {
|
||||
try {
|
||||
const adminUser = localStorage.getItem('admin_user')
|
||||
if (adminUser) {
|
||||
return JSON.parse(adminUser).user_type
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('获取用户类型失败', error)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// 组件挂载时不再自动验证token,避免登录后立即触发401错误
|
||||
// token验证交给具体的API调用时处理
|
||||
onMounted(() => {
|
||||
// 仅确保用户状态已正确加载
|
||||
console.log('Layout组件已挂载,用户状态:', userStore.isAuthenticated)
|
||||
console.log('userStore:', userStore)
|
||||
})
|
||||
|
||||
// 侧边栏折叠状态
|
||||
|
||||
@@ -2,6 +2,20 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import NProgress from 'nprogress'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const userType = computed(() => {
|
||||
try {
|
||||
const adminUser = localStorage.getItem('admin_user')
|
||||
if (adminUser) {
|
||||
console.log('user_type:', JSON.parse(adminUser).user_type)
|
||||
return JSON.parse(adminUser).user_type
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('获取用户类型失败', error)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -16,20 +30,29 @@ const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('@/layout/Layout.vue'),
|
||||
redirect: '/dashboard',
|
||||
redirect: userType.value === 'agent' ? '/dashboard_agent' : userType.value === 'agent_directly' ? '/dashboard_directly' : '/login',
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
path: 'dashboard_agent',
|
||||
name: 'DashboardAgent',
|
||||
component: () => import('@/views/DashboardAgent.vue'),
|
||||
meta: {
|
||||
title: '数据统计 - 代理后台管理系统',
|
||||
icon: 'Odometer'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'dashboard_directly',
|
||||
name: 'DashboardDirectly',
|
||||
component: () => import('@/views/DashboardDirectly.vue'),
|
||||
meta: {
|
||||
title: '数据统计 - 直营代理后台管理系统',
|
||||
icon: 'Odometer'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'Users',
|
||||
@@ -48,6 +71,15 @@ const routes = [
|
||||
icon: 'Money'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'direct-sale',
|
||||
name: 'DirectSale',
|
||||
component: () => import('@/views/DirectSale.vue'),
|
||||
meta: {
|
||||
title: '直接销售 - 代理后台管理系统',
|
||||
icon: 'Coin'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'commissions',
|
||||
name: 'Commissions',
|
||||
|
||||
@@ -1,188 +1,187 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {defineStore} from 'pinia'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import api from '@/utils/api'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
user: null,
|
||||
token: localStorage.getItem('admin_token') || null,
|
||||
loading: false,
|
||||
statusCheckInterval: null
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAuthenticated: (state) => !!state.token && !!state.user,
|
||||
isAdmin: (state) => state.user?.role === 'admin'
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
async login(credentials) {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await api.auth.login(credentials)
|
||||
const { token, user } = response.data
|
||||
|
||||
this.token = token
|
||||
this.user = user
|
||||
|
||||
// 存储到本地存储
|
||||
localStorage.setItem('admin_token', token)
|
||||
localStorage.setItem('admin_user', JSON.stringify(user))
|
||||
|
||||
this.startStatusCheck() // 登录成功后开始状态检查
|
||||
ElMessage.success(`欢迎回来,${user.username}!`)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '登录失败'
|
||||
ElMessage.error(message)
|
||||
return { success: false, message }
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
state: () => ({
|
||||
user: null,
|
||||
token: localStorage.getItem('admin_token') || null,
|
||||
loading: false,
|
||||
statusCheckInterval: null
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAuthenticated: (state) => !!state.token && !!state.user,
|
||||
isAdmin: (state) => state.user?.role === 'admin'
|
||||
},
|
||||
|
||||
// 登出
|
||||
async logout() {
|
||||
try {
|
||||
this.stopStatusCheck() // 登出时停止状态检查
|
||||
|
||||
// 清除状态
|
||||
this.user = null
|
||||
this.token = null
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_user')
|
||||
|
||||
ElMessage.success('退出成功,期待您的再次光临!')
|
||||
} catch (error) {
|
||||
console.error('登出失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 检查认证状态(仅在需要时验证token有效性)
|
||||
async checkAuth() {
|
||||
const token = localStorage.getItem('admin_token')
|
||||
const userStr = localStorage.getItem('admin_user')
|
||||
|
||||
if (!token || !userStr) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// 先从本地存储恢复状态
|
||||
const user = JSON.parse(userStr)
|
||||
this.token = token
|
||||
this.user = user
|
||||
|
||||
// 可选:验证token是否仍然有效(仅在必要时调用)
|
||||
// 这里不主动验证,让具体的API调用时自然验证
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('认证检查失败:', error)
|
||||
// 清除无效的本地存储数据
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_user')
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
// 更新个人信息
|
||||
async updateProfile(profileData) {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await api.users.updateUser(this.user.id, profileData)
|
||||
|
||||
this.user = { ...this.user, ...response.data }
|
||||
localStorage.setItem('admin_user', JSON.stringify(this.user))
|
||||
|
||||
ElMessage.success('个人信息更新成功')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '更新失败'
|
||||
ElMessage.error(message)
|
||||
return { success: false, message }
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 修改密码
|
||||
async changePassword(passwordData) {
|
||||
this.loading = true
|
||||
try {
|
||||
await api.auth.changePassword(passwordData)
|
||||
ElMessage.success('密码修改成功,请重新登录')
|
||||
|
||||
// 修改密码后需要重新登录
|
||||
setTimeout(() => {
|
||||
this.logout()
|
||||
}, 1500)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '密码修改失败'
|
||||
ElMessage.error(message)
|
||||
return { success: false, message }
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户详情
|
||||
async getUserDetails(userId) {
|
||||
try {
|
||||
const response = await api.users.getUserById(userId)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '获取用户详情失败'
|
||||
ElMessage.error(message)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 获取当前用户信息
|
||||
async fetchUserInfo() {
|
||||
try {
|
||||
const response = await api.auth.getCurrentUser()
|
||||
this.user = response.data.user
|
||||
localStorage.setItem('admin_user', JSON.stringify(this.user))
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '获取用户信息失败'
|
||||
console.error('获取用户信息失败:', error)
|
||||
return { success: false, message }
|
||||
}
|
||||
},
|
||||
|
||||
// 开始状态检查
|
||||
startStatusCheck() {
|
||||
// 如果已经有定时器在运行,先清除
|
||||
if (this.statusCheckInterval) {
|
||||
clearInterval(this.statusCheckInterval)
|
||||
}
|
||||
|
||||
// 每5分钟检查一次用户状态
|
||||
this.statusCheckInterval = setInterval(async () => {
|
||||
if (this.isAuthenticated) {
|
||||
try {
|
||||
await api.auth.getCurrentUser()
|
||||
} catch (error) {
|
||||
// 如果请求失败,说明token可能已失效或用户被拉黑
|
||||
// api拦截器会自动处理这些情况
|
||||
console.log('用户状态检查失败,可能已被拉黑或token失效')
|
||||
}
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
async login(credentials) {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await api.auth.login(credentials)
|
||||
const {token, agent} = response.data.data
|
||||
this.token = token
|
||||
this.user = agent
|
||||
|
||||
// 存储到本地存储
|
||||
localStorage.setItem('admin_token', token)
|
||||
localStorage.setItem('admin_user', JSON.stringify(agent))
|
||||
|
||||
this.startStatusCheck() // 登录成功后开始状态检查
|
||||
ElMessage.success(`欢迎回来,${agent.realName}!`)
|
||||
return {success: true}
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '登录失败'
|
||||
ElMessage.error(message)
|
||||
return {success: false, message}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 登出
|
||||
async logout() {
|
||||
try {
|
||||
this.stopStatusCheck() // 登出时停止状态检查
|
||||
|
||||
// 清除状态
|
||||
this.user = null
|
||||
this.token = null
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_user')
|
||||
|
||||
ElMessage.success('退出成功,期待您的再次光临!')
|
||||
} catch (error) {
|
||||
console.error('登出失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 检查认证状态(仅在需要时验证token有效性)
|
||||
async checkAuth() {
|
||||
const token = localStorage.getItem('admin_token')
|
||||
const userStr = localStorage.getItem('admin_user')
|
||||
|
||||
if (!token || !userStr) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// 先从本地存储恢复状态
|
||||
const user = JSON.parse(userStr)
|
||||
this.token = token
|
||||
this.user = user
|
||||
|
||||
// 可选:验证token是否仍然有效(仅在必要时调用)
|
||||
// 这里不主动验证,让具体的API调用时自然验证
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('认证检查失败:', error)
|
||||
// 清除无效的本地存储数据
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_user')
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
// 更新个人信息
|
||||
async updateProfile(profileData) {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await api.users.updateUser(this.user.id, profileData)
|
||||
|
||||
this.user = {...this.user, ...response.data}
|
||||
localStorage.setItem('admin_user', JSON.stringify(this.user))
|
||||
|
||||
ElMessage.success('个人信息更新成功')
|
||||
return {success: true}
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '更新失败'
|
||||
ElMessage.error(message)
|
||||
return {success: false, message}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 修改密码
|
||||
async changePassword(passwordData) {
|
||||
this.loading = true
|
||||
try {
|
||||
await api.auth.changePassword(passwordData)
|
||||
ElMessage.success('密码修改成功,请重新登录')
|
||||
|
||||
// 修改密码后需要重新登录
|
||||
setTimeout(() => {
|
||||
this.logout()
|
||||
}, 1500)
|
||||
|
||||
return {success: true}
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '密码修改失败'
|
||||
ElMessage.error(message)
|
||||
return {success: false, message}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户详情
|
||||
async getUserDetails(userId) {
|
||||
try {
|
||||
const response = await api.users.getUserById(userId)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '获取用户详情失败'
|
||||
ElMessage.error(message)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 获取当前用户信息
|
||||
async fetchUserInfo() {
|
||||
try {
|
||||
const response = await api.auth.getCurrentUser()
|
||||
this.user = response.data.user
|
||||
localStorage.setItem('admin_user', JSON.stringify(this.user))
|
||||
return {success: true}
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || '获取用户信息失败'
|
||||
console.error('获取用户信息失败:', error)
|
||||
return {success: false, message}
|
||||
}
|
||||
},
|
||||
|
||||
// 开始状态检查
|
||||
startStatusCheck() {
|
||||
// 如果已经有定时器在运行,先清除
|
||||
if (this.statusCheckInterval) {
|
||||
clearInterval(this.statusCheckInterval)
|
||||
}
|
||||
|
||||
// 每5分钟检查一次用户状态
|
||||
this.statusCheckInterval = setInterval(async () => {
|
||||
if (this.isAuthenticated) {
|
||||
try {
|
||||
await api.auth.getCurrentUser()
|
||||
} catch (error) {
|
||||
// 如果请求失败,说明token可能已失效或用户被拉黑
|
||||
// api拦截器会自动处理这些情况
|
||||
console.log('用户状态检查失败,可能已被拉黑或token失效')
|
||||
}
|
||||
}
|
||||
}, 5 * 60 * 1000) // 5分钟
|
||||
},
|
||||
|
||||
// 停止状态检查
|
||||
stopStatusCheck() {
|
||||
if (this.statusCheckInterval) {
|
||||
clearInterval(this.statusCheckInterval)
|
||||
this.statusCheckInterval = null
|
||||
}
|
||||
}
|
||||
}, 5 * 60 * 1000) // 5分钟
|
||||
},
|
||||
|
||||
// 停止状态检查
|
||||
stopStatusCheck() {
|
||||
if (this.statusCheckInterval) {
|
||||
clearInterval(this.statusCheckInterval)
|
||||
this.statusCheckInterval = null
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
382
src/utils/api.js
382
src/utils/api.js
@@ -1,15 +1,15 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
import {ElMessage, ElLoading} from 'element-plus'
|
||||
import NProgress from 'nprogress'
|
||||
import { apiURL } from './config.js'
|
||||
import {apiURL} from './config.js'
|
||||
|
||||
// 创建axios实例
|
||||
const request = axios.create({
|
||||
baseURL: apiURL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
baseURL: apiURL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
let loadingInstance = null
|
||||
@@ -18,203 +18,223 @@ let isLoggingOut = false // 防止重复登出
|
||||
|
||||
// 显示加载
|
||||
const showLoading = () => {
|
||||
if (requestCount === 0) {
|
||||
loadingInstance = ElLoading.service({
|
||||
text: '加载中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
}
|
||||
requestCount++
|
||||
if (requestCount === 0) {
|
||||
loadingInstance = ElLoading.service({
|
||||
text: '加载中...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
})
|
||||
}
|
||||
requestCount++
|
||||
}
|
||||
|
||||
// 隐藏加载
|
||||
const hideLoading = () => {
|
||||
requestCount--
|
||||
if (requestCount <= 0) {
|
||||
requestCount = 0
|
||||
if (loadingInstance) {
|
||||
loadingInstance.close()
|
||||
loadingInstance = null
|
||||
requestCount--
|
||||
if (requestCount <= 0) {
|
||||
requestCount = 0
|
||||
if (loadingInstance) {
|
||||
loadingInstance.close()
|
||||
loadingInstance = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
// 开始进度条
|
||||
NProgress.start()
|
||||
|
||||
// 显示加载动画(除了某些不需要的请求)
|
||||
if (!config.hideLoading) {
|
||||
showLoading()
|
||||
(config) => {
|
||||
// 开始进度条
|
||||
NProgress.start()
|
||||
|
||||
// 显示加载动画(除了某些不需要的请求)
|
||||
if (!config.hideLoading) {
|
||||
showLoading()
|
||||
}
|
||||
|
||||
// 添加认证token
|
||||
const token = localStorage.getItem('admin_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
hideLoading()
|
||||
NProgress.done()
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 添加认证token
|
||||
const token = localStorage.getItem('admin_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
hideLoading()
|
||||
NProgress.done()
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
hideLoading()
|
||||
NProgress.done()
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
hideLoading()
|
||||
NProgress.done()
|
||||
|
||||
const { response } = error
|
||||
|
||||
if (response) {
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
// 防止重复处理401错误
|
||||
if (!isLoggingOut) {
|
||||
isLoggingOut = true
|
||||
|
||||
// 只在非登录页面显示错误消息
|
||||
if (window.location.pathname !== '/admin/login') {
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
(response) => {
|
||||
hideLoading()
|
||||
NProgress.done()
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
hideLoading()
|
||||
NProgress.done()
|
||||
|
||||
const {response} = error
|
||||
|
||||
if (response) {
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
// 防止重复处理401错误
|
||||
if (!isLoggingOut) {
|
||||
isLoggingOut = true
|
||||
|
||||
// 只在非登录页面显示错误消息
|
||||
if (window.location.pathname !== '/admin/login') {
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
}
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_user')
|
||||
|
||||
// 立即跳转到登录页,减少延迟
|
||||
setTimeout(() => {
|
||||
if (typeof window !== 'undefined' && window.location.pathname !== '/admin/login') {
|
||||
window.location.href = '/admin/login'
|
||||
}
|
||||
// 重置标志
|
||||
setTimeout(() => {
|
||||
isLoggingOut = false
|
||||
}, 1000)
|
||||
}, 500)
|
||||
}
|
||||
break
|
||||
case 403:
|
||||
// 检查是否是用户被拉黑
|
||||
if (response.data.code === 'USER_BLACKLISTED') {
|
||||
// 防止重复处理拉黑错误
|
||||
if (!isLoggingOut) {
|
||||
isLoggingOut = true
|
||||
|
||||
ElMessage.error(response.data.message || '账户已被拉黑,请联系管理员')
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_user')
|
||||
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
if (typeof window !== 'undefined' && window.location.pathname !== '/admin/login') {
|
||||
window.location.href = '/admin/login'
|
||||
}
|
||||
// 重置标志
|
||||
setTimeout(() => {
|
||||
isLoggingOut = false
|
||||
}, 1000)
|
||||
}, 500)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('没有权限访问此资源')
|
||||
}
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error('请求的资源不存在')
|
||||
break
|
||||
case 400:
|
||||
ElMessage.error(response.data.message || '请求参数错误')
|
||||
break
|
||||
case 429:
|
||||
ElMessage.error('请求过于频繁,请稍后再试')
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error('服务器内部错误')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(response.data.error.message || '请求失败')
|
||||
}
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_user')
|
||||
|
||||
// 立即跳转到登录页,减少延迟
|
||||
setTimeout(() => {
|
||||
if (typeof window !== 'undefined' && window.location.pathname !== '/admin/login') {
|
||||
window.location.href = '/admin/login'
|
||||
}
|
||||
// 重置标志
|
||||
setTimeout(() => {
|
||||
isLoggingOut = false
|
||||
}, 1000)
|
||||
}, 500)
|
||||
}
|
||||
break
|
||||
case 403:
|
||||
// 检查是否是用户被拉黑
|
||||
if (response.data.code === 'USER_BLACKLISTED') {
|
||||
// 防止重复处理拉黑错误
|
||||
if (!isLoggingOut) {
|
||||
isLoggingOut = true
|
||||
|
||||
ElMessage.error(response.data.message || '账户已被拉黑,请联系管理员')
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('admin_token')
|
||||
localStorage.removeItem('admin_user')
|
||||
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
if (typeof window !== 'undefined' && window.location.pathname !== '/admin/login') {
|
||||
window.location.href = '/admin/login'
|
||||
}
|
||||
// 重置标志
|
||||
setTimeout(() => {
|
||||
isLoggingOut = false
|
||||
}, 1000)
|
||||
}, 500)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('没有权限访问此资源')
|
||||
}
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error('请求的资源不存在')
|
||||
break
|
||||
case 422:
|
||||
ElMessage.error(response.data.message || '请求参数错误')
|
||||
break
|
||||
case 429:
|
||||
ElMessage.error('请求过于频繁,请稍后再试')
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error('服务器内部错误')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(response.data.error.message || '请求失败')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('网络错误,请检查网络连接')
|
||||
} else {
|
||||
ElMessage.error('网络错误,请检查网络连接')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// API接口定义
|
||||
const api = {
|
||||
// 代理认证相关
|
||||
auth: {
|
||||
login: (data) => request.post('/agents/auth/login', data),
|
||||
getCurrentUser: () => request.get('/agents/auth/me'),
|
||||
changePassword: (data) => request.put('/agents/auth/change-password', data)
|
||||
},
|
||||
|
||||
// 代理下用户管理(只读)
|
||||
users: {
|
||||
getUsers: (params) => request.get('/agents/users', { params }),
|
||||
getUserById: (id) => request.get(`/agents/users/${id}`),
|
||||
getUserStats: () => request.get('/agents/users/stats')
|
||||
},
|
||||
|
||||
// 代理数据统计
|
||||
dashboard: {
|
||||
getStats: () => request.get('/agents/dashboard/stats'),
|
||||
getChartData: (type) => request.get(`/agents/dashboard/charts/${type}`)
|
||||
},
|
||||
// 代理认证相关
|
||||
auth: {
|
||||
login: (data) => request.post('/agents/auth/login', data),
|
||||
getCurrentUser: () => request.get('/agents/auth/me'),
|
||||
changePassword: (data) => request.put('/agents/auth/change-password', data)
|
||||
},
|
||||
|
||||
// 佣金管理
|
||||
commissions: {
|
||||
// 获取佣金列表
|
||||
getList: (params) => request.get('/agents/commissions', { params }),
|
||||
// 获取佣金统计
|
||||
getStats: (params) => request.get('/agents/commissions/stats', { params }),
|
||||
// 发放单个佣金
|
||||
pay: (id) => request.post(`/agents/commissions/${id}/pay`),
|
||||
// 批量发放佣金
|
||||
batchPay: (data) => request.post('/agents/commissions/batch-pay', data)
|
||||
},
|
||||
// 代理下用户管理(只读)
|
||||
users: {
|
||||
getUsers: (params) => request.get('/users', {params}),
|
||||
getUserById: (id) => request.get(`/agents/users/${id}`),
|
||||
getUserStats: () => request.get('/agents/users/stats')
|
||||
},
|
||||
|
||||
// 转账记录管理(只读)
|
||||
transfers: {
|
||||
getTransfers: (params) => request.get('/agents/transfers', { params }),
|
||||
getTransferStats: () => request.get('/agents/transfers/stats')
|
||||
},
|
||||
|
||||
// 文件上传
|
||||
upload: {
|
||||
uploadImage: (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('image', file)
|
||||
return request.post('/upload/image', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
// 代理数据统计
|
||||
dashboard: {
|
||||
// 代理数据统计
|
||||
getStats: () => request.get('/agent/stats'),
|
||||
getChartData: (type) => request.get(`/agents/dashboard/charts/${type}`),
|
||||
getUserChart: (params) => request.get("/agent/user-growth-trend", {params}),
|
||||
getCommissionTrend: () => request.get("/agent/commission-trend"),
|
||||
getCommissionDistribution: () => request.get("/agent/commission-distribution"),
|
||||
getRecentUsers: () => request.get("/agent/recent-users"),
|
||||
getRecentCommissions: () => request.get("/agent/recent-commissions"),
|
||||
|
||||
// 直营代理数据统计
|
||||
getStatsAgentDirectly: () => request.get('/agent/stats_agent_directly'),
|
||||
},
|
||||
|
||||
// 佣金管理
|
||||
commissions: {
|
||||
// 获取佣金列表
|
||||
getList: (params) => request.get('/agents/commissions/list', {params}),
|
||||
// 获取佣金统计
|
||||
getStats: (params) => request.get('/agents/commissions/stats', {params}),
|
||||
// 发放单个佣金
|
||||
pay: (id) => request.post(`/agents/commissions/${id}/pay`),
|
||||
// 批量发放佣金
|
||||
batchPay: (data) => request.post('/agents/commissions/batch-pay', data)
|
||||
},
|
||||
|
||||
// 转账记录管理(只读)
|
||||
transfers: {
|
||||
getList: (params) => request.get('/transfers', {params}),
|
||||
getTransfers: (params) => request.get('/agents/transfers', {params}),
|
||||
getTransferStats: () => request.get('/agents/transfers/stats')
|
||||
},
|
||||
|
||||
// 直营列表
|
||||
directSale: {
|
||||
getStats: () => request.get('/direct-sale/stats'),// 获取整体数据
|
||||
getDirectSales: (params) => request.get('/direct-sale', {params}),// 获取直营列表
|
||||
withdraw: (data) => request.post(`/users/withdraw`, data),// 提现
|
||||
// 直营用户
|
||||
addUser: (data) => request.post(`/users/create`, data),
|
||||
listUsers: (params) => request.get('/users/directly_operated', {params}),
|
||||
},
|
||||
|
||||
// 文件上传
|
||||
upload: {
|
||||
uploadImage: (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('image', file)
|
||||
return request.post('/upload/image', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 为了向后兼容,添加直接的 get、post 等方法
|
||||
get: (url, config) => request.get(url, config),
|
||||
post: (url, data, config) => request.post(url, data, config),
|
||||
put: (url, data, config) => request.put(url, data, config),
|
||||
delete: (url, config) => request.delete(url, config)
|
||||
},
|
||||
|
||||
// 为了向后兼容,添加直接的 get、post 等方法
|
||||
get: (url, config) => request.get(url, config),
|
||||
post: (url, data, config) => request.post(url, data, config),
|
||||
put: (url, data, config) => request.put(url, data, config),
|
||||
delete: (url, config) => request.delete(url, config)
|
||||
}
|
||||
|
||||
export default api
|
||||
@@ -2,13 +2,13 @@
|
||||
const config = {
|
||||
development: {
|
||||
baseURL: 'https://minio.zrbjr.com',
|
||||
uploadURL: 'http://localhost:3002/api/upload',
|
||||
apiURL: 'http://localhost:3002/api'
|
||||
uploadURL: 'http://192.168.0.11:3002/api/upload',
|
||||
apiURL: 'http://192.168.0.11:3002/api'
|
||||
},
|
||||
production: {
|
||||
baseURL: 'https://minio.zrbjr.com',
|
||||
uploadURL: `${window.location.origin}/api/upload`,
|
||||
apiURL: `${window.location.origin}/api`
|
||||
uploadURL: `/api/upload`,
|
||||
apiURL: `/api`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,34 +27,31 @@ export const { baseURL, uploadURL, apiURL } = config[env]
|
||||
* @returns {string} 完整的图片URL
|
||||
*/
|
||||
export const getImageUrl = (imagePath) => {
|
||||
const cleanBaseURL = baseURL.replace(/\/$/, '')
|
||||
if (!imagePath) return ''
|
||||
if (imagePath.startsWith('http')) return imagePath
|
||||
|
||||
// 在开发环境下,使用代理路径
|
||||
if (env === 'development') {
|
||||
// 如果路径已经包含uploads,直接使用
|
||||
if (imagePath.startsWith('/uploads/') || imagePath.startsWith('uploads/')) {
|
||||
const cleanPath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`
|
||||
return cleanPath
|
||||
// console.log('getImageUrl called with:', imagePath)
|
||||
if (!imagePath) return ''
|
||||
if (imagePath.startsWith('http')) return imagePath
|
||||
const cleanBaseURL = baseURL.replace(/\/$/, '')
|
||||
|
||||
// 如果图片路径以/uploads开头,直接返回原路径
|
||||
if (imagePath.startsWith('/uploads')) {
|
||||
// console.log('Image starts with /uploads, returning original path:', imagePath)
|
||||
return `${cleanBaseURL}/jurongquan${imagePath}`
|
||||
}
|
||||
// 否则添加uploads前缀
|
||||
return `${cleanBaseURL}${imagePath}`
|
||||
}
|
||||
|
||||
// 生产环境下使用完整URL
|
||||
|
||||
let cleanImagePath
|
||||
|
||||
// 如果路径已经包含uploads,直接使用
|
||||
if (imagePath.startsWith('/uploads/') || imagePath.startsWith('uploads/')) {
|
||||
cleanImagePath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`
|
||||
} else {
|
||||
// 否则添加uploads前缀
|
||||
cleanImagePath = `/uploads/${imagePath}`
|
||||
}
|
||||
|
||||
return `${cleanBaseURL}${cleanImagePath}`
|
||||
|
||||
// 在开发环境下,也需要根据路径前缀处理
|
||||
if (env === 'development') {
|
||||
const cleanBaseURL = baseURL.replace(/\/$/, '')
|
||||
const cleanImagePath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`
|
||||
const fullUrl = `${cleanBaseURL}${cleanImagePath}`
|
||||
// console.log('Development environment, returning:', fullUrl)
|
||||
return fullUrl
|
||||
}
|
||||
|
||||
// 生产环境下使用完整URL
|
||||
const cleanImagePath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`
|
||||
const fullUrl = `${cleanBaseURL}${cleanImagePath}`
|
||||
|
||||
return fullUrl
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
131
src/utils/public_method.js
Normal file
131
src/utils/public_method.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 验证并脱敏电话号码
|
||||
* @param {string} phone - 需要验证和脱敏的电话号码
|
||||
* @returns {string|boolean} - 脱敏后的电话号码,无效则返回false
|
||||
*/
|
||||
export function maskPhoneNumber(phone) {
|
||||
// 检查是否为字符串
|
||||
if (typeof phone !== 'string') {
|
||||
return phone;
|
||||
}
|
||||
|
||||
// 移除所有非数字字符
|
||||
const cleaned = phone.replace(/\D/g, '');
|
||||
|
||||
// 验证常见的电话号码格式
|
||||
// 支持: 11位手机号(中国大陆)、带区号的固定电话
|
||||
const phoneRegex = /^(1[3-9]\d{9})$|^(\d{3,4}-\d{7,8})$|^(\d{3,4}\d{7,8})$/;
|
||||
|
||||
if (!phoneRegex.test(cleaned) && !phoneRegex.test(phone)) {
|
||||
return phone; // 不是有效的电话号码
|
||||
}
|
||||
|
||||
// 根据不同格式进行脱敏
|
||||
if (cleaned.length === 11) {
|
||||
// 手机号: 保留前3位和后4位,中间4位用*代替
|
||||
return cleaned.replace(/^(\d{3})(\d{4})(\d{4})$/, '$1****$3');
|
||||
} else if (phone.includes('-')) {
|
||||
// 带区号的固定电话: 区号不变,号码中间用*代替
|
||||
const [areaCode, number] = phone.split('-');
|
||||
if (number.length <= 4) {
|
||||
return `${areaCode}-****`;
|
||||
}
|
||||
return `${areaCode}-${number.substr(0, 2)}****${number.substr(-2)}`;
|
||||
} else {
|
||||
// 不带区号的固定电话
|
||||
if (cleaned.length <= 4) {
|
||||
return '****';
|
||||
}
|
||||
return `${cleaned.substr(0, 2)}****${cleaned.substr(-2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 姓名脱敏处理
|
||||
* @param {string} name - 需要脱敏的姓名
|
||||
* @returns {string} - 脱敏后的姓名
|
||||
*/
|
||||
export function maskName(name) {
|
||||
// 检查输入是否为有效字符串
|
||||
if (!name || typeof name !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 去除前后空格
|
||||
const trimmedName = name.trim();
|
||||
|
||||
// 检查是否为英文姓名(包含空格)
|
||||
if (trimmedName.includes(' ')) {
|
||||
const parts = trimmedName.split(' ').filter(part => part);
|
||||
|
||||
// 处理英文名:名全显,姓只显首字母
|
||||
if (parts.length >= 2) {
|
||||
const firstName = parts.slice(0, -1).join(' ');
|
||||
const lastName = parts[parts.length - 1];
|
||||
return `${firstName} ${lastName.charAt(0)}*`;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理中文姓名
|
||||
const length = trimmedName.length;
|
||||
|
||||
switch (length) {
|
||||
case 1:
|
||||
// 单字名,不脱敏
|
||||
return trimmedName;
|
||||
case 2:
|
||||
// 双字名,隐藏第二个字
|
||||
return `${trimmedName[0]}*`;
|
||||
case 3:
|
||||
// 三字名,隐藏中间字
|
||||
return `${trimmedName[0]}*${trimmedName[2]}`;
|
||||
case 4:
|
||||
// 四字名(如复姓),隐藏中间两个字
|
||||
return `${trimmedName[0]}**${trimmedName[3]}`;
|
||||
default:
|
||||
// 更长的姓名,显示首尾各两个字,中间用*代替
|
||||
if (length > 4) {
|
||||
return `${trimmedName.substr(0, 2)}${'*'.repeat(length - 4)}${trimmedName.substr(-2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return trimmedName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换日期 yyyy-HH-mm
|
||||
*/
|
||||
export const convertToDateOnly = (dateString) => {
|
||||
// 创建Date对象
|
||||
const date = new Date(dateString);
|
||||
|
||||
// 获取年、月、日
|
||||
const year = date.getFullYear();
|
||||
// 月份从0开始,所以需要加1
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
// 拼接成YYYY-MM-DD格式
|
||||
return `${year}-${month}-${day - 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将ISO格式日期时间转换为yyyy-MM-dd HH:mm:ss格式
|
||||
* @param {string} isoString - ISO格式的日期时间字符串,如2025-09-04T06:18:08.000Z
|
||||
* @returns {string} - 转换后的日期时间字符串
|
||||
*/
|
||||
export function formatIsoToCustom(utcString) {
|
||||
// 创建Date对象
|
||||
const date = new Date(utcString);
|
||||
|
||||
// 使用UTC方法获取各时间部分,确保使用UTC时间而非本地时间
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // 月份从0开始
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
||||
|
||||
// 拼接成目标格式
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
@@ -5,23 +5,21 @@
|
||||
<div class="header-left">
|
||||
<div class="welcome-section">
|
||||
<div class="greeting-icon">
|
||||
<el-icon><View /></el-icon>
|
||||
<el-icon>
|
||||
<View/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="greeting-text">
|
||||
<h1 class="page-title">代理数据统计</h1>
|
||||
<p class="page-subtitle">{{ getGreeting() }},{{ userStore.user?.username }}!</p>
|
||||
<p class="page-subtitle">{{ getGreeting() }},{{ userStore.user?.realName }}!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-stats">
|
||||
<div class="quick-stat">
|
||||
<span class="stat-value">{{ statsData.totalUsers }}</span>
|
||||
<span class="stat-label">下级用户</span>
|
||||
</div>
|
||||
<div class="quick-stat">
|
||||
<span class="stat-value">{{ statsData.todayCommission }}</span>
|
||||
<span class="stat-label">今日佣金</span>
|
||||
<span class="stat-value">{{ statsData.users.today_new_users }}</span>
|
||||
<span class="stat-label">今日新增用户</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
@@ -32,24 +30,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-section">
|
||||
<el-row :gutter="24" class="stats-row">
|
||||
<el-col :xs="24" :sm="12" :md="6" :lg="6" v-for="stat in stats" :key="stat.key">
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col
|
||||
v-for="stat in stats"
|
||||
:key="stat.key"
|
||||
class="stat-col"
|
||||
>
|
||||
<div class="stat-card" :class="stat.class" @click="handleStatClick(stat.key)">
|
||||
<div class="stat-background"></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<el-icon><component :is="stat.icon" /></el-icon>
|
||||
<el-icon>
|
||||
<component :is="stat.icon"/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number" :data-value="stat.value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<div class="stat-change" :class="stat.changeClass">
|
||||
<el-icon><component :is="stat.changeIcon" /></el-icon>
|
||||
{{ stat.change }}
|
||||
</div>
|
||||
<div class="stat-change" :class="stat.changeClass">
|
||||
<el-icon><component :is="stat.changeIcon" /></el-icon>
|
||||
{{ stat.change }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-decoration"></div>
|
||||
@@ -57,7 +61,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<!-- 用户增长趋势 -->
|
||||
@@ -65,69 +69,78 @@
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">下级用户增长趋势</span>
|
||||
<span class="card-title">直推用户增长趋势</span>
|
||||
<el-select v-model="userChartPeriod" size="small" style="width: 100px" @change="loadUserChart">
|
||||
<el-option label="7天" value="7d" />
|
||||
<el-option label="30天" value="30d" />
|
||||
<el-option label="90天" value="90d" />
|
||||
<el-option label="7天" value="7"/>
|
||||
<el-option label="30天" value="30"/>
|
||||
<el-option label="90天" value="90"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="userChartOption" :loading="chartLoading" />
|
||||
<div v-if="userChartData.length==0" class="empty-state">
|
||||
<el-empty description="暂无数据"/>
|
||||
</div>
|
||||
<v-chart :option="userChartOption" :loading="chartLoading"/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
|
||||
<!-- 佣金收入趋势 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">佣金收入趋势</span>
|
||||
<span class="card-title">用户转账流水</span>
|
||||
<el-tag type="success" size="small">近30天</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="commissionChartOption" :loading="chartLoading" />
|
||||
<div v-if="commissionChartData.length==0" class="empty-state">
|
||||
<el-empty description="暂无数据"/>
|
||||
</div>
|
||||
<v-chart v-else :option="commissionChartOption" :loading="chartLoading"/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
|
||||
<!-- 业务分析图表 -->
|
||||
<el-row :gutter="20" class="business-charts-row">
|
||||
<!-- <el-row :gutter="20" class="business-charts-row">-->
|
||||
<!-- 佣金类型分布 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">佣金类型分布</span>
|
||||
<el-tag type="primary" size="small">总览</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="commissionTypeOption" :loading="chartLoading" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- <el-col :xs="24" :lg="12">-->
|
||||
<!-- <el-card class="chart-card" shadow="hover">-->
|
||||
<!-- <template #header>-->
|
||||
<!-- <div class="card-header">-->
|
||||
<!-- <span class="card-title">佣金类型分布</span>-->
|
||||
<!-- <el-tag type="primary" size="small">总览</el-tag>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <div class="chart-container">-->
|
||||
<!-- <div v-if="commissionTypeData.length==0" class="empty-state">-->
|
||||
<!-- <el-empty description="暂无数据"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- <v-chart :option="commissionTypeOption" :loading="chartLoading"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </el-card>-->
|
||||
<!-- </el-col>-->
|
||||
|
||||
<!-- 用户活跃度 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">用户活跃度</span>
|
||||
<el-tag type="info" size="small">近7天</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<v-chart :option="userActivityOption" :loading="chartLoading" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- <el-col :xs="24" :lg="12">-->
|
||||
<!-- <el-card class="chart-card" shadow="hover">-->
|
||||
<!-- <template #header>-->
|
||||
<!-- <div class="card-header">-->
|
||||
<!-- <span class="card-title">用户活跃度</span>-->
|
||||
<!-- <el-tag type="info" size="small">近7天</el-tag>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <div class="chart-container">-->
|
||||
<!-- <v-chart :option="userActivityOption" :loading="chartLoading"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </el-card>-->
|
||||
<!-- </el-col>-->
|
||||
<!-- </el-row>-->
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<el-row :gutter="20" class="activity-row">
|
||||
<!-- 最新用户 -->
|
||||
@@ -141,59 +154,60 @@
|
||||
</template>
|
||||
<div class="activity-list">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-skeleton :rows="3" animated />
|
||||
<el-skeleton :rows="3" animated/>
|
||||
</div>
|
||||
<div v-else-if="recentUsers.length === 0" class="empty-state">
|
||||
<el-empty description="暂无数据" />
|
||||
<el-empty description="暂无数据"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="user in recentUsers" :key="user.id" class="activity-item">
|
||||
<el-avatar :size="40" :src="user.avatar">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<el-avatar :size="40" :src="getImageUrl(user.avatar)">
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
<div class="activity-info">
|
||||
<div class="activity-title">{{ user.username }}</div>
|
||||
<div class="activity-desc">{{ user.phone }}</div>
|
||||
<div class="activity-title">{{ maskPhoneNumber(user.username) }}</div>
|
||||
<div class="activity-desc">{{ maskPhoneNumber(user.phone) }}</div>
|
||||
<div class="activity-time">{{ formatTime(user.created_at) }}</div>
|
||||
</div>
|
||||
<el-tag type="success" size="small">
|
||||
新用户
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 最新佣金 -->
|
||||
|
||||
<!-- 最新营收 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="activity-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">最新佣金</span>
|
||||
<el-link type="primary" @click="$router.push('/commissions')">查看全部</el-link>
|
||||
<span class="card-title">最新营收</span>
|
||||
<!-- <el-link type="primary" @click="$router.push('/commissions')">查看全部</el-link> -->
|
||||
</div>
|
||||
</template>
|
||||
<div class="activity-list">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-skeleton :rows="3" animated />
|
||||
<el-skeleton :rows="3" animated/>
|
||||
</div>
|
||||
<div v-else-if="recentCommissions.length === 0" class="empty-state">
|
||||
<el-empty description="暂无数据" />
|
||||
<el-empty description="暂无数据"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="commission in recentCommissions" :key="commission.id" class="activity-item">
|
||||
<div class="commission-icon">
|
||||
<el-icon class="default-icon"><Money /></el-icon>
|
||||
<el-icon class="default-icon">
|
||||
<Money/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="activity-info">
|
||||
<div class="activity-title">+{{ commission.amount }}</div>
|
||||
<div class="activity-desc">{{ commission.description }}</div>
|
||||
<div class="activity-title">+{{ commission.commission_amount }}</div>
|
||||
<div class="activity-desc">{{ maskPhoneNumber(commission.username) + '('+ maskPhoneNumber(commission.real_name) +')' }}</div>
|
||||
<div class="activity-time">{{ formatTime(commission.created_at) }}</div>
|
||||
</div>
|
||||
<el-tag :type="getCommissionStatusType(commission.status)" size="small">
|
||||
{{ getCommissionStatusText(commission.status) }}
|
||||
</el-tag>
|
||||
<!-- <el-tag type="info" size="small">-->
|
||||
<!-- {{ commission.commission_type }}-->
|
||||
<!-- </el-tag>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,11 +218,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {ref, reactive, onMounted, computed, watch} from 'vue'
|
||||
import {useUserStore} from '@/stores/user'
|
||||
import {useRouter} from 'vue-router'
|
||||
import api from '@/utils/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {getImageUrl} from '@/utils/config'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
User,
|
||||
@@ -223,15 +238,16 @@ import {
|
||||
Coin
|
||||
} from '@element-plus/icons-vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart, PieChart, BarChart } from 'echarts/charts'
|
||||
import {use} from 'echarts/core'
|
||||
import {CanvasRenderer} from 'echarts/renderers'
|
||||
import {LineChart, PieChart, BarChart} from 'echarts/charts'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent
|
||||
} from 'echarts/components'
|
||||
import {maskPhoneNumber} from "../utils/public_method";
|
||||
|
||||
// 注册 ECharts 组件
|
||||
use([
|
||||
@@ -251,14 +267,19 @@ const router = useRouter()
|
||||
// 响应式数据
|
||||
const loading = ref(true)
|
||||
const chartLoading = ref(false)
|
||||
const userChartPeriod = ref('30d')
|
||||
const userChartPeriod = ref('30')
|
||||
|
||||
// 统计数据
|
||||
const statsData = ref({
|
||||
totalUsers: 0,
|
||||
totalCommission: 0,
|
||||
todayCommission: 0,
|
||||
pendingCommission: 0
|
||||
users: {
|
||||
total_users: 0,
|
||||
today_new_users: 0,
|
||||
active_users: 0,
|
||||
},
|
||||
commissions: {
|
||||
total_commission: 0,
|
||||
monthly_commission: 0
|
||||
}
|
||||
})
|
||||
|
||||
// 最新数据
|
||||
@@ -275,44 +296,39 @@ const userActivityData = ref([])
|
||||
const stats = computed(() => [
|
||||
{
|
||||
key: 'users',
|
||||
label: '下级用户总数',
|
||||
value: statsData.value.totalUsers,
|
||||
label: '直推用户数量',
|
||||
value: statsData.value.users.total_users,
|
||||
icon: User,
|
||||
class: 'stat-users',
|
||||
change: '+12.5%',
|
||||
changeClass: 'positive',
|
||||
changeIcon: ArrowUp
|
||||
},
|
||||
{
|
||||
key: 'active_users',
|
||||
label: '分享用户数量',
|
||||
value: statsData.value.users.active_users || 0,
|
||||
icon: TrendCharts,
|
||||
class: 'stat-users'
|
||||
},
|
||||
{
|
||||
key: 'commission',
|
||||
label: '累计佣金',
|
||||
value: `¥${statsData.value.totalCommission}`,
|
||||
label: '营收',
|
||||
value: `¥${statsData.value.commissions.total_commission}`,
|
||||
icon: Money,
|
||||
class: 'stat-commission',
|
||||
change: '+8.2%',
|
||||
changeClass: 'positive',
|
||||
changeIcon: ArrowUp
|
||||
},
|
||||
{
|
||||
key: 'today',
|
||||
label: '今日佣金',
|
||||
value: `¥${statsData.value.todayCommission}`,
|
||||
icon: Coin,
|
||||
class: 'stat-today',
|
||||
change: '+15.3%',
|
||||
changeClass: 'positive',
|
||||
changeIcon: ArrowUp
|
||||
class: 'stat-commission'
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
label: '待发放佣金',
|
||||
value: `¥${statsData.value.pendingCommission}`,
|
||||
label: '本月营收',
|
||||
value: `¥${statsData.value.commissions.monthly_commission}`,
|
||||
icon: Clock,
|
||||
class: 'stat-pending',
|
||||
change: '-2.1%',
|
||||
changeClass: 'negative',
|
||||
changeIcon: ArrowDown
|
||||
}
|
||||
class: 'stat-pending'
|
||||
},
|
||||
// {
|
||||
// key: 'nature_users',
|
||||
// label: '自然用户',
|
||||
// value: `${statsData.value.users.nature_users || 0}`,
|
||||
// icon: Clock,
|
||||
// class: 'stat-pending'
|
||||
// }
|
||||
])
|
||||
|
||||
// 用户增长图表配置
|
||||
@@ -377,7 +393,7 @@ const commissionChartOption = computed(() => ({
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: commissionChartData.value.map(item => item.date)
|
||||
data: commissionChartData.value.map(item => item.date),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
@@ -396,7 +412,7 @@ const commissionChartOption = computed(() => ({
|
||||
const commissionTypeOption = computed(() => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
formatter: '{a}:{b}<br/>数量: {c}<br/>占比:{d}%<br/>佣金金额:{e}'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
@@ -455,7 +471,19 @@ const getGreeting = () => {
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
return dayjs(time).format('MM-DD HH:mm')
|
||||
const date = dayjs(time)
|
||||
const now = dayjs()
|
||||
const diff = now.diff(date)
|
||||
|
||||
if (diff < 60000) {
|
||||
return '刚刚'
|
||||
} else if (diff < 3600000) {
|
||||
return `${Math.floor(diff / 60000)}分钟前`
|
||||
} else if (diff < 86400000) {
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
} else {
|
||||
return date.format('YYYY-MM-DD')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取佣金状态类型
|
||||
@@ -480,23 +508,24 @@ const getCommissionStatusText = (status) => {
|
||||
|
||||
// 处理统计卡片点击
|
||||
const handleStatClick = (key) => {
|
||||
switch (key) {
|
||||
case 'users':
|
||||
router.push('/users')
|
||||
break
|
||||
case 'commission':
|
||||
case 'today':
|
||||
case 'pending':
|
||||
router.push('/commissions')
|
||||
break
|
||||
}
|
||||
// switch (key) {
|
||||
// case 'users':
|
||||
// router.push('/users')
|
||||
// break
|
||||
// case 'commission':
|
||||
// case 'today':
|
||||
// case 'pending':
|
||||
// router.push('/commissions')
|
||||
// break
|
||||
// }
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getStats()
|
||||
statsData.value = response.data
|
||||
statsData.value = response.data.data
|
||||
console.log('statsData',statsData.value)
|
||||
} catch (error) {
|
||||
ElMessage.error('获取统计数据失败')
|
||||
}
|
||||
@@ -506,13 +535,11 @@ const loadStats = async () => {
|
||||
const loadChartData = async () => {
|
||||
chartLoading.value = true
|
||||
try {
|
||||
const response = await api.dashboard.getChartData()
|
||||
const data = response.data
|
||||
|
||||
userChartData.value = data.userChart || []
|
||||
commissionChartData.value = data.commissionChart || []
|
||||
commissionTypeData.value = data.commissionType || []
|
||||
userActivityData.value = data.userActivity || []
|
||||
await loadUserChart()
|
||||
await loadCommissionTrendData()
|
||||
await loadCommissionDistributionData()
|
||||
await loadRecentUsersData()
|
||||
await loadRecentCommissionsData()
|
||||
} catch (error) {
|
||||
ElMessage.error('获取图表数据失败')
|
||||
} finally {
|
||||
@@ -522,14 +549,66 @@ const loadChartData = async () => {
|
||||
|
||||
// 加载用户图表
|
||||
const loadUserChart = async () => {
|
||||
chartLoading.value = true
|
||||
try {
|
||||
const response = await api.dashboard.getUserChart({ period: userChartPeriod.value })
|
||||
userChartData.value = response.data
|
||||
const response = await api.dashboard.getUserChart({days: userChartPeriod.value})
|
||||
userChartData.value = response.data.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载佣金收入趋势数据
|
||||
const loadCommissionTrendData = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getCommissionTrend()
|
||||
commissionChartData.value = response.data.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载佣金类型分布数据
|
||||
const loadCommissionDistributionData = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getCommissionDistribution()
|
||||
var tmpData = []
|
||||
if (response.data.data.length > 0) {
|
||||
await response.data.data.forEach((item, index) => {
|
||||
tmpData.push({
|
||||
name: item.type,
|
||||
value: item.count,
|
||||
amount: item.amount,
|
||||
})
|
||||
})
|
||||
commissionTypeData.value = tmpData
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最新下级用户数据
|
||||
const loadRecentUsersData = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getRecentUsers()
|
||||
recentUsers.value = response.data.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最新营收
|
||||
const loadRecentCommissionsData = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getRecentCommissions()
|
||||
recentCommissions.value = response.data.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
chartLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,10 +616,10 @@ const loadUserChart = async () => {
|
||||
const loadRecentData = async () => {
|
||||
try {
|
||||
const [usersResponse, commissionsResponse] = await Promise.all([
|
||||
api.users.getList({ page: 1, size: 5, sort: 'created_at', order: 'desc' }),
|
||||
api.commissions.getList({ page: 1, size: 5, sort: 'created_at', order: 'desc' })
|
||||
api.users.getList({page: 1, size: 5, sort: 'created_at', order: 'desc'}),
|
||||
api.commissions.getList({page: 1, size: 5, sort: 'created_at', order: 'desc'})
|
||||
])
|
||||
|
||||
|
||||
recentUsers.value = usersResponse.data.list || []
|
||||
recentCommissions.value = commissionsResponse.data.list || []
|
||||
} catch (error) {
|
||||
@@ -555,7 +634,7 @@ const refreshData = async () => {
|
||||
await Promise.all([
|
||||
loadStats(),
|
||||
loadChartData(),
|
||||
loadRecentData()
|
||||
// loadRecentData()
|
||||
])
|
||||
ElMessage.success('数据刷新成功')
|
||||
} catch (error) {
|
||||
@@ -573,6 +652,7 @@ watch(userChartPeriod, () => {
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
refreshData()
|
||||
// console.log('statsData.users:',statsData.value.users)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -652,6 +732,43 @@ onMounted(() => {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-col {
|
||||
flex: 1;
|
||||
min-width: 200px; /* 根据需要调整最小宽度 */
|
||||
max-width: 25%; /* 5个卡片就是 100% / 5 = 20% */
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
.stat-col {
|
||||
max-width: 25%; /* 4个一行 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.stat-col {
|
||||
max-width: 33.333%; /* 3个一行 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stat-col {
|
||||
max-width: 50%; /* 2个一行 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stat-col {
|
||||
max-width: 100%; /* 1个一行 */
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
@@ -749,6 +866,7 @@ onMounted(() => {
|
||||
|
||||
.chart-container {
|
||||
height: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
@@ -759,9 +877,15 @@ onMounted(() => {
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
padding: 12px 12px 10px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.activity-item:hover {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
transform: translateX(8px);
|
||||
padding-left: 16px;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
@@ -820,19 +944,19 @@ onMounted(() => {
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.header-stats {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.stat-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
}
|
||||
960
src/views/DashboardDirectly.vue
Normal file
960
src/views/DashboardDirectly.vue
Normal file
@@ -0,0 +1,960 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<div class="dashboard-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<div class="welcome-section">
|
||||
<div class="greeting-icon">
|
||||
<el-icon>
|
||||
<View/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="greeting-text">
|
||||
<h1 class="page-title">直营代理数据统计</h1>
|
||||
<p class="page-subtitle">{{ getGreeting() }},{{ userStore.user?.realName }}!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-stats">
|
||||
<!-- <div class="quick-stat">
|
||||
<span class="stat-value">{{ statsData.users.total_users }}</span>
|
||||
<span class="stat-label">总用户数</span>
|
||||
</div> -->
|
||||
<div class="quick-stat">
|
||||
<span class="stat-value">{{ statsData.commissions.total_commission }}</span>
|
||||
<span class="stat-label">总收入</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" :icon="Refresh" @click="refreshData" :loading="loading" class="refresh-btn">
|
||||
刷新数据
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-section">
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col
|
||||
v-for="stat in stats"
|
||||
:key="stat.key"
|
||||
class="stat-col"
|
||||
>
|
||||
<div class="stat-card" :class="stat.class">
|
||||
<div class="stat-background"></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon">
|
||||
<el-icon>
|
||||
<component :is="stat.icon"/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-number" :data-value="stat.value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<div class="stat-change" :class="stat.changeClass">
|
||||
<el-icon><component :is="stat.changeIcon" /></el-icon>
|
||||
{{ stat.change }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-decoration"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<!-- 用户增长趋势 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">用户增长趋势</span>
|
||||
<el-select v-model="userChartPeriod" size="small" style="width: 100px" @change="loadUserChart">
|
||||
<el-option label="7天" value="7"/>
|
||||
<el-option label="30天" value="30"/>
|
||||
<el-option label="90天" value="90"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<div v-if="userChartData.length==0" class="empty-state">
|
||||
<el-empty description="暂无数据"/>
|
||||
</div>
|
||||
<v-chart :option="userChartOption" :loading="chartLoading"/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 营收收入趋势 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">用户转账流水</span>
|
||||
<el-tag type="success" size="small">近30天</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<div v-if="commissionChartData.length==0" class="empty-state">
|
||||
<el-empty description="暂无数据"/>
|
||||
</div>
|
||||
<v-chart v-else :option="commissionChartOption" :loading="chartLoading"/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 业务分析图表 -->
|
||||
<!-- <el-row :gutter="20" class="business-charts-row">-->
|
||||
<!-- 营收类型分布 -->
|
||||
<!-- <el-col :xs="24" :lg="12">-->
|
||||
<!-- <el-card class="chart-card" shadow="hover">-->
|
||||
<!-- <template #header>-->
|
||||
<!-- <div class="card-header">-->
|
||||
<!-- <span class="card-title">营收类型分布</span>-->
|
||||
<!-- <el-tag type="primary" size="small">总览</el-tag>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <div class="chart-container">-->
|
||||
<!-- <div v-if="commissionTypeData.length==0" class="empty-state">-->
|
||||
<!-- <el-empty description="暂无数据"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- <v-chart :option="commissionTypeOption" :loading="chartLoading"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </el-card>-->
|
||||
<!-- </el-col>-->
|
||||
|
||||
<!-- 用户活跃度 -->
|
||||
<!-- <el-col :xs="24" :lg="12">-->
|
||||
<!-- <el-card class="chart-card" shadow="hover">-->
|
||||
<!-- <template #header>-->
|
||||
<!-- <div class="card-header">-->
|
||||
<!-- <span class="card-title">用户活跃度</span>-->
|
||||
<!-- <el-tag type="info" size="small">近7天</el-tag>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <div class="chart-container">-->
|
||||
<!-- <v-chart :option="userActivityOption" :loading="chartLoading"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </el-card>-->
|
||||
<!-- </el-col>-->
|
||||
<!-- </el-row>-->
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<el-row :gutter="20" class="activity-row">
|
||||
<!-- 最新用户 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="activity-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">最新下级商户</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="activity-list">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-skeleton :rows="3" animated/>
|
||||
</div>
|
||||
<div v-else-if="recentUsers.length === 0" class="empty-state">
|
||||
<el-empty description="暂无数据"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="user in recentUsers" :key="user.id" class="activity-item">
|
||||
<el-avatar :size="40" :src="getImageUrl(user.avatar)">
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
<div class="activity-info">
|
||||
<div class="activity-title">{{ maskPhoneNumber(user.username) }}</div>
|
||||
<div class="activity-desc">{{ maskPhoneNumber(user.phone) }}</div>
|
||||
<div class="activity-time">{{ formatTime(user.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 最新营收 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="activity-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">最新营收</span>
|
||||
<!-- <el-link type="primary" @click="$router.push('/commissions')">查看全部</el-link> -->
|
||||
</div>
|
||||
</template>
|
||||
<div class="activity-list">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-skeleton :rows="3" animated/>
|
||||
</div>
|
||||
<div v-else-if="recentCommissions.length === 0" class="empty-state">
|
||||
<el-empty description="暂无数据"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="commission in recentCommissions" :key="commission.id" class="activity-item">
|
||||
<div class="commission-icon">
|
||||
<el-icon class="default-icon">
|
||||
<Money/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="activity-info">
|
||||
<div class="activity-title">+{{ commission.commission_amount }}</div>
|
||||
<div class="activity-desc">{{ maskPhoneNumber(commission.username) + '('+ maskPhoneNumber(commission.real_name) +')' }}</div>
|
||||
<div class="activity-time">{{ formatTime(commission.created_at) }}</div>
|
||||
</div>
|
||||
<!-- <el-tag type="info" size="small">-->
|
||||
<!-- {{ commission.commission_type }}-->
|
||||
<!-- </el-tag>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, reactive, onMounted, computed, watch} from 'vue'
|
||||
import {useUserStore} from '@/stores/user'
|
||||
import {useRouter} from 'vue-router'
|
||||
import api from '@/utils/api'
|
||||
import {getImageUrl} from '@/utils/config'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
User,
|
||||
View,
|
||||
TrendCharts,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Refresh,
|
||||
UserFilled,
|
||||
Money,
|
||||
Clock,
|
||||
Coin,
|
||||
Avatar,
|
||||
Watermelon,
|
||||
Sell
|
||||
} from '@element-plus/icons-vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import {use} from 'echarts/core'
|
||||
import {CanvasRenderer} from 'echarts/renderers'
|
||||
import {LineChart, PieChart, BarChart} from 'echarts/charts'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent
|
||||
} from 'echarts/components'
|
||||
import {maskPhoneNumber} from "../utils/public_method";
|
||||
|
||||
// 注册 ECharts 组件
|
||||
use([
|
||||
CanvasRenderer,
|
||||
LineChart,
|
||||
PieChart,
|
||||
BarChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent
|
||||
])
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(true)
|
||||
const chartLoading = ref(false)
|
||||
const userChartPeriod = ref('30')
|
||||
|
||||
// 统计数据
|
||||
const statsData = ref({
|
||||
users: {
|
||||
// total_users: 0,
|
||||
total_operated: 0,
|
||||
agent_share_users: 0,
|
||||
operated_share_users: 0,
|
||||
total_directly_agents: 0
|
||||
},
|
||||
commissions: {
|
||||
total_commission: 0,
|
||||
directly_agents_income: 0,
|
||||
active_users_income: 0,
|
||||
operated_commission: 0,
|
||||
get_commission: 0,
|
||||
loading_commission: 0,
|
||||
}
|
||||
})
|
||||
|
||||
// 最新数据
|
||||
const recentUsers = ref([])
|
||||
const recentCommissions = ref([])
|
||||
|
||||
// 图表数据
|
||||
const userChartData = ref([])
|
||||
const commissionChartData = ref([])
|
||||
const commissionTypeData = ref([])
|
||||
const userActivityData = ref([])
|
||||
|
||||
// 统计卡片配置
|
||||
const stats = computed(() => [
|
||||
{
|
||||
key: 'users',
|
||||
label: '直销商户人数',
|
||||
value: statsData.value.users.total_directly_agents,
|
||||
icon: Avatar,
|
||||
class: 'stat-users1',
|
||||
change: `直销商户利润${statsData.value.commissions.directly_agents_income}`,
|
||||
changeClass: statsData.value.commissions.directly_agents_income >= 0 ? 'positive' : 'negative',
|
||||
changeIcon: statsData.value.commissions.directly_agents_income >= 0 ? 'ArrowUp' : 'ArrowDown'
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
label: '直销商户分享人数',
|
||||
value: statsData.value.users.agent_share_users,
|
||||
icon: User,
|
||||
class: 'stat-users2',
|
||||
change: `直销商户分享利润${statsData.value.commissions.active_users_income}`,
|
||||
changeClass: statsData.value.commissions.active_users_income >= 0 ? 'positive' : 'negative',
|
||||
changeIcon: statsData.value.commissions.active_users_income >= 0 ? 'ArrowUp' : 'ArrowDown'
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
label: '直营人数',
|
||||
value: `${statsData.value.users.total_operated}`,
|
||||
icon: Money,
|
||||
class: 'stat-users3',
|
||||
change: `直营营收${statsData.value.commissions.operated_commission}`,
|
||||
changeClass: statsData.value.commissions.operated_commission >= 0 ? 'positive' : 'negative',
|
||||
changeIcon: statsData.value.commissions.operated_commission >= 0 ? 'ArrowUp' : 'ArrowDown'
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
label: '直营商户人数',
|
||||
value: `${statsData.value.users.active_users}`,
|
||||
icon: Watermelon,
|
||||
class: 'stat-users4',
|
||||
change: `已提现营收${statsData.value.commissions.get_commission}`,
|
||||
changeClass: statsData.value.commissions.get_commission >= 0 ? 'positive' : 'negative',
|
||||
changeIcon: statsData.value.commissions.get_commission >= 0 ? 'ArrowUp' : 'ArrowDown'
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
label: '直营商户分享人数',
|
||||
value: `${statsData.value.users.operated_share_users}`,
|
||||
icon: Sell,
|
||||
class: 'stat-users5',
|
||||
change: `待提现营收${statsData.value.commissions.loading_commission}`,
|
||||
changeClass: statsData.value.commissions.loading_commission >= 0 ? 'positive' : 'negative',
|
||||
changeIcon: statsData.value.commissions.loading_commission >= 0 ? 'ArrowUp' : 'ArrowDown'
|
||||
}
|
||||
])
|
||||
|
||||
// 用户增长图表配置
|
||||
const userChartOption = computed(() => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: userChartData.value.map(item => item.date)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
name: '新增用户',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: userChartData.value.map(item => item.count),
|
||||
itemStyle: {
|
||||
color: '#409EFF'
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: 'rgba(64, 158, 255, 0.3)'
|
||||
}, {
|
||||
offset: 1, color: 'rgba(64, 158, 255, 0.1)'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}]
|
||||
}))
|
||||
|
||||
// 营收收入图表配置
|
||||
const commissionChartOption = computed(() => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: commissionChartData.value.map(item => item.date),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
name: '营收收入',
|
||||
type: 'bar',
|
||||
data: commissionChartData.value.map(item => item.amount),
|
||||
itemStyle: {
|
||||
color: '#67C23A'
|
||||
}
|
||||
}]
|
||||
}))
|
||||
|
||||
// 营收类型分布图表配置
|
||||
const commissionTypeOption = computed(() => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a}:{b}<br/>数量: {c}<br/>占比:{d}%<br/>营收金额:{e}'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [{
|
||||
name: '营收类型',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: commissionTypeData.value,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}))
|
||||
|
||||
// 用户活跃度图表配置
|
||||
const userActivityOption = computed(() => ({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: userActivityData.value.map(item => item.date)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
name: '活跃用户',
|
||||
type: 'line',
|
||||
data: userActivityData.value.map(item => item.count),
|
||||
itemStyle: {
|
||||
color: '#E6A23C'
|
||||
}
|
||||
}]
|
||||
}))
|
||||
|
||||
// 获取问候语
|
||||
const getGreeting = () => {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 12) return '早上好'
|
||||
if (hour < 18) return '下午好'
|
||||
return '晚上好'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
const date = dayjs(time)
|
||||
const now = dayjs()
|
||||
const diff = now.diff(date)
|
||||
|
||||
if (diff < 60000) {
|
||||
return '刚刚'
|
||||
} else if (diff < 3600000) {
|
||||
return `${Math.floor(diff / 60000)}分钟前`
|
||||
} else if (diff < 86400000) {
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
} else {
|
||||
return date.format('YYYY-MM-DD')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getStatsAgentDirectly()
|
||||
statsData.value = response.data.data
|
||||
console.log('statsData',statsData.value)
|
||||
} catch (error) {
|
||||
ElMessage.error('获取统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载图表数据
|
||||
const loadChartData = async () => {
|
||||
chartLoading.value = true
|
||||
try {
|
||||
await loadUserChart()
|
||||
await loadCommissionTrendData()
|
||||
await loadCommissionDistributionData()
|
||||
await loadRecentUsersData()
|
||||
await loadRecentCommissionsData()
|
||||
} catch (error) {
|
||||
ElMessage.error('获取图表数据失败')
|
||||
} finally {
|
||||
chartLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户图表
|
||||
const loadUserChart = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getUserChart({days: userChartPeriod.value})
|
||||
userChartData.value = response.data.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载营收收入趋势数据
|
||||
const loadCommissionTrendData = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getCommissionTrend()
|
||||
commissionChartData.value = response.data.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载营收类型分布数据
|
||||
const loadCommissionDistributionData = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getCommissionDistribution()
|
||||
var tmpData = []
|
||||
if (response.data.data.length > 0) {
|
||||
await response.data.data.forEach((item, index) => {
|
||||
tmpData.push({
|
||||
name: item.type,
|
||||
value: item.count,
|
||||
amount: item.amount,
|
||||
})
|
||||
})
|
||||
commissionTypeData.value = tmpData
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最新下级用户数据
|
||||
const loadRecentUsersData = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getRecentUsers()
|
||||
recentUsers.value = response.data.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最新营收
|
||||
const loadRecentCommissionsData = async () => {
|
||||
try {
|
||||
const response = await api.dashboard.getRecentCommissions()
|
||||
recentCommissions.value = response.data.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取用户图表数据失败')
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载最新数据
|
||||
const loadRecentData = async () => {
|
||||
try {
|
||||
const [usersResponse, commissionsResponse] = await Promise.all([
|
||||
api.users.getList({page: 1, size: 5, sort: 'created_at', order: 'desc'}),
|
||||
api.commissions.getList({page: 1, size: 5, sort: 'created_at', order: 'desc'})
|
||||
])
|
||||
|
||||
recentUsers.value = usersResponse.data.list || []
|
||||
recentCommissions.value = commissionsResponse.data.list || []
|
||||
} catch (error) {
|
||||
ElMessage.error('获取最新数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
loadStats(),
|
||||
loadChartData(),
|
||||
// loadRecentData()
|
||||
])
|
||||
ElMessage.success('数据刷新成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('数据刷新失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听图表周期变化
|
||||
watch(userChartPeriod, () => {
|
||||
loadUserChart()
|
||||
})
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
refreshData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 24px 32px;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.greeting-icon {
|
||||
font-size: 48px;
|
||||
margin-right: 20px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.quick-stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-col {
|
||||
flex: 1;
|
||||
min-width: 200px; /* 根据需要调整最小宽度 */
|
||||
max-width: 20%; /* 5个卡片就是 100% / 5 = 20% */
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
.stat-col {
|
||||
max-width: 25%; /* 4个一行 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.stat-col {
|
||||
max-width: 33.333%; /* 3个一行 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stat-col {
|
||||
max-width: 50%; /* 2个一行 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.stat-col {
|
||||
max-width: 100%; /* 1个一行 */
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 48px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.stat-users1 .stat-icon {
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.stat-users2 .stat-icon {
|
||||
color: #89ce49;
|
||||
}
|
||||
|
||||
.stat-users3 .stat-icon {
|
||||
color: #3ac2b0;
|
||||
}
|
||||
|
||||
.stat-users4 .stat-icon {
|
||||
color: #E6A23C;
|
||||
}
|
||||
|
||||
.stat-users5 .stat-icon {
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-change.positive {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.stat-change.negative {
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.charts-row,
|
||||
.business-charts-row,
|
||||
.activity-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chart-card,
|
||||
.activity-card {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 12px 10px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.activity-item:hover {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
transform: translateX(8px);
|
||||
padding-left: 16px;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.activity-desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: #C0C4CC;
|
||||
}
|
||||
|
||||
.commission-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #67C23A;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.empty-state {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
638
src/views/DirectSale.vue
Normal file
638
src/views/DirectSale.vue
Normal file
@@ -0,0 +1,638 @@
|
||||
<template>
|
||||
<div class="direct-sale-container">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h1>直营列表</h1>
|
||||
<p>查看代理下的直营用户</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="createDirectSaler">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
创建直营
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="search-section">
|
||||
<el-card>
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="关键字">
|
||||
<el-input
|
||||
v-model="searchForm.search"
|
||||
placeholder="请输入姓名、用户名或手机号"
|
||||
clearable
|
||||
style="width: 250px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search/>
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon>
|
||||
<Refresh/>
|
||||
</el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-section">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon">
|
||||
<el-icon color="#409EFF">
|
||||
<User/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ directSaleStats.total || 0 }}</div>
|
||||
<div class="stats-label">总用户数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon">
|
||||
<el-icon color="#67C23A">
|
||||
<Coin/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ directSaleStats.total_beans || 0 }}</div>
|
||||
<div class="stats-label">总融豆数量</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon">
|
||||
<el-icon color="#E6A23C">
|
||||
<Money/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ directSaleStats.today_withdrawals || 0 }}</div>
|
||||
<div class="stats-label">今日提现</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon">
|
||||
<el-icon color="#F56C6C">
|
||||
<TrendCharts/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ directSaleStats.total_withdrawals || 0 }}</div>
|
||||
<div class="stats-label">总提现金额</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 直营列表表格 -->
|
||||
<div class="table-section">
|
||||
<el-card>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="directSaleList"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID"/>
|
||||
<el-table-column prop="real_name" label="姓名"/>
|
||||
<el-table-column label="手机号">
|
||||
<template #default="{ row }">
|
||||
{{ maskPhoneNumber(row.phone) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="balance" label="融豆数量">
|
||||
<template #default="{ row }">
|
||||
<span class="beans-amount">{{ Math.abs(row.balance) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="level" label="等级">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getLevelType(row.level)">
|
||||
{{ getLevelText(row.level) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="注册时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleWithdraw(row)"
|
||||
>
|
||||
提现
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 直营用户 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑用户' : '添加直营用户'"
|
||||
width="600px"
|
||||
:before-close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="userFormRef"
|
||||
:model="userForm"
|
||||
:rules="userRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input
|
||||
v-model="userForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="真实姓名" prop="real_name">
|
||||
<el-input
|
||||
v-model="userForm.real_name"
|
||||
placeholder="请输入真实姓名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="userForm.password"
|
||||
type="password"
|
||||
:placeholder="isEdit ? '留空则不修改密码' : '请输入密码'"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="头像" prop="avatar">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:action="uploadAction"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleAvatarSuccess"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
>
|
||||
<img v-if="userForm.avatarReal" :src="getImageUrl(userForm.avatar)" class="avatar"/>
|
||||
<el-icon v-else class="avatar-uploader-icon">
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
{{ isEdit ? '更新' : '创建' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, reactive, onMounted, computed} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {
|
||||
Search,
|
||||
Refresh,
|
||||
User,
|
||||
Coin,
|
||||
Money,
|
||||
TrendCharts
|
||||
} from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
import {maskPhoneNumber} from '@/utils/public_method'
|
||||
import {useUserStore} from "@/stores/user";
|
||||
import {getImageUrl} from "@/utils/config";
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const directSaleList = ref([])
|
||||
const directSaleStats = ref({
|
||||
total: 0,
|
||||
total_beans: 0,
|
||||
today_withdrawals: 0,
|
||||
total_withdrawals: 0
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
search: '',
|
||||
})
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 创建直营
|
||||
const createDirectSaler = async () => {
|
||||
isEdit.value = false
|
||||
resetUserForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 获取直营列表
|
||||
const getDirectSaleList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
size: pagination.size,
|
||||
search: searchForm.search,
|
||||
}
|
||||
|
||||
const response = await api.directSale.listUsers(params)
|
||||
directSaleList.value = response.data.data.users
|
||||
pagination.total = response.data.data.pagination.total
|
||||
|
||||
// TODO 统计
|
||||
directSaleStats.value.total = response.data.data.pagination.total
|
||||
directSaleStats.value.total_beans = response.data.data.all_total.balance_total
|
||||
directSaleStats.value.today_withdrawals = response.data.data.all_total.withdraw_num_total
|
||||
directSaleStats.value.total_withdrawals = response.data.data.all_total.withdraw_total
|
||||
//directSaleStats.value = response.data.data.pagination
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('获取直营列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const getDirectSaleStats = async () => {
|
||||
try {
|
||||
const response = await api.directSale.getStats()
|
||||
directSaleStats.value = response.data.data
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('获取统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getDirectSaleList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.search = ''
|
||||
pagination.page = 1
|
||||
getDirectSaleList()
|
||||
}
|
||||
|
||||
// 分页大小改变
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
pagination.page = 1
|
||||
getDirectSaleList()
|
||||
}
|
||||
|
||||
// 当前页改变
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
getDirectSaleList()
|
||||
}
|
||||
|
||||
// 处理提现
|
||||
const handleWithdraw = async (row) => {
|
||||
try {
|
||||
ElMessageBox.prompt(
|
||||
`请输入提现金额`,
|
||||
'提现确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info',
|
||||
inputPattern:
|
||||
/^(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/,
|
||||
inputErrorMessage: '请输入正确提现金额',
|
||||
}
|
||||
).then(({value}) => {
|
||||
if (value > 0){
|
||||
var params = {
|
||||
userId: row.id,
|
||||
amount: value,
|
||||
}
|
||||
api.directSale.withdraw(params).then((res) => {
|
||||
ElMessage.success('提现操作成功')
|
||||
// 刷新列表和统计数据
|
||||
getDirectSaleList()
|
||||
})
|
||||
} else {
|
||||
ElMessage.error('请输入正确提现金额')
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.log(error)
|
||||
ElMessage.error('提现操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取等级类型
|
||||
const getLevelType = (level) => {
|
||||
const levelMap = {
|
||||
'normal': '',
|
||||
'vip': 'success',
|
||||
'svip': 'warning'
|
||||
}
|
||||
return levelMap[level] || ''
|
||||
}
|
||||
|
||||
// 获取等级文本
|
||||
const getLevelText = (level) => {
|
||||
const levelMap = {
|
||||
'normal': '普通',
|
||||
'vip': 'VIP',
|
||||
'svip': 'SVIP'
|
||||
}
|
||||
return levelMap[level] || '普通'
|
||||
}
|
||||
|
||||
// 直营用户
|
||||
const userStore = useUserStore()
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const submitting = ref(false)
|
||||
const userFormRef = ref()
|
||||
const userForm = reactive({
|
||||
username: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
avatar: '',
|
||||
invite: '',
|
||||
real_name: '',
|
||||
avatarReal: '', // 上传临时查看
|
||||
})
|
||||
const userRules = computed(() => ({
|
||||
password: [
|
||||
{required: !isEdit.value, message: '请输入密码', trigger: 'blur'},
|
||||
{min: 6, message: '密码长度不能少于6位', trigger: 'blur'}
|
||||
],
|
||||
real_name: [
|
||||
{required: true, message: '请输入真实姓名', trigger: 'blur'},
|
||||
{min: 2, max: 10, message: '姓名长度在 2 到 10 个字符', trigger: 'blur'}
|
||||
],
|
||||
phone: [
|
||||
{required: !isEdit.value, message: '请输入电话', trigger: 'blur'},
|
||||
{pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur'}
|
||||
],
|
||||
}))
|
||||
|
||||
// 头像上传
|
||||
const uploadAction = '/api/upload/image'
|
||||
const uploadHeaders = computed(() => {
|
||||
const token = userStore.token || localStorage.getItem('admin_token')
|
||||
if (!token) {
|
||||
console.warn('上传时未找到认证令牌')
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
// 头像上传前验证
|
||||
const beforeAvatarUpload = (file) => {
|
||||
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png'
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isJPG) {
|
||||
ElMessage.error('头像只能是 JPG/PNG 格式!')
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('头像大小不能超过 2MB!')
|
||||
}
|
||||
return isJPG && isLt2M
|
||||
}
|
||||
// 头像上传成功
|
||||
const handleAvatarSuccess = (response) => {
|
||||
if (response.success) {
|
||||
userForm.avatar = response.data.path
|
||||
userForm.avatarReal = response.data.url
|
||||
ElMessage.success('头像上传成功')
|
||||
} else {
|
||||
ElMessage.error('头像上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
userFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
userForm.username = userForm.phone
|
||||
if (!isEdit.value) {
|
||||
// 新增
|
||||
api.directSale.addUser(userForm).then((res) => {
|
||||
console.log(res)
|
||||
if (res.data.success) {
|
||||
ElMessage.success('用户创建成功')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// TODO 编辑
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getDirectSaleList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清空内容
|
||||
const resetUserForm = () => {
|
||||
Object.assign(userForm, {
|
||||
username: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
avatar: '',
|
||||
invite: '',
|
||||
real_name: '',
|
||||
avatarReal: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleDialogClose = () => {
|
||||
resetUserForm()
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
getDirectSaleList()
|
||||
// getDirectSaleStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.direct-sale-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: block;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-left p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
padding-top: 4px; /* 微调垂直对齐 */
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 32px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.beans-amount {
|
||||
color: #67C23A;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 头像上传 */
|
||||
.avatar-uploader {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-uploader:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -25,7 +25,7 @@
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名或邮箱"
|
||||
placeholder="电话号码"
|
||||
size="large"
|
||||
prefix-icon="User"
|
||||
clearable
|
||||
@@ -103,7 +103,7 @@ const loginForm = reactive({
|
||||
// 表单验证规则
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名或邮箱', trigger: 'blur' },
|
||||
{ required: true, message: '请输入电话号码', trigger: 'blur' },
|
||||
{ min: 3, message: '用户名长度不能少于3位', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
@@ -125,8 +125,11 @@ const handleLogin = async () => {
|
||||
captchaId: captchaInfo.captchaId,
|
||||
captchaText: captchaInfo.captchaText
|
||||
})
|
||||
console.log(result,'result');
|
||||
|
||||
|
||||
if (result.success) {
|
||||
console.log('登录信息:',JSON.parse(localStorage.getItem('admin_user')))
|
||||
// 保存记住我状态
|
||||
if (loginForm.remember) {
|
||||
localStorage.setItem('admin_remember', 'true')
|
||||
@@ -137,7 +140,7 @@ const handleLogin = async () => {
|
||||
}
|
||||
|
||||
// 跳转到仪表盘
|
||||
router.push('/dashboard')
|
||||
JSON.parse(localStorage.getItem('admin_user')).user_type === 'agent' ? router.push('/dashboard_agent') : router.push('/dashboard_directly')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
|
||||
@@ -9,44 +9,64 @@
|
||||
<div class="search-section">
|
||||
<el-card>
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="用户手机号">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.phone"
|
||||
placeholder="请输入用户手机号"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
v-model="searchForm.search"
|
||||
placeholder="请输入用户名或手机号"
|
||||
clearable
|
||||
style="width: 250px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账类型">
|
||||
<el-select
|
||||
v-model="searchForm.type"
|
||||
placeholder="请选择转账类型"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="转入" value="in" />
|
||||
<el-option label="转出" value="out" />
|
||||
</el-select>
|
||||
<!-- <el-form-item label="转账类型">-->
|
||||
<!-- <el-select-->
|
||||
<!-- v-model="searchForm.type"-->
|
||||
<!-- placeholder="请选择转账类型"-->
|
||||
<!-- clearable-->
|
||||
<!-- style="width: 150px"-->
|
||||
<!-- >-->
|
||||
<!-- <el-option label="全部" value=""/>-->
|
||||
<!-- <el-option label="转入" value="in"/>-->
|
||||
<!-- <el-option label="转出" value="out"/>-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="最小金额">
|
||||
<el-input
|
||||
type="number"
|
||||
v-model="searchForm.min_amount"
|
||||
placeholder="请输入最小金额"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大金额">
|
||||
<el-input
|
||||
type="number"
|
||||
v-model="searchForm.max_amount"
|
||||
placeholder="请输入最大金额"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="searchForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
v-model="searchForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
<el-icon>
|
||||
<Search/>
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
<el-icon>
|
||||
<Refresh/>
|
||||
</el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
@@ -61,10 +81,12 @@
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon">
|
||||
<el-icon color="#409EFF"><Money /></el-icon>
|
||||
<el-icon color="#409EFF">
|
||||
<Money/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ transferStats.totalAmount || 0 }}</div>
|
||||
<div class="stats-value">{{ transferStats.total_amount || 0 }}</div>
|
||||
<div class="stats-label">总转账金额</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,10 +96,12 @@
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon">
|
||||
<el-icon color="#67C23A"><TrendCharts /></el-icon>
|
||||
<el-icon color="#67C23A">
|
||||
<TrendCharts/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ transferStats.totalCount || 0 }}</div>
|
||||
<div class="stats-value">{{ transferStats.total_transfers || 0 }}</div>
|
||||
<div class="stats-label">总转账笔数</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,11 +111,13 @@
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon">
|
||||
<el-icon color="#E6A23C"><ArrowUp /></el-icon>
|
||||
<el-icon color="#E6A23C">
|
||||
<Coin/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ transferStats.inAmount || 0 }}</div>
|
||||
<div class="stats-label">转入金额</div>
|
||||
<div class="stats-value">{{ transferStats.today_amount || 0 }}</div>
|
||||
<div class="stats-label">今日账金额</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -100,11 +126,13 @@
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon">
|
||||
<el-icon color="#F56C6C"><ArrowDown /></el-icon>
|
||||
<el-icon color="#F56C6C">
|
||||
<List/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-value">{{ transferStats.outAmount || 0 }}</div>
|
||||
<div class="stats-label">转出金额</div>
|
||||
<div class="stats-value">{{ transferStats.today_transfers || 0 }}</div>
|
||||
<div class="stats-label">今日账笔数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -115,51 +143,42 @@
|
||||
<!-- 转账记录表格 -->
|
||||
<div class="table-section">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>转账记录列表</span>
|
||||
<el-button type="primary" @click="handleExport">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出记录
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="transferList"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
:data="transferList"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="记录ID" width="100" />
|
||||
<el-table-column prop="userPhone" label="用户手机号" width="120" />
|
||||
<el-table-column prop="userName" label="用户姓名" width="100" />
|
||||
<el-table-column label="转账类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.type === 'in' ? 'success' : 'danger'">
|
||||
{{ row.type === 'in' ? '转入' : '转出' }}
|
||||
</el-tag>
|
||||
<el-table-column prop="id" label="记录ID" width="100"/>
|
||||
<el-table-column label="转账流向">
|
||||
<template v-slot="scope">
|
||||
{{
|
||||
scope.row.from_real_name + '(' + maskPhoneNumber(scope.row.from_phone) + ')->' + scope.row.to_real_name + '(' + maskPhoneNumber(scope.row.to_phone) + ')'
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="转账金额" width="120">
|
||||
<!-- <el-table-column prop="userName" label="用户姓名" width="100" />-->
|
||||
<!-- <el-table-column label="转账类型" width="100">-->
|
||||
<!-- <template v-slot="scope">-->
|
||||
<!-- <el-tag>-->
|
||||
<!-- <span v-if="scope.row.type=='deposit'">押金</span>-->
|
||||
<!-- <span v-if="scope.row.type=='withdraw'">撤回</span>-->
|
||||
<!-- <span v-if="scope.row.type=='transfer'">转账</span>-->
|
||||
<!-- </el-tag>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<el-table-column prop="amount" label="转账金额">
|
||||
<template #default="{ row }">
|
||||
<span :class="row.type === 'in' ? 'amount-in' : 'amount-out'">
|
||||
{{ row.type === 'in' ? '+' : '-' }}{{ row.amount }}
|
||||
<!-- :class="row.type === 'in' ? 'amount-in' : 'amount-out'"-->
|
||||
<span>
|
||||
{{ row.amount }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="balance" label="余额" width="120" />
|
||||
<el-table-column prop="description" label="转账说明" min-width="150" />
|
||||
<el-table-column prop="description" label="转账说明"/>
|
||||
<el-table-column prop="createdAt" label="转账时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
{{ row.created_at }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -167,13 +186,13 @@
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -182,8 +201,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {ref, reactive, onMounted} from 'vue'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {
|
||||
Search,
|
||||
Refresh,
|
||||
@@ -194,23 +213,33 @@ import {
|
||||
Download
|
||||
} from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
import {formatIsoToCustom, maskPhoneNumber} from '@/utils/public_method'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const transferList = ref([])
|
||||
const transferStats = ref({})
|
||||
const transferStats = ref({
|
||||
completed_transfers: 0, // 已完成转移
|
||||
failed_transfers: 0, // 失败传输
|
||||
pending_transfers: 0, // 等待传输
|
||||
today_amount: 0, // 今日数量
|
||||
today_transfers: 0, // 今日转账
|
||||
total_amount: 0, // 总计数量
|
||||
total_transfers: 0 // 总计传输次数
|
||||
})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
phone: '',
|
||||
type: '',
|
||||
dateRange: []
|
||||
search: '',
|
||||
dateRange: [],
|
||||
min_amount: null,
|
||||
max_amount: null,
|
||||
})
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 20,
|
||||
size: 50,
|
||||
total: 0
|
||||
})
|
||||
|
||||
@@ -221,60 +250,46 @@ const getTransferList = async () => {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
size: pagination.size,
|
||||
phone: searchForm.phone,
|
||||
type: searchForm.type
|
||||
search: searchForm.search,
|
||||
min_amount: searchForm.min_amount,
|
||||
max_amount: searchForm.max_amount
|
||||
}
|
||||
|
||||
|
||||
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
|
||||
params.startDate = searchForm.dateRange[0]
|
||||
params.endDate = searchForm.dateRange[1]
|
||||
params.start_date = searchForm.dateRange[0]
|
||||
params.end_date = searchForm.dateRange[1].split(" ")[0] + ' 23:59:59'
|
||||
}
|
||||
|
||||
|
||||
const response = await api.transfers.getList(params)
|
||||
transferList.value = response.data.list
|
||||
pagination.total = response.data.total
|
||||
transferList.value = response.data.data.transfers
|
||||
pagination.total = response.data.data.pagination.total
|
||||
transferStats.value = response.data.data.stats
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
ElMessage.error('获取转账记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取转账统计
|
||||
const getTransferStats = async () => {
|
||||
try {
|
||||
const params = {
|
||||
phone: searchForm.phone,
|
||||
type: searchForm.type
|
||||
}
|
||||
|
||||
if (searchForm.dateRange && searchForm.dateRange.length === 2) {
|
||||
params.startDate = searchForm.dateRange[0]
|
||||
params.endDate = searchForm.dateRange[1]
|
||||
}
|
||||
|
||||
const response = await api.transfers.getStats(params)
|
||||
transferStats.value = response.data
|
||||
} catch (error) {
|
||||
ElMessage.error('获取统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getTransferList()
|
||||
getTransferStats()
|
||||
if (searchForm.min_amount > searchForm.max_amount) {
|
||||
ElMessage.error('最小金额不能超过最大金额')
|
||||
} else {
|
||||
getTransferList()
|
||||
}
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.phone = ''
|
||||
searchForm.type = ''
|
||||
searchForm.search = ''
|
||||
searchForm.dateRange = []
|
||||
searchForm.min_amount = null
|
||||
searchForm.max_amount = null
|
||||
pagination.page = 1
|
||||
getTransferList()
|
||||
getTransferStats()
|
||||
}
|
||||
|
||||
// 分页大小改变
|
||||
@@ -324,7 +339,6 @@ const getStatusText = (status) => {
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
getTransferList()
|
||||
getTransferStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,292 +4,215 @@
|
||||
<h1 class="page-title">用户管理</h1>
|
||||
<p class="page-subtitle">查看我的下级用户信息</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 搜索和操作栏 -->
|
||||
<el-card class="search-card" shadow="never">
|
||||
<el-row :gutter="20" class="search-row">
|
||||
<!-- 第一行:主要搜索条件 -->
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="搜索用户名或真实姓名"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="搜索用户名或真实姓名"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="选择状态"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="活跃" value="active" />
|
||||
<el-option label="非活跃" value="inactive" />
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6">
|
||||
<el-select
|
||||
v-model="searchForm.sort"
|
||||
placeholder="排序方式"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="注册时间(新到旧)" value="created_at_desc" />
|
||||
<el-option label="注册时间(旧到新)" value="created_at_asc" />
|
||||
<el-option label="用户名(A-Z)" value="username_asc" />
|
||||
<el-option label="用户名(Z-A)" value="username_desc" />
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6" class="button-group">
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
<el-icon>
|
||||
<Search/>
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
<el-icon>
|
||||
<Refresh/>
|
||||
</el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-col>
|
||||
|
||||
<!-- 第二行:地区筛选 -->
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6">
|
||||
<el-select
|
||||
v-model="searchForm.city"
|
||||
placeholder="选择城市"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@change="onCityChange"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option
|
||||
v-for="city in availableCities"
|
||||
:key="city"
|
||||
:label="city"
|
||||
:value="city"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6">
|
||||
<el-select
|
||||
v-model="searchForm.district"
|
||||
placeholder="选择地区"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
:disabled="!searchForm.city"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option
|
||||
v-for="district in searchDistricts"
|
||||
:key="district.id"
|
||||
:label="district.district_name"
|
||||
:value="district.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-row :gutter="20" class="stats-section">
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card class="stat-card" shadow="never">
|
||||
<el-card class="stats-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.totalUsers }}</div>
|
||||
<div class="stat-label">下级用户总数</div>
|
||||
<div class="stats-icon">
|
||||
<el-icon color="#409EFF">
|
||||
<User/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stat-value">{{ stats.total_users }}</div>
|
||||
<div class="stat-label">下级用户总数</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" color="#409EFF"><User /></el-icon>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card class="stat-card" shadow="never">
|
||||
<el-card class="stats-card" shadow="never">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.activeUsers }}</div>
|
||||
<div class="stat-label">活跃用户</div>
|
||||
<div class="stats-icon">
|
||||
<el-icon class="stat-icon" color="#67C23A">
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stat-value">{{ stats.active_users }}</div>
|
||||
<div class="stat-label">活跃用户</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" color="#67C23A"><UserFilled /></el-icon>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card class="stat-card" shadow="never">
|
||||
<el-card class="stats-card" shadow="never">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">¥{{ formatBalance(stats.totalBalance) }}</div>
|
||||
<div class="stat-label">用户总余额</div>
|
||||
<div class="stats-icon">
|
||||
<el-icon class="stat-icon" color="#E6A23C">
|
||||
<Money/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stat-value">¥{{ Math.abs(formatBalance(stats.total_balance)) }}</div>
|
||||
<div class="stat-label">用户总余额</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" color="#E6A23C"><Money /></el-icon>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card class="stat-card" shadow="never">
|
||||
<el-card class="stats-card" shadow="never">
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.todayNewUsers }}</div>
|
||||
<div class="stat-label">今日新增</div>
|
||||
<div class="stats-icon">
|
||||
<el-icon class="stat-icon" color="#F56C6C">
|
||||
<Top/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stat-value">{{ stats.today_new_users }}</div>
|
||||
<div class="stat-label">今日新增</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon" color="#F56C6C"><Plus /></el-icon>
|
||||
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<el-card class="table-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">用户列表 ({{ pagination.total }})</span>
|
||||
<el-button type="primary" @click="exportUsers">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出数据
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="users"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
v-loading="loading"
|
||||
:data="users"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
|
||||
<el-table-column type="index" label="#" width="60"/>
|
||||
|
||||
<el-table-column label="头像" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-avatar :size="40" :src="row.avatar">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<el-avatar :size="40" :src="row.avatar?getImageUrl(row.avatar):''">
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
sortable="custom"
|
||||
min-width="120"
|
||||
prop="username"
|
||||
label="用户名"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="user-info">
|
||||
<div class="username">{{ row.username }}</div>
|
||||
<div class="username">{{ maskPhoneNumber(row.username) }}</div>
|
||||
<div class="user-id">ID: {{ row.id }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
|
||||
<el-table-column
|
||||
prop="real_name"
|
||||
label="姓名"
|
||||
min-width="120"
|
||||
prop="real_name"
|
||||
label="姓名"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.real_name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="城市" width="100">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.city_name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="地区" width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.district_name || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="余额" width="120" sortable="custom">
|
||||
|
||||
<el-table-column label="余额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="balance-amount">¥{{ formatBalance(row.balance) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="积分" width="100" sortable="custom">
|
||||
|
||||
<el-table-column label="积分" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="points-amount">{{ formatPoints(row.points) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getUserStatusType(row)" size="small">
|
||||
{{ getUserStatusText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
|
||||
<el-table-column
|
||||
prop="created_at"
|
||||
label="注册时间"
|
||||
sortable="custom"
|
||||
width="180"
|
||||
prop="created_at"
|
||||
label="注册时间"
|
||||
width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
{{ convertToDateOnly(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="last_login_at"
|
||||
label="最后登录"
|
||||
width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.last_login_at ? formatDate(row.last_login_at) : '从未登录' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="showUserDetail(row)"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="showUserDetail(row)"
|
||||
>
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
|
||||
<!-- 用户详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="用户详情"
|
||||
width="800px"
|
||||
v-model="detailDialogVisible"
|
||||
title="用户详情"
|
||||
width="800px"
|
||||
>
|
||||
<div v-if="selectedUser" class="user-detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="头像">
|
||||
<el-avatar :size="60" :src="selectedUser.avatar">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<el-avatar :size="60" :src="getImageUrl(selectedUser.avatar)">
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</el-avatar>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名">{{ selectedUser.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名">{{ maskPhoneNumber(selectedUser.username) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="真实姓名">{{ selectedUser.real_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ selectedUser.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ maskPhoneNumber(selectedUser.phone) || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{ selectedUser.id_card || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="城市">{{ selectedUser.city_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地区">{{ selectedUser.district_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="余额">¥{{ formatBalance(selectedUser.balance) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="余额">¥{{ Math.abs(formatBalance(selectedUser.balance)) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="积分">{{ formatPoints(selectedUser.points) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间">{{ formatDate(selectedUser.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后登录">{{ selectedUser.last_login_at ? formatDate(selectedUser.last_login_at) : '从未登录' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getUserStatusType(selectedUser)" size="small">
|
||||
{{ getUserStatusText(selectedUser) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间">{{ convertToDateOnly(selectedUser.created_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -297,19 +220,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, User, UserFilled, Money, Plus, Download } from '@element-plus/icons-vue'
|
||||
import {ref, reactive, onMounted, computed} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {Search, Refresh, User, UserFilled, Money, Plus, Download} from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
import {getImageUrl} from "@/utils/config";
|
||||
import {maskPhoneNumber, convertToDateOnly} from "@/utils/public_method"
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const users = ref([])
|
||||
const stats = ref({
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
totalBalance: 0,
|
||||
todayNewUsers: 0
|
||||
total_users: 0,
|
||||
active_users: 0,
|
||||
total_balance: 0,
|
||||
today_new_users: 0
|
||||
})
|
||||
const availableCities = ref([])
|
||||
const searchDistricts = ref([])
|
||||
@@ -339,16 +264,23 @@ const fetchUsers = async () => {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status,
|
||||
city: searchForm.city,
|
||||
district: searchForm.district,
|
||||
sort: searchForm.sort
|
||||
search: searchForm.keyword,
|
||||
sort_by: 'created_at_desc',
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* status: searchForm.status,
|
||||
* city: searchForm.city,
|
||||
* district: searchForm.district,
|
||||
* sort: searchForm.sort
|
||||
* @type {axios.AxiosResponse<any>}
|
||||
*/
|
||||
|
||||
const response = await api.users.getUsers(params)
|
||||
users.value = response.data.users
|
||||
pagination.total = response.data.total
|
||||
users.value = response.data.data.users
|
||||
pagination.total = response.data.data.pagination.total
|
||||
stats.value = response.data.data.stats
|
||||
console.log(stats.value)
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
ElMessage.error('获取用户列表失败')
|
||||
@@ -361,7 +293,6 @@ const fetchUsers = async () => {
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await api.users.getStats()
|
||||
stats.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
}
|
||||
@@ -377,10 +308,6 @@ const handleSearch = () => {
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
keyword: '',
|
||||
status: '',
|
||||
city: '',
|
||||
district: '',
|
||||
sort: 'created_at_desc'
|
||||
})
|
||||
pagination.page = 1
|
||||
fetchUsers()
|
||||
@@ -399,7 +326,7 @@ const handleSizeChange = (size) => {
|
||||
}
|
||||
|
||||
// 排序变化
|
||||
const handleSortChange = ({ prop, order }) => {
|
||||
const handleSortChange = ({prop, order}) => {
|
||||
if (order === 'ascending') {
|
||||
searchForm.sort = `${prop}_asc`
|
||||
} else if (order === 'descending') {
|
||||
@@ -450,7 +377,7 @@ const getUserStatusType = (user) => {
|
||||
const lastLogin = new Date(user.last_login_at)
|
||||
const now = new Date()
|
||||
const daysDiff = (now - lastLogin) / (1000 * 60 * 60 * 24)
|
||||
|
||||
|
||||
if (daysDiff <= 7) return 'success'
|
||||
if (daysDiff <= 30) return 'warning'
|
||||
return 'danger'
|
||||
@@ -462,7 +389,7 @@ const getUserStatusText = (user) => {
|
||||
const lastLogin = new Date(user.last_login_at)
|
||||
const now = new Date()
|
||||
const daysDiff = (now - lastLogin) / (1000 * 60 * 60 * 24)
|
||||
|
||||
|
||||
if (daysDiff <= 1) return '今日活跃'
|
||||
if (daysDiff <= 7) return '本周活跃'
|
||||
if (daysDiff <= 30) return '本月活跃'
|
||||
@@ -471,7 +398,8 @@ const getUserStatusText = (user) => {
|
||||
|
||||
// 格式化余额
|
||||
const formatBalance = (balance) => {
|
||||
return (balance || 0).toFixed(2)
|
||||
if (balance === null || balance === undefined) return '0.00'
|
||||
return Number(balance).toFixed(2)
|
||||
}
|
||||
|
||||
// 格式化积分
|
||||
@@ -488,7 +416,7 @@ const formatDate = (date) => {
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchUsers()
|
||||
fetchStats()
|
||||
// fetchStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -526,22 +454,32 @@ onMounted(() => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
.stats-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.stats-card {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
font-size: 32px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
@@ -552,31 +490,10 @@ onMounted(() => {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 32px;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
import {resolve} from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -16,7 +16,7 @@ export default defineConfig({
|
||||
port: 5174,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://192.168.1.43:3002',
|
||||
changeOrigin: true
|
||||
},
|
||||
// '/admin': {
|
||||
@@ -24,7 +24,7 @@ export default defineConfig({
|
||||
// changeOrigin: true
|
||||
// },
|
||||
'/uploads': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://192.168.1.43:3002',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user