内容更改
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>后台管理系统</title>
|
||||
<title>供应商后台管理系统</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<el-aside :width="isCollapse ? '64px' : '200px'" class="sidebar">
|
||||
<div class="logo">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
<el-icon><List /></el-icon>
|
||||
<template #title>订单管理</template>
|
||||
</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-icon><UserFilled /></el-icon>
|
||||
<template #title>个人资料</template>
|
||||
@@ -252,7 +256,7 @@ const breadcrumbs = computed(() => {
|
||||
matched.forEach(item => {
|
||||
if (item.path !== '/') {
|
||||
breadcrumbList.push({
|
||||
title: item.meta.title.replace(' - 后台管理系统', ''),
|
||||
title: item.meta.title.replace(' - 供应商后台管理系统', ''),
|
||||
path: item.path
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,7 +82,16 @@ const routes = [
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'income',
|
||||
name: 'Income',
|
||||
component: () => import('@/views/Income.vue'),
|
||||
meta: {
|
||||
title: '提现管理',
|
||||
icon: 'Money',
|
||||
requiresAdmin: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '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接口定义
|
||||
@@ -60,7 +222,10 @@ const api = {
|
||||
getAttributes: (productId) => apiRequest.get(`/products/${productId}/attributes`),
|
||||
createAttribute: (productId, data) => apiRequest.post(`/products/${productId}/attributes`, 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),
|
||||
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
|
||||
@@ -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) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品信息" min-width="200">
|
||||
<el-table-column label="商品信息">
|
||||
<template #default="{ row }">
|
||||
<div class="order-items">
|
||||
<div v-for="item in row.items" :key="item.id" class="order-item">
|
||||
@@ -97,6 +97,18 @@
|
||||
</div>
|
||||
</template>
|
||||
</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">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="viewOrder(row)">
|
||||
@@ -206,7 +218,34 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-button type="primary" size="small" @click="deliveryDialogVisible = true; deliveryForm.id = selectedOrder.id;" class="delivery-btn">
|
||||
发货
|
||||
</el-button>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -222,6 +261,7 @@ import dayjs from 'dayjs';
|
||||
const loading = ref(false);
|
||||
const orders = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const deliveryDialogVisible = ref(false);
|
||||
const selectedOrder = ref(null);
|
||||
|
||||
const filters = reactive({
|
||||
@@ -237,6 +277,12 @@ const pagination = reactive({
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const deliveryForm = reactive({
|
||||
logistics_company: '',
|
||||
logistics_no: '',
|
||||
id: '',
|
||||
});
|
||||
|
||||
// 加载订单列表
|
||||
const loadOrders = async () => {
|
||||
loading.value = true;
|
||||
@@ -259,6 +305,7 @@ const loadOrders = async () => {
|
||||
pagination.total = data.data.total;
|
||||
} catch (error) {
|
||||
ElMessage.error('加载订单列表失败');
|
||||
console.error('加载订单列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -314,6 +361,12 @@ const updateOrderStatus = async (order, newStatus) => {
|
||||
// 关闭对话框
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false;
|
||||
deliveryDialogVisible.value = false;
|
||||
deliveryForm.value = {
|
||||
logistics_company: '',
|
||||
logistics_no: '',
|
||||
id: '',
|
||||
};
|
||||
selectedOrder.value = null;
|
||||
};
|
||||
|
||||
@@ -340,6 +393,23 @@ const getStatusText = (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) => {
|
||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||
@@ -419,4 +489,11 @@ onMounted(() => {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delivery-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -496,7 +496,8 @@ const loadProduct = async () => {
|
||||
console.log('表单数据:', form);
|
||||
|
||||
} catch (error) {
|
||||
ElMessage.error('加载商品信息失败',error)
|
||||
ElMessage.error('加载商品信息失败');
|
||||
console.error('加载商品信息失败:', error);
|
||||
// router.back()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,16 +16,17 @@ export default defineConfig({
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://192.168.0.11:3000',
|
||||
changeOrigin: true
|
||||
target: 'http://192.168.0.12:3008',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/mid': {
|
||||
target: 'http://localhost:3005',
|
||||
target: 'http://192.168.0.4:3005/',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/mid/, '')
|
||||
},
|
||||
'/uploads': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://192.168.0.12:3008',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user