内容更改
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>后台管理系统</title>
|
<title>供应商后台管理系统</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<el-aside :width="isCollapse ? '64px' : '200px'" class="sidebar">
|
<el-aside :width="isCollapse ? '64px' : '200px'" class="sidebar">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img v-if="!isCollapse" src="/logo.svg" alt="Logo" class="logo-img" />
|
<img v-if="!isCollapse" src="/logo.svg" alt="Logo" class="logo-img" />
|
||||||
<span v-if="!isCollapse" class="logo-text">后台管理</span>
|
<span v-if="!isCollapse" class="logo-text">供应商后台管理</span>
|
||||||
<el-icon v-else class="logo-icon"><Setting /></el-icon>
|
<el-icon v-else class="logo-icon"><Setting /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
<el-icon><List /></el-icon>
|
<el-icon><List /></el-icon>
|
||||||
<template #title>订单管理</template>
|
<template #title>订单管理</template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item v-if="userStore.isAdmin" index="/income">
|
||||||
|
<el-icon><Money /></el-icon>
|
||||||
|
<template #title>提现管理</template>
|
||||||
|
</el-menu-item>
|
||||||
<el-menu-item index="/profile">
|
<el-menu-item index="/profile">
|
||||||
<el-icon><UserFilled /></el-icon>
|
<el-icon><UserFilled /></el-icon>
|
||||||
<template #title>个人资料</template>
|
<template #title>个人资料</template>
|
||||||
@@ -252,7 +256,7 @@ const breadcrumbs = computed(() => {
|
|||||||
matched.forEach(item => {
|
matched.forEach(item => {
|
||||||
if (item.path !== '/') {
|
if (item.path !== '/') {
|
||||||
breadcrumbList.push({
|
breadcrumbList.push({
|
||||||
title: item.meta.title.replace(' - 后台管理系统', ''),
|
title: item.meta.title.replace(' - 供应商后台管理系统', ''),
|
||||||
path: item.path
|
path: item.path
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,16 @@ const routes = [
|
|||||||
requiresAdmin: true
|
requiresAdmin: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'income',
|
||||||
|
name: 'Income',
|
||||||
|
component: () => import('@/views/Income.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '提现管理',
|
||||||
|
icon: 'Money',
|
||||||
|
requiresAdmin: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
|
|||||||
183
src/utils/api.js
183
src/utils/api.js
@@ -1,4 +1,166 @@
|
|||||||
import {apiRequest, midRequest} from './request'
|
// 工厂函数(复用拦截器逻辑)
|
||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage, ElLoading } from 'element-plus'
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
let loadingInstance = null
|
||||||
|
let requestCount = 0
|
||||||
|
let isLoggingOut = false // 防止重复登出
|
||||||
|
|
||||||
|
export const createRequest = (baseURL) => {
|
||||||
|
const request = axios.create({
|
||||||
|
baseURL,
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const showLoading = () => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
request.interceptors.request.use(
|
||||||
|
(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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
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('登录已过期,请重新登录')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除本地存储
|
||||||
|
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
|
||||||
|
case 400:
|
||||||
|
ElMessage.error(response.data.message || '请求参数错误')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
ElMessage.error(response.data.error.message || '请求失败')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error('网络错误,请检查网络连接')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
// 生成不同的实例
|
||||||
|
export const apiRequest = createRequest(import.meta.env.VITE_API_BASE_URL || '/api')
|
||||||
|
export const midRequest = createRequest(import.meta.env.VITE_UPLOAD_BASE_URL || '/mid')
|
||||||
|
|
||||||
|
|
||||||
// API接口定义
|
// API接口定义
|
||||||
@@ -60,7 +222,10 @@ const api = {
|
|||||||
getAttributes: (productId) => apiRequest.get(`/products/${productId}/attributes`),
|
getAttributes: (productId) => apiRequest.get(`/products/${productId}/attributes`),
|
||||||
createAttribute: (productId, data) => apiRequest.post(`/products/${productId}/attributes`, data),
|
createAttribute: (productId, data) => apiRequest.post(`/products/${productId}/attributes`, data),
|
||||||
updateAttribute: (productId, attrId, data) => apiRequest.put(`/products/${productId}/attributes/${attrId}`, data),
|
updateAttribute: (productId, attrId, data) => apiRequest.put(`/products/${productId}/attributes/${attrId}`, data),
|
||||||
deleteAttribute: (productId, attrId) => apiRequest.delete(`/products/${productId}/attributes/${attrId}`)
|
deleteAttribute: (productId, attrId) => apiRequest.delete(`/products/${productId}/attributes/${attrId}`),
|
||||||
|
|
||||||
|
// 订单相关
|
||||||
|
delivery: (data) => apiRequest.post('/orders/delivery', data),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 新的规格管理系统(笛卡尔积)
|
// 新的规格管理系统(笛卡尔积)
|
||||||
@@ -80,6 +245,20 @@ const api = {
|
|||||||
updateCombination: (id, data) => apiRequest.put(`/specifications/combinations/${id}`, data),
|
updateCombination: (id, data) => apiRequest.put(`/specifications/combinations/${id}`, data),
|
||||||
deleteCombination: (id) => apiRequest.delete(`/specifications/combinations/${id}`)
|
deleteCombination: (id) => apiRequest.delete(`/specifications/combinations/${id}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 提现管理
|
||||||
|
income: {
|
||||||
|
getIncomeList: (params) => apiRequest.get('/income', {params}),
|
||||||
|
createIncome: (data) => apiRequest.post('/income', data),
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 为了向后兼容,添加直接的 get、post 等方法
|
||||||
|
get: (url, config) => apiRequest.get(url, config),
|
||||||
|
post: (url, data, config) => apiRequest.post(url, data, config),
|
||||||
|
put: (url, data, config) => apiRequest.put(url, data, config),
|
||||||
|
delete: (url, config) => apiRequest.delete(url, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
// 工厂函数(复用拦截器逻辑)
|
|
||||||
import axios from 'axios'
|
|
||||||
import { ElMessage, ElLoading } from 'element-plus'
|
|
||||||
import NProgress from 'nprogress'
|
|
||||||
let loadingInstance = null
|
|
||||||
let requestCount = 0
|
|
||||||
let isLoggingOut = false // 防止重复登出
|
|
||||||
|
|
||||||
export const createRequest = (baseURL) => {
|
|
||||||
const request = axios.create({
|
|
||||||
baseURL,
|
|
||||||
timeout: 10000,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const showLoading = () => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 请求拦截器
|
|
||||||
request.interceptors.request.use(
|
|
||||||
(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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 响应拦截器
|
|
||||||
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('登录已过期,请重新登录')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除本地存储
|
|
||||||
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
|
|
||||||
case 400:
|
|
||||||
ElMessage.error(response.data.message || '请求参数错误')
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
ElMessage.error(response.data.error.message || '请求失败')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ElMessage.error('网络错误,请检查网络连接')
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
// 生成不同的实例
|
|
||||||
export const apiRequest = createRequest(import.meta.env.VITE_API_BASE_URL || '/api')
|
|
||||||
export const midRequest = createRequest(import.meta.env.VITE_UPLOAD_BASE_URL || '/mid')
|
|
||||||
export const statsRequest = createRequest(import.meta.env.VITE_STATS_BASE_URL || '/stats')
|
|
||||||
494
src/views/Income.vue
Normal file
494
src/views/Income.vue
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
<template>
|
||||||
|
<div class="income-container">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1 class="page-title">
|
||||||
|
提现管理
|
||||||
|
</h1>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
class="withdraw-btn"
|
||||||
|
@click="getIncome"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
申请提现
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<div class="table-container">
|
||||||
|
<el-table
|
||||||
|
:data="incomeList"
|
||||||
|
v-loading="loading"
|
||||||
|
stripe
|
||||||
|
class="income-table"
|
||||||
|
header-row-class-name="table-header"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
|
<el-table-column prop="user.name" label="用户" width="120" />
|
||||||
|
<el-table-column prop="amount" label="金额" width="120" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="amount-text">¥{{ row.amount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="getStatusType(row.status)"
|
||||||
|
size="small"
|
||||||
|
class="status-tag"
|
||||||
|
>
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createdAt" label="创建时间" min-width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="time-text">{{ formatTime(row.createdAt) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页器 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.limit"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="loadIncomeList"
|
||||||
|
@current-change="loadIncomeList"
|
||||||
|
class="custom-pagination"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提现对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
title="申请提现"
|
||||||
|
v-model="drawerVisible"
|
||||||
|
width="500px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<el-form
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
ref="formRef"
|
||||||
|
label-width="80px"
|
||||||
|
>
|
||||||
|
<el-form-item label="提现金额" prop="amount">
|
||||||
|
<el-input
|
||||||
|
v-model="form.amount"
|
||||||
|
type="number"
|
||||||
|
placeholder="请输入提现金额"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<span>¥</span>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<el-button @click="drawerVisible = false" size="large">
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="submitForm"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
确认提现
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import api from '@/utils/api'
|
||||||
|
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus'
|
||||||
|
import { Money, Plus, Check } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const incomeList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const drawerVisible = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
const form = reactive({
|
||||||
|
amount: 0
|
||||||
|
})
|
||||||
|
const rules = reactive({
|
||||||
|
amount: [
|
||||||
|
{ required: true, message: '请输入金额', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 0.01, message: '金额必须大于0', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 状态类型映射
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'pending': 'warning',
|
||||||
|
'processing': 'primary',
|
||||||
|
'completed': 'success',
|
||||||
|
'failed': 'danger',
|
||||||
|
'cancelled': 'info'
|
||||||
|
}
|
||||||
|
return statusMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态文本映射
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'pending': '待处理',
|
||||||
|
'processing': '处理中',
|
||||||
|
'completed': '已完成',
|
||||||
|
'failed': '失败',
|
||||||
|
'cancelled': '已取消'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间格式化
|
||||||
|
const formatTime = (time) => {
|
||||||
|
if (!time) return '-'
|
||||||
|
const date = new Date(time)
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadIncomeList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await api.income.getIncomeList({
|
||||||
|
page: pagination.page,
|
||||||
|
limit: pagination.limit
|
||||||
|
})
|
||||||
|
incomeList.value = data
|
||||||
|
pagination.total = data.total
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取提现列表失败:', error)
|
||||||
|
ElMessage.error('获取提现列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIncome = async () => {
|
||||||
|
drawerVisible.value = true
|
||||||
|
// 重置表单
|
||||||
|
form.amount = 0
|
||||||
|
if (formRef.value) {
|
||||||
|
formRef.value.resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await formRef.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
const response = await api.income.createIncome({
|
||||||
|
amount: Number(form.amount),
|
||||||
|
userId: userStore.user.id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
ElMessage.success('提现申请提交成功')
|
||||||
|
loadIncomeList()
|
||||||
|
drawerVisible.value = false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提现失败:', error)
|
||||||
|
ElMessage.error('提现申请失败,请重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
loadIncomeList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 页面容器 */
|
||||||
|
.income-container {
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面头部 */
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 1.5em;;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格容器 */
|
||||||
|
.table-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.income-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.table-header) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.table-header th) {
|
||||||
|
background-color: #f8fafc !important;
|
||||||
|
color: #475569;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row) {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row:hover) {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-text {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页器 */
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-pagination) {
|
||||||
|
--el-pagination-button-color: #64748b;
|
||||||
|
--el-pagination-hover-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框样式 */
|
||||||
|
:deep(.withdraw-dialog) {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.withdraw-dialog .el-dialog__header) {
|
||||||
|
color: white;
|
||||||
|
padding: 20px 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.withdraw-dialog .el-dialog__title) {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.withdraw-dialog .el-dialog__headerbtn .el-dialog__close) {
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.withdraw-dialog .el-dialog__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
padding: 32px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.withdraw-form .el-form-item__label) {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.amount-input .el-input__inner) {
|
||||||
|
padding-left: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.amount-input .el-input__inner:focus) {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-symbol {
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.income-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withdraw-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
padding: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.withdraw-dialog) {
|
||||||
|
width: 90% !important;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
padding: 24px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions .el-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.income-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
padding: 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .cell) {
|
||||||
|
padding: 8px 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
padding: 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-pagination .el-pagination__sizes),
|
||||||
|
:deep(.custom-pagination .el-pagination__jump) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画优化 */
|
||||||
|
:deep(.el-loading-mask) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格条纹优化 */
|
||||||
|
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
|
||||||
|
background-color: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
:deep(.el-table__body-wrapper)::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body-wrapper)::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
{{ formatDate(row.created_at) }}
|
{{ formatDate(row.created_at) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商品信息" min-width="200">
|
<el-table-column label="商品信息">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="order-items">
|
<div class="order-items">
|
||||||
<div v-for="item in row.items" :key="item.id" class="order-item">
|
<div v-for="item in row.items" :key="item.id" class="order-item">
|
||||||
@@ -97,6 +97,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column prop="producer" label="供应商">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.producer }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="logistics_info" label="物流信息">
|
||||||
|
<template #default="{ row }">
|
||||||
|
快递单号:{{ row.delivery_code || '暂无' }}
|
||||||
|
<br>
|
||||||
|
物流公司:{{ row.logistics_company || '暂无' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" size="small" @click="viewOrder(row)">
|
<el-button type="primary" size="small" @click="viewOrder(row)">
|
||||||
@@ -206,7 +218,34 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<el-button type="primary" size="small" @click="deliveryDialogVisible = true; deliveryForm.id = selectedOrder.id;" class="delivery-btn">
|
||||||
|
发货
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 发货对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="deliveryDialogVisible"
|
||||||
|
title="发货"
|
||||||
|
width="400px"
|
||||||
|
:before-close="closeDialog"
|
||||||
|
>
|
||||||
|
<el-form :model="deliveryForm" :rules="deliveryRules" ref="deliveryFormRef">
|
||||||
|
<el-form-item label="物流公司" prop="logistics_company">
|
||||||
|
<el-input v-model="deliveryForm.logistics_company" placeholder="请输入物流公司名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="物流单号" prop="logistics_no">
|
||||||
|
<el-input v-model="deliveryForm.logistics_no" placeholder="请输入物流单号" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="submitDelivery">
|
||||||
|
提交发货
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -222,6 +261,7 @@ import dayjs from 'dayjs';
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const orders = ref([]);
|
const orders = ref([]);
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
|
const deliveryDialogVisible = ref(false);
|
||||||
const selectedOrder = ref(null);
|
const selectedOrder = ref(null);
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
@@ -237,6 +277,12 @@ const pagination = reactive({
|
|||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deliveryForm = reactive({
|
||||||
|
logistics_company: '',
|
||||||
|
logistics_no: '',
|
||||||
|
id: '',
|
||||||
|
});
|
||||||
|
|
||||||
// 加载订单列表
|
// 加载订单列表
|
||||||
const loadOrders = async () => {
|
const loadOrders = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -259,6 +305,7 @@ const loadOrders = async () => {
|
|||||||
pagination.total = data.data.total;
|
pagination.total = data.data.total;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('加载订单列表失败');
|
ElMessage.error('加载订单列表失败');
|
||||||
|
console.error('加载订单列表失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -314,6 +361,12 @@ const updateOrderStatus = async (order, newStatus) => {
|
|||||||
// 关闭对话框
|
// 关闭对话框
|
||||||
const closeDialog = () => {
|
const closeDialog = () => {
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
|
deliveryDialogVisible.value = false;
|
||||||
|
deliveryForm.value = {
|
||||||
|
logistics_company: '',
|
||||||
|
logistics_no: '',
|
||||||
|
id: '',
|
||||||
|
};
|
||||||
selectedOrder.value = null;
|
selectedOrder.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -340,6 +393,23 @@ const getStatusText = (status) => {
|
|||||||
return texts[status] || status;
|
return texts[status] || status;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deliveryRules = reactive({
|
||||||
|
logistics_company: [{ required: true, message: '请输入物流公司名称', trigger: 'blur' }],
|
||||||
|
logistics_no: [{ required: true, message: '请输入物流单号', trigger: 'blur' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitDelivery = async () => {
|
||||||
|
try {
|
||||||
|
await api.products.delivery(deliveryForm);
|
||||||
|
ElMessage.success('发货成功');
|
||||||
|
closeDialog();
|
||||||
|
loadOrders();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('发货失败');
|
||||||
|
console.error('发货失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 格式化日期
|
// 格式化日期
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||||
@@ -419,4 +489,11 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delivery-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -496,7 +496,8 @@ const loadProduct = async () => {
|
|||||||
console.log('表单数据:', form);
|
console.log('表单数据:', form);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('加载商品信息失败',error)
|
ElMessage.error('加载商品信息失败');
|
||||||
|
console.error('加载商品信息失败:', error);
|
||||||
// router.back()
|
// router.back()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,17 @@ export default defineConfig({
|
|||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://192.168.0.11:3000',
|
target: 'http://192.168.0.12:3008',
|
||||||
changeOrigin: true
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
},
|
},
|
||||||
'/mid': {
|
'/mid': {
|
||||||
target: 'http://localhost:3005',
|
target: 'http://192.168.0.4:3005/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/mid/, '')
|
rewrite: (path) => path.replace(/^\/mid/, '')
|
||||||
},
|
},
|
||||||
'/uploads': {
|
'/uploads': {
|
||||||
target: 'http://localhost:3000',
|
target: 'http://192.168.0.12:3008',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user