Compare commits
9 Commits
1c461ab5c3
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d725b51cb | |||
| 857f46ad17 | |||
| e251153d14 | |||
| 3662ee072b | |||
| 3bdb2ff5f3 | |||
| b2bbbf8c44 | |||
| 01bda65010 | |||
| a27327c7fa | |||
| 48156d72aa |
6
.env.production
Normal file
6
.env.production
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 鐢熶骇鐜閰嶇疆
|
||||||
|
# 浣跨敤鐩稿璺緞锛岄€氳繃Nginx浠g悊鍒板悗绔?
|
||||||
|
# API鍩虹URL锛堢浉瀵硅矾寰勶紝閫氳繃Nginx浠g悊锛?# 閲嶈锛氬繀椤讳娇鐢ㄧ浉瀵硅矾寰?/api锛屼笉瑕佷娇鐢ㄥ畬鏁碪RL
|
||||||
|
VITE_API_BASE_URL=/api
|
||||||
|
|
||||||
|
# PingPong妯″紡锛坰andbox/production锛?VITE_PINGPONG_MODE=sandbox
|
||||||
@@ -73,7 +73,7 @@ MTKJPAY-FRONT/
|
|||||||
在 `src/api/request.js` 中配置后端API地址:
|
在 `src/api/request.js` 中配置后端API地址:
|
||||||
```javascript
|
```javascript
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: 'http://localhost:8082/api',
|
baseURL: '/api', // 使用相对路径,通过Nginx代理
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|||||||
77
src/App.vue
77
src/App.vue
@@ -9,14 +9,22 @@
|
|||||||
<el-header>
|
<el-header>
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1>MT Pay 管理系统</h1>
|
<h1>MT Pay 管理系统</h1>
|
||||||
<el-menu
|
<div class="nav-menu">
|
||||||
mode="horizontal"
|
<router-link
|
||||||
:default-active="activeIndex"
|
to="/manage/order"
|
||||||
router
|
class="nav-item"
|
||||||
>
|
:class="{ active: activeIndex === '/manage/order' || activeIndex.startsWith('/manage/order') }"
|
||||||
<el-menu-item index="/query">订单查询</el-menu-item>
|
>
|
||||||
<el-menu-item index="/manage/product">商品管理</el-menu-item>
|
订单管理
|
||||||
</el-menu>
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
to="/manage/product"
|
||||||
|
class="nav-item"
|
||||||
|
:class="{ active: activeIndex === '/manage/product' || activeIndex.startsWith('/manage/product') }"
|
||||||
|
>
|
||||||
|
商品管理
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main>
|
<el-main>
|
||||||
@@ -56,6 +64,7 @@ const activeIndex = computed(() => route.path)
|
|||||||
color: white;
|
color: white;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
@@ -64,6 +73,58 @@ const activeIndex = computed(() => route.path)
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 导航菜单样式 - 直接显示,不隐藏 */
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 20px;
|
||||||
|
line-height: 60px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
border-bottom: 2px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端适配 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-content {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 0 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.header-content h1 {
|
.header-content h1 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -42,3 +42,14 @@ export function calculateCurrencyConversion(data) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询订单列表(支持分页和多条件查询)
|
||||||
|
*/
|
||||||
|
export function queryOrders(query) {
|
||||||
|
return request({
|
||||||
|
url: '/order/query',
|
||||||
|
method: 'post',
|
||||||
|
data: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,3 +65,30 @@ export function uploadProductImage(file) {
|
|||||||
// 注意:不设置 Content-Type,让浏览器自动设置(包含 boundary)
|
// 注意:不设置 Content-Type,让浏览器自动设置(包含 boundary)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下架商品
|
||||||
|
* 下架后商品所有SKU库存改为0,链接失效无法再被访问
|
||||||
|
*/
|
||||||
|
export function offShelfProductById(id) {
|
||||||
|
return request({
|
||||||
|
url: `/product/${id}/off-shelf`,
|
||||||
|
method: 'put'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询商品列表(支持多条件查询)
|
||||||
|
* @param {Object} query - 查询条件
|
||||||
|
* @param {string} query.name - 商品名称(模糊查询)
|
||||||
|
* @param {string} query.linkCode - 商品链接码(精确查询)
|
||||||
|
* @param {string} query.status - 商品状态(ACTIVE-上架,INACTIVE-下架)
|
||||||
|
* @param {string} query.salesRegion - 发售地区(货币代码,如:MYR, PHP, THB, VND, SGD, CNY, USD等)
|
||||||
|
*/
|
||||||
|
export function queryProducts(query) {
|
||||||
|
return request({
|
||||||
|
url: '/product/query',
|
||||||
|
method: 'post',
|
||||||
|
data: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ request.interceptors.request.use(
|
|||||||
if (config.data instanceof FormData) {
|
if (config.data instanceof FormData) {
|
||||||
delete config.headers['Content-Type']
|
delete config.headers['Content-Type']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只有访问ERP接口时才添加Token
|
||||||
|
// 客户接口(商品、订单等)不需要Token
|
||||||
|
const isErpApi = config.url?.includes('/erp/')
|
||||||
|
if (isErpApi) {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
if (token) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
@@ -48,7 +59,32 @@ request.interceptors.response.use(
|
|||||||
let message = '请求失败'
|
let message = '请求失败'
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const data = error.response.data
|
const data = error.response.data
|
||||||
message = data?.message || `请求失败: ${error.response.status}`
|
const status = error.response.status
|
||||||
|
|
||||||
|
// 处理401未授权错误(Token过期或无效)
|
||||||
|
// 只有访问ERP接口时才需要处理认证错误
|
||||||
|
if (status === 401 || data?.code === '4002' || data?.code === '7004') {
|
||||||
|
// 清除Token和用户信息
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('userInfo')
|
||||||
|
|
||||||
|
// 只有访问ERP相关接口或管理页面时才跳转到登录页
|
||||||
|
const isErpApi = error.config?.url?.includes('/erp/')
|
||||||
|
const isManagePage = window.location.pathname.startsWith('/manage')
|
||||||
|
|
||||||
|
if (isErpApi || isManagePage) {
|
||||||
|
if (window.location.pathname !== '/login' && window.location.pathname !== '/register') {
|
||||||
|
ElMessage.error('登录已过期,请重新登录')
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message = data?.message || '登录已过期,请重新登录'
|
||||||
|
} else {
|
||||||
|
message = data?.message || `请求失败: ${status}`
|
||||||
|
}
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = '网络错误,请检查网络连接'
|
message = '网络错误,请检查网络连接'
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
76
src/api/user.js
Normal file
76
src/api/user.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户注册
|
||||||
|
*/
|
||||||
|
export function register(data) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/user/register',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
*/
|
||||||
|
export function login(data) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/user/login',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息(通过Token)
|
||||||
|
*/
|
||||||
|
export function getCurrentUser() {
|
||||||
|
return request({
|
||||||
|
url: '/erp/user/info',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息
|
||||||
|
*/
|
||||||
|
export function getCurrentUserInfo() {
|
||||||
|
return request({
|
||||||
|
url: '/erp/user/info',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
*/
|
||||||
|
export function updateUserInfo(data) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/user/info',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
*/
|
||||||
|
export function changePassword(data) {
|
||||||
|
return request({
|
||||||
|
url: '/erp/user/change-password',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
export function logout() {
|
||||||
|
return request({
|
||||||
|
url: '/erp/user/logout',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@@ -165,6 +165,46 @@ const zh = {
|
|||||||
currencySGD: '新加坡元',
|
currencySGD: '新加坡元',
|
||||||
currencyHKD: '港币',
|
currencyHKD: '港币',
|
||||||
currencyPHP: '菲律宾比索'
|
currencyPHP: '菲律宾比索'
|
||||||
|
},
|
||||||
|
// 用户相关
|
||||||
|
user: {
|
||||||
|
login: '登录',
|
||||||
|
register: '注册',
|
||||||
|
logout: '退出登录',
|
||||||
|
username: '账号',
|
||||||
|
password: '密码',
|
||||||
|
nickName: '用户名称',
|
||||||
|
phone: '手机号',
|
||||||
|
email: '邮箱',
|
||||||
|
storeCode: '店铺号',
|
||||||
|
loginTitle: '用户登录',
|
||||||
|
registerTitle: '用户注册',
|
||||||
|
loginSuccess: '登录成功',
|
||||||
|
registerSuccess: '注册成功',
|
||||||
|
logoutSuccess: '退出登录成功',
|
||||||
|
usernameRequired: '请输入账号',
|
||||||
|
passwordRequired: '请输入密码',
|
||||||
|
storeCodeRequired: '请输入店铺号',
|
||||||
|
usernamePlaceholder: '请输入账号(3-50个字符)',
|
||||||
|
passwordPlaceholder: '请输入密码(6-20个字符)',
|
||||||
|
nickNamePlaceholder: '请输入用户名称(可选)',
|
||||||
|
phonePlaceholder: '请输入手机号(可选)',
|
||||||
|
emailPlaceholder: '请输入邮箱(可选)',
|
||||||
|
storeCodePlaceholder: '请输入店铺号',
|
||||||
|
usernameInvalid: '账号只能包含字母、数字和下划线',
|
||||||
|
passwordInvalid: '密码长度必须在6-20个字符之间',
|
||||||
|
phoneInvalid: '手机号格式不正确',
|
||||||
|
emailInvalid: '邮箱格式不正确',
|
||||||
|
noAccount: '还没有账号?',
|
||||||
|
hasAccount: '已有账号?',
|
||||||
|
goRegister: '立即注册',
|
||||||
|
goLogin: '立即登录',
|
||||||
|
rememberMe: '记住我',
|
||||||
|
forgotPassword: '忘记密码?',
|
||||||
|
userInfo: '用户信息',
|
||||||
|
welcome: '欢迎',
|
||||||
|
lastLoginTime: '最后登录时间',
|
||||||
|
lastLoginIp: '最后登录IP'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,6 +367,46 @@ const en = {
|
|||||||
currencySGD: 'Singapore Dollar',
|
currencySGD: 'Singapore Dollar',
|
||||||
currencyHKD: 'Hong Kong Dollar',
|
currencyHKD: 'Hong Kong Dollar',
|
||||||
currencyPHP: 'Philippine Peso'
|
currencyPHP: 'Philippine Peso'
|
||||||
|
},
|
||||||
|
// 用户相关
|
||||||
|
user: {
|
||||||
|
login: 'Login',
|
||||||
|
register: 'Register',
|
||||||
|
logout: 'Logout',
|
||||||
|
username: 'Username',
|
||||||
|
password: 'Password',
|
||||||
|
nickName: 'Nick Name',
|
||||||
|
phone: 'Phone',
|
||||||
|
email: 'Email',
|
||||||
|
storeCode: 'Store Code',
|
||||||
|
loginTitle: 'User Login',
|
||||||
|
registerTitle: 'User Register',
|
||||||
|
loginSuccess: 'Login successful',
|
||||||
|
registerSuccess: 'Registration successful',
|
||||||
|
logoutSuccess: 'Logout successful',
|
||||||
|
usernameRequired: 'Please enter username',
|
||||||
|
passwordRequired: 'Please enter password',
|
||||||
|
storeCodeRequired: 'Please enter store code',
|
||||||
|
usernamePlaceholder: 'Please enter username (3-50 characters)',
|
||||||
|
passwordPlaceholder: 'Please enter password (6-20 characters)',
|
||||||
|
nickNamePlaceholder: 'Please enter nick name (optional)',
|
||||||
|
phonePlaceholder: 'Please enter phone number (optional)',
|
||||||
|
emailPlaceholder: 'Please enter email (optional)',
|
||||||
|
storeCodePlaceholder: 'Please enter store code',
|
||||||
|
usernameInvalid: 'Username can only contain letters, numbers and underscores',
|
||||||
|
passwordInvalid: 'Password length must be between 6-20 characters',
|
||||||
|
phoneInvalid: 'Invalid phone number format',
|
||||||
|
emailInvalid: 'Invalid email format',
|
||||||
|
noAccount: 'No account yet?',
|
||||||
|
hasAccount: 'Already have an account?',
|
||||||
|
goRegister: 'Register now',
|
||||||
|
goLogin: 'Login now',
|
||||||
|
rememberMe: 'Remember me',
|
||||||
|
forgotPassword: 'Forgot password?',
|
||||||
|
userInfo: 'User Information',
|
||||||
|
welcome: 'Welcome',
|
||||||
|
lastLoginTime: 'Last Login Time',
|
||||||
|
lastLoginIp: 'Last Login IP'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,6 +566,46 @@ const may = {
|
|||||||
currencySGD: 'Dolar Singapura',
|
currencySGD: 'Dolar Singapura',
|
||||||
currencyHKD: 'Dolar Hong Kong',
|
currencyHKD: 'Dolar Hong Kong',
|
||||||
currencyPHP: 'Peso Filipina'
|
currencyPHP: 'Peso Filipina'
|
||||||
|
},
|
||||||
|
// 用户相关
|
||||||
|
user: {
|
||||||
|
login: 'Log Masuk',
|
||||||
|
register: 'Daftar',
|
||||||
|
logout: 'Log Keluar',
|
||||||
|
username: 'Nama Pengguna',
|
||||||
|
password: 'Kata Laluan',
|
||||||
|
nickName: 'Nama Panggilan',
|
||||||
|
phone: 'Telefon',
|
||||||
|
email: 'E-mel',
|
||||||
|
storeCode: 'Kod Kedai',
|
||||||
|
loginTitle: 'Log Masuk Pengguna',
|
||||||
|
registerTitle: 'Pendaftaran Pengguna',
|
||||||
|
loginSuccess: 'Log masuk berjaya',
|
||||||
|
registerSuccess: 'Pendaftaran berjaya',
|
||||||
|
logoutSuccess: 'Log keluar berjaya',
|
||||||
|
usernameRequired: 'Sila masukkan nama pengguna',
|
||||||
|
passwordRequired: 'Sila masukkan kata laluan',
|
||||||
|
storeCodeRequired: 'Sila masukkan kod kedai',
|
||||||
|
usernamePlaceholder: 'Sila masukkan nama pengguna (3-50 aksara)',
|
||||||
|
passwordPlaceholder: 'Sila masukkan kata laluan (6-20 aksara)',
|
||||||
|
nickNamePlaceholder: 'Sila masukkan nama panggilan (pilihan)',
|
||||||
|
phonePlaceholder: 'Sila masukkan nombor telefon (pilihan)',
|
||||||
|
emailPlaceholder: 'Sila masukkan e-mel (pilihan)',
|
||||||
|
storeCodePlaceholder: 'Sila masukkan kod kedai',
|
||||||
|
usernameInvalid: 'Nama pengguna hanya boleh mengandungi huruf, nombor dan garis bawah',
|
||||||
|
passwordInvalid: 'Panjang kata laluan mestilah antara 6-20 aksara',
|
||||||
|
phoneInvalid: 'Format nombor telefon tidak sah',
|
||||||
|
emailInvalid: 'Format e-mel tidak sah',
|
||||||
|
noAccount: 'Belum ada akaun?',
|
||||||
|
hasAccount: 'Sudah ada akaun?',
|
||||||
|
goRegister: 'Daftar sekarang',
|
||||||
|
goLogin: 'Log masuk sekarang',
|
||||||
|
rememberMe: 'Ingat saya',
|
||||||
|
forgotPassword: 'Lupa kata laluan?',
|
||||||
|
userInfo: 'Maklumat Pengguna',
|
||||||
|
welcome: 'Selamat datang',
|
||||||
|
lastLoginTime: 'Masa Log Masuk Terakhir',
|
||||||
|
lastLoginIp: 'IP Log Masuk Terakhir'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +712,46 @@ const fil = {
|
|||||||
currencySGD: 'Singapore Dollar',
|
currencySGD: 'Singapore Dollar',
|
||||||
currencyHKD: 'Hong Kong Dollar',
|
currencyHKD: 'Hong Kong Dollar',
|
||||||
currencyPHP: 'Philippine Peso'
|
currencyPHP: 'Philippine Peso'
|
||||||
|
},
|
||||||
|
// 用户相关
|
||||||
|
user: {
|
||||||
|
login: 'Mag-login',
|
||||||
|
register: 'Magrehistro',
|
||||||
|
logout: 'Mag-logout',
|
||||||
|
username: 'Username',
|
||||||
|
password: 'Password',
|
||||||
|
nickName: 'Palayaw',
|
||||||
|
phone: 'Telepono',
|
||||||
|
email: 'Email',
|
||||||
|
storeCode: 'Store Code',
|
||||||
|
loginTitle: 'User Login',
|
||||||
|
registerTitle: 'User Register',
|
||||||
|
loginSuccess: 'Matagumpay na pag-login',
|
||||||
|
registerSuccess: 'Matagumpay na pagrehistro',
|
||||||
|
logoutSuccess: 'Matagumpay na pag-logout',
|
||||||
|
usernameRequired: 'Mangyaring maglagay ng username',
|
||||||
|
passwordRequired: 'Mangyaring maglagay ng password',
|
||||||
|
storeCodeRequired: 'Mangyaring maglagay ng store code',
|
||||||
|
usernamePlaceholder: 'Mangyaring maglagay ng username (3-50 characters)',
|
||||||
|
passwordPlaceholder: 'Mangyaring maglagay ng password (6-20 characters)',
|
||||||
|
nickNamePlaceholder: 'Mangyaring maglagay ng palayaw (opsyonal)',
|
||||||
|
phonePlaceholder: 'Mangyaring maglagay ng numero ng telepono (opsyonal)',
|
||||||
|
emailPlaceholder: 'Mangyaring maglagay ng email (opsyonal)',
|
||||||
|
storeCodePlaceholder: 'Mangyaring maglagay ng store code',
|
||||||
|
usernameInvalid: 'Ang username ay maaari lamang maglaman ng mga titik, numero at underscore',
|
||||||
|
passwordInvalid: 'Ang haba ng password ay dapat nasa pagitan ng 6-20 characters',
|
||||||
|
phoneInvalid: 'Hindi wasto ang format ng numero ng telepono',
|
||||||
|
emailInvalid: 'Hindi wasto ang format ng email',
|
||||||
|
noAccount: 'Wala pang account?',
|
||||||
|
hasAccount: 'Mayroon nang account?',
|
||||||
|
goRegister: 'Magrehistro ngayon',
|
||||||
|
goLogin: 'Mag-login ngayon',
|
||||||
|
rememberMe: 'Tandaan ako',
|
||||||
|
forgotPassword: 'Nakalimutan ang password?',
|
||||||
|
userInfo: 'Impormasyon ng User',
|
||||||
|
welcome: 'Maligayang pagdating',
|
||||||
|
lastLoginTime: 'Huling Oras ng Login',
|
||||||
|
lastLoginIp: 'Huling IP ng Login'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,6 +910,46 @@ const th = {
|
|||||||
currencySGD: 'ดอลลาร์สิงคโปร์',
|
currencySGD: 'ดอลลาร์สิงคโปร์',
|
||||||
currencyHKD: 'ดอลลาร์ฮ่องกง',
|
currencyHKD: 'ดอลลาร์ฮ่องกง',
|
||||||
currencyPHP: 'เปโซฟิลิปปินส์'
|
currencyPHP: 'เปโซฟิลิปปินส์'
|
||||||
|
},
|
||||||
|
// 用户相关
|
||||||
|
user: {
|
||||||
|
login: 'เข้าสู่ระบบ',
|
||||||
|
register: 'ลงทะเบียน',
|
||||||
|
logout: 'ออกจากระบบ',
|
||||||
|
username: 'ชื่อผู้ใช้',
|
||||||
|
password: 'รหัสผ่าน',
|
||||||
|
nickName: 'ชื่อเล่น',
|
||||||
|
phone: 'เบอร์โทรศัพท์',
|
||||||
|
email: 'อีเมล',
|
||||||
|
storeCode: 'รหัสร้านค้า',
|
||||||
|
loginTitle: 'เข้าสู่ระบบผู้ใช้',
|
||||||
|
registerTitle: 'ลงทะเบียนผู้ใช้',
|
||||||
|
loginSuccess: 'เข้าสู่ระบบสำเร็จ',
|
||||||
|
registerSuccess: 'ลงทะเบียนสำเร็จ',
|
||||||
|
logoutSuccess: 'ออกจากระบบสำเร็จ',
|
||||||
|
usernameRequired: 'กรุณากรอกชื่อผู้ใช้',
|
||||||
|
passwordRequired: 'กรุณากรอกรหัสผ่าน',
|
||||||
|
storeCodeRequired: 'กรุณากรอกรหัสร้านค้า',
|
||||||
|
usernamePlaceholder: 'กรุณากรอกชื่อผู้ใช้ (3-50 ตัวอักษร)',
|
||||||
|
passwordPlaceholder: 'กรุณากรอกรหัสผ่าน (6-20 ตัวอักษร)',
|
||||||
|
nickNamePlaceholder: 'กรุณากรอกชื่อเล่น (ไม่บังคับ)',
|
||||||
|
phonePlaceholder: 'กรุณากรอกเบอร์โทรศัพท์ (ไม่บังคับ)',
|
||||||
|
emailPlaceholder: 'กรุณากรอกอีเมล (ไม่บังคับ)',
|
||||||
|
storeCodePlaceholder: 'กรุณากรอกรหัสร้านค้า',
|
||||||
|
usernameInvalid: 'ชื่อผู้ใช้สามารถมีได้เฉพาะตัวอักษร ตัวเลข และขีดล่าง',
|
||||||
|
passwordInvalid: 'ความยาวรหัสผ่านต้องอยู่ระหว่าง 6-20 ตัวอักษร',
|
||||||
|
phoneInvalid: 'รูปแบบเบอร์โทรศัพท์ไม่ถูกต้อง',
|
||||||
|
emailInvalid: 'รูปแบบอีเมลไม่ถูกต้อง',
|
||||||
|
noAccount: 'ยังไม่มีบัญชี?',
|
||||||
|
hasAccount: 'มีบัญชีแล้ว?',
|
||||||
|
goRegister: 'ลงทะเบียนตอนนี้',
|
||||||
|
goLogin: 'เข้าสู่ระบบตอนนี้',
|
||||||
|
rememberMe: 'จำฉันไว้',
|
||||||
|
forgotPassword: 'ลืมรหัสผ่าน?',
|
||||||
|
userInfo: 'ข้อมูลผู้ใช้',
|
||||||
|
welcome: 'ยินดีต้อนรับ',
|
||||||
|
lastLoginTime: 'เวลาเข้าสู่ระบบล่าสุด',
|
||||||
|
lastLoginIp: 'IP เข้าสู่ระบบล่าสุด'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,6 +1109,46 @@ const vie = {
|
|||||||
currencySGD: 'Đô La Singapore',
|
currencySGD: 'Đô La Singapore',
|
||||||
currencyHKD: 'Đô La Hồng Kông',
|
currencyHKD: 'Đô La Hồng Kông',
|
||||||
currencyPHP: 'Peso Philippines'
|
currencyPHP: 'Peso Philippines'
|
||||||
|
},
|
||||||
|
// 用户相关
|
||||||
|
user: {
|
||||||
|
login: 'Đăng nhập',
|
||||||
|
register: 'Đăng ký',
|
||||||
|
logout: 'Đăng xuất',
|
||||||
|
username: 'Tên đăng nhập',
|
||||||
|
password: 'Mật khẩu',
|
||||||
|
nickName: 'Tên hiển thị',
|
||||||
|
phone: 'Số điện thoại',
|
||||||
|
email: 'Email',
|
||||||
|
storeCode: 'Mã cửa hàng',
|
||||||
|
loginTitle: 'Đăng nhập người dùng',
|
||||||
|
registerTitle: 'Đăng ký người dùng',
|
||||||
|
loginSuccess: 'Đăng nhập thành công',
|
||||||
|
registerSuccess: 'Đăng ký thành công',
|
||||||
|
logoutSuccess: 'Đăng xuất thành công',
|
||||||
|
usernameRequired: 'Vui lòng nhập tên đăng nhập',
|
||||||
|
passwordRequired: 'Vui lòng nhập mật khẩu',
|
||||||
|
storeCodeRequired: 'Vui lòng nhập mã cửa hàng',
|
||||||
|
usernamePlaceholder: 'Vui lòng nhập tên đăng nhập (3-50 ký tự)',
|
||||||
|
passwordPlaceholder: 'Vui lòng nhập mật khẩu (6-20 ký tự)',
|
||||||
|
nickNamePlaceholder: 'Vui lòng nhập tên hiển thị (tùy chọn)',
|
||||||
|
phonePlaceholder: 'Vui lòng nhập số điện thoại (tùy chọn)',
|
||||||
|
emailPlaceholder: 'Vui lòng nhập email (tùy chọn)',
|
||||||
|
storeCodePlaceholder: 'Vui lòng nhập mã cửa hàng',
|
||||||
|
usernameInvalid: 'Tên đăng nhập chỉ có thể chứa chữ cái, số và dấu gạch dưới',
|
||||||
|
passwordInvalid: 'Độ dài mật khẩu phải từ 6-20 ký tự',
|
||||||
|
phoneInvalid: 'Định dạng số điện thoại không hợp lệ',
|
||||||
|
emailInvalid: 'Định dạng email không hợp lệ',
|
||||||
|
noAccount: 'Chưa có tài khoản?',
|
||||||
|
hasAccount: 'Đã có tài khoản?',
|
||||||
|
goRegister: 'Đăng ký ngay',
|
||||||
|
goLogin: 'Đăng nhập ngay',
|
||||||
|
rememberMe: 'Ghi nhớ đăng nhập',
|
||||||
|
forgotPassword: 'Quên mật khẩu?',
|
||||||
|
userInfo: 'Thông tin người dùng',
|
||||||
|
welcome: 'Chào mừng',
|
||||||
|
lastLoginTime: 'Thời gian đăng nhập cuối',
|
||||||
|
lastLoginIp: 'IP đăng nhập cuối'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,6 +1255,46 @@ const id = {
|
|||||||
currencySGD: 'Dolar Singapura',
|
currencySGD: 'Dolar Singapura',
|
||||||
currencyHKD: 'Dolar Hong Kong',
|
currencyHKD: 'Dolar Hong Kong',
|
||||||
currencyPHP: 'Peso Filipina'
|
currencyPHP: 'Peso Filipina'
|
||||||
|
},
|
||||||
|
// 用户相关
|
||||||
|
user: {
|
||||||
|
login: 'Masuk',
|
||||||
|
register: 'Daftar',
|
||||||
|
logout: 'Keluar',
|
||||||
|
username: 'Nama Pengguna',
|
||||||
|
password: 'Kata Sandi',
|
||||||
|
nickName: 'Nama Panggilan',
|
||||||
|
phone: 'Telepon',
|
||||||
|
email: 'Email',
|
||||||
|
storeCode: 'Kode Toko',
|
||||||
|
loginTitle: 'Masuk Pengguna',
|
||||||
|
registerTitle: 'Pendaftaran Pengguna',
|
||||||
|
loginSuccess: 'Berhasil masuk',
|
||||||
|
registerSuccess: 'Pendaftaran berhasil',
|
||||||
|
logoutSuccess: 'Berhasil keluar',
|
||||||
|
usernameRequired: 'Silakan masukkan nama pengguna',
|
||||||
|
passwordRequired: 'Silakan masukkan kata sandi',
|
||||||
|
storeCodeRequired: 'Silakan masukkan kode toko',
|
||||||
|
usernamePlaceholder: 'Silakan masukkan nama pengguna (3-50 karakter)',
|
||||||
|
passwordPlaceholder: 'Silakan masukkan kata sandi (6-20 karakter)',
|
||||||
|
nickNamePlaceholder: 'Silakan masukkan nama panggilan (opsional)',
|
||||||
|
phonePlaceholder: 'Silakan masukkan nomor telepon (opsional)',
|
||||||
|
emailPlaceholder: 'Silakan masukkan email (opsional)',
|
||||||
|
storeCodePlaceholder: 'Silakan masukkan kode toko',
|
||||||
|
usernameInvalid: 'Nama pengguna hanya dapat berisi huruf, angka dan garis bawah',
|
||||||
|
passwordInvalid: 'Panjang kata sandi harus antara 6-20 karakter',
|
||||||
|
phoneInvalid: 'Format nomor telepon tidak valid',
|
||||||
|
emailInvalid: 'Format email tidak valid',
|
||||||
|
noAccount: 'Belum punya akun?',
|
||||||
|
hasAccount: 'Sudah punya akun?',
|
||||||
|
goRegister: 'Daftar sekarang',
|
||||||
|
goLogin: 'Masuk sekarang',
|
||||||
|
rememberMe: 'Ingat saya',
|
||||||
|
forgotPassword: 'Lupa kata sandi?',
|
||||||
|
userInfo: 'Informasi Pengguna',
|
||||||
|
welcome: 'Selamat datang',
|
||||||
|
lastLoginTime: 'Waktu Masuk Terakhir',
|
||||||
|
lastLoginIp: 'IP Masuk Terakhir'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
src/main.js
16
src/main.js
@@ -17,18 +17,14 @@ app.use(router)
|
|||||||
app.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
|
|
||||||
// 添加错误处理
|
// 添加错误处理(生产环境静默处理)
|
||||||
app.config.errorHandler = (err, instance, info) => {
|
app.config.errorHandler = (err, instance, info) => {
|
||||||
console.error('Vue错误:', err)
|
// 生产环境可以记录到错误追踪服务
|
||||||
console.error('错误信息:', info)
|
if (import.meta.env.DEV) {
|
||||||
console.error('组件实例:', instance)
|
console.error('Vue错误:', err, info)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 挂载应用
|
// 挂载应用
|
||||||
try {
|
app.mount('#app')
|
||||||
app.mount('#app')
|
|
||||||
console.log('Vue应用已成功挂载')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('应用挂载失败:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import Checkout from '../views/Checkout.vue'
|
|||||||
import PaymentResult from '../views/PaymentResult.vue'
|
import PaymentResult from '../views/PaymentResult.vue'
|
||||||
import OrderQuery from '../views/OrderQuery.vue'
|
import OrderQuery from '../views/OrderQuery.vue'
|
||||||
import ProductDetail from '../views/ProductDetail.vue'
|
import ProductDetail from '../views/ProductDetail.vue'
|
||||||
|
import Login from '../views/Login.vue'
|
||||||
|
import Register from '../views/Register.vue'
|
||||||
|
import userStore from '../store/user'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -11,6 +14,18 @@ const routes = [
|
|||||||
name: 'Home',
|
name: 'Home',
|
||||||
redirect: '/manage/product'
|
redirect: '/manage/product'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: Login,
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'Register',
|
||||||
|
component: Register,
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/product/:id',
|
path: '/product/:id',
|
||||||
name: 'ProductDetail',
|
name: 'ProductDetail',
|
||||||
@@ -63,15 +78,29 @@ const routes = [
|
|||||||
name: 'OrderQuery',
|
name: 'OrderQuery',
|
||||||
component: OrderQuery
|
component: OrderQuery
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/manage/order',
|
||||||
|
name: 'OrderManage',
|
||||||
|
component: () => import('../views/OrderManage.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/manage/product',
|
path: '/manage/product',
|
||||||
name: 'ProductManage',
|
name: 'ProductManage',
|
||||||
component: () => import('../views/ProductManage.vue')
|
component: () => import('../views/ProductManage.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/manage/product/create',
|
path: '/manage/product/create',
|
||||||
name: 'ProductCreate',
|
name: 'ProductCreate',
|
||||||
component: () => import('../views/ProductCreate.vue')
|
component: () => import('../views/ProductCreate.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/manage/user/profile',
|
||||||
|
name: 'UserProfile',
|
||||||
|
component: () => import('../views/UserProfile.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -80,9 +109,27 @@ const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加路由守卫,用于调试
|
// 路由守卫
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
console.log('路由导航:', from.path, '->', to.path)
|
// 检查是否需要认证
|
||||||
|
if (to.meta.requiresAuth) {
|
||||||
|
// 检查是否已登录
|
||||||
|
if (!userStore.state.isLoggedIn) {
|
||||||
|
// 未登录,跳转到登录页,并保存目标路由
|
||||||
|
next({
|
||||||
|
path: '/login',
|
||||||
|
query: { redirect: to.fullPath }
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已登录且访问登录/注册页,重定向到首页
|
||||||
|
if (userStore.state.isLoggedIn && (to.path === '/login' || to.path === '/register')) {
|
||||||
|
next('/manage/product')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
74
src/store/user.js
Normal file
74
src/store/user.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户状态管理
|
||||||
|
*/
|
||||||
|
const state = reactive({
|
||||||
|
// 用户信息
|
||||||
|
user: null,
|
||||||
|
// Token
|
||||||
|
token: null,
|
||||||
|
// 是否已登录
|
||||||
|
isLoggedIn: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从localStorage恢复用户信息
|
||||||
|
function initUser() {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const userInfo = localStorage.getItem('userInfo')
|
||||||
|
|
||||||
|
if (token && userInfo) {
|
||||||
|
try {
|
||||||
|
state.token = token
|
||||||
|
state.user = JSON.parse(userInfo)
|
||||||
|
state.isLoggedIn = true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('恢复用户信息失败:', e)
|
||||||
|
clearUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置用户信息和Token
|
||||||
|
function setUser(user, token) {
|
||||||
|
state.user = user
|
||||||
|
state.token = token
|
||||||
|
state.isLoggedIn = true
|
||||||
|
|
||||||
|
// 保存到localStorage
|
||||||
|
if (token) {
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除用户信息
|
||||||
|
function clearUser() {
|
||||||
|
state.user = null
|
||||||
|
state.token = null
|
||||||
|
state.isLoggedIn = false
|
||||||
|
|
||||||
|
// 清除localStorage
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('userInfo')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
function updateUser(user) {
|
||||||
|
state.user = { ...state.user, ...user }
|
||||||
|
localStorage.setItem('userInfo', JSON.stringify(state.user))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
initUser()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
state,
|
||||||
|
setUser,
|
||||||
|
clearUser,
|
||||||
|
updateUser,
|
||||||
|
initUser
|
||||||
|
}
|
||||||
|
|
||||||
164
src/views/Login.vue
Normal file
164
src/views/Login.vue
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<el-card class="login-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>用户登录</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="loginFormRef"
|
||||||
|
:model="loginForm"
|
||||||
|
:rules="loginRules"
|
||||||
|
label-width="100px"
|
||||||
|
@submit.prevent="handleLogin"
|
||||||
|
>
|
||||||
|
<el-form-item label="账号" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.username"
|
||||||
|
placeholder="请输入账号(3-50个字符)"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码(6-20个字符)"
|
||||||
|
show-password
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<div class="login-footer">
|
||||||
|
<span>还没有账号?</span>
|
||||||
|
<el-link type="primary" @click="goToRegister">
|
||||||
|
立即注册
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { login } from '../api/user'
|
||||||
|
import userStore from '../store/user'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const loginFormRef = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const loginForm = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const loginRules = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入账号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!loginFormRef.value) return
|
||||||
|
|
||||||
|
await loginFormRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await login({
|
||||||
|
username: loginForm.username,
|
||||||
|
password: loginForm.password
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.code === '0000' && response.data) {
|
||||||
|
// 保存用户信息和Token
|
||||||
|
userStore.setUser(response.data, response.data.token)
|
||||||
|
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
|
||||||
|
// 跳转到首页或之前访问的页面
|
||||||
|
const redirect = router.currentRoute.value.query.redirect || '/manage/product'
|
||||||
|
router.push(redirect)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
// 错误消息已在request拦截器中显示
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToRegister = () => {
|
||||||
|
router.push('/register')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer .el-link {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.login-card {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -314,13 +314,17 @@ const loadOrder = async () => {
|
|||||||
await loadTranslationByCurrency(order.value.currency)
|
await loadTranslationByCurrency(order.value.currency)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果订单未支付且需要货币转换,提前计算货币转换信息
|
// 如果订单未支付且需要货币转换,提前计算货币转换信息(页面加载时就转换)
|
||||||
if (order.value.paymentStatus === 'UNPAID' && order.value.currency) {
|
if (order.value.paymentStatus === 'UNPAID' && order.value.currency) {
|
||||||
// 检查是否需要货币转换(PayPal支持的货币列表)
|
// PayPal支持的货币列表(注意:CNY和MYR不支持,需要转换为USD)
|
||||||
const supportedCurrencies = ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'JPY', 'CNY', 'HKD', 'SGD', 'NZD',
|
const supportedCurrencies = ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'JPY', 'HKD', 'SGD', 'NZD',
|
||||||
'CHF', 'SEK', 'NOK', 'DKK', 'PLN', 'MXN', 'BRL', 'INR', 'KRW', 'THB']
|
'CHF', 'SEK', 'NOK', 'DKK', 'PLN', 'MXN', 'BRL', 'INR', 'KRW', 'THB']
|
||||||
|
|
||||||
const needsConversion = !supportedCurrencies.includes(order.value.currency)
|
// 检查是否需要货币转换:
|
||||||
|
// 1. CNY和MYR必须转换为USD(PayPal不支持)
|
||||||
|
// 2. 其他不支持的货币也需要转换
|
||||||
|
const mustConvert = ['CNY', 'MYR'].includes(order.value.currency)
|
||||||
|
const needsConversion = mustConvert || !supportedCurrencies.includes(order.value.currency)
|
||||||
|
|
||||||
// 如果订单还没有货币转换信息,或者需要更新,则计算
|
// 如果订单还没有货币转换信息,或者需要更新,则计算
|
||||||
if (needsConversion && (!order.value.paymentCurrency || order.value.paymentCurrency === order.value.currency)) {
|
if (needsConversion && (!order.value.paymentCurrency || order.value.paymentCurrency === order.value.currency)) {
|
||||||
@@ -340,11 +344,12 @@ const loadOrder = async () => {
|
|||||||
order.value.paymentAmount = conversion.paymentAmount
|
order.value.paymentAmount = conversion.paymentAmount
|
||||||
order.value.exchangeRate = conversion.exchangeRate
|
order.value.exchangeRate = conversion.exchangeRate
|
||||||
order.value.rateLockedAt = conversion.rateLockedAt
|
order.value.rateLockedAt = conversion.rateLockedAt
|
||||||
console.log('货币转换信息已计算并更新,支付状态:', order.value.paymentStatus)
|
console.log('货币转换信息已计算并更新(页面加载时),原始货币:', order.value.currency,
|
||||||
|
'支付货币:', order.value.paymentCurrency, '支付金额:', order.value.paymentAmount)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('计算货币转换信息失败:', error)
|
console.error('计算货币转换信息失败:', error)
|
||||||
// 不显示错误,因为不影响订单显示
|
ElMessage.warning('计算货币转换信息失败,请稍后重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,6 +382,9 @@ const handlePay = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 步骤2:构建PayPal订单创建请求
|
// 步骤2:构建PayPal订单创建请求
|
||||||
|
// 注意:如果已经有货币转换信息(页面加载时已计算),使用转换后的货币和金额
|
||||||
|
// 后端会根据currencyCode自动进行转换(CNY/MYR转USD),但这里使用原始货币和金额
|
||||||
|
// 后端会返回转换后的信息,前端显示时使用order.value.paymentCurrency和paymentAmount
|
||||||
const paypalOrderData = {
|
const paypalOrderData = {
|
||||||
intent: 'CAPTURE', // 立即捕获
|
intent: 'CAPTURE', // 立即捕获
|
||||||
referenceId: order.value.orderNo, // ERP订单号
|
referenceId: order.value.orderNo, // ERP订单号
|
||||||
@@ -407,25 +415,19 @@ const handlePay = async () => {
|
|||||||
const paypalOrder = responseData.paypalOrder || responseData // 兼容新旧格式
|
const paypalOrder = responseData.paypalOrder || responseData // 兼容新旧格式
|
||||||
const currencyConversion = responseData.currencyConversion
|
const currencyConversion = responseData.currencyConversion
|
||||||
|
|
||||||
// 如果有货币转换信息,更新订单显示
|
// 如果有货币转换信息,更新订单显示(如果页面加载时已经计算过,这里只是确认)
|
||||||
if (currencyConversion) {
|
if (currencyConversion) {
|
||||||
order.value = {
|
// 更新订单显示信息(保留原有字段,只更新货币转换相关字段)
|
||||||
...order.value,
|
order.value.originalCurrency = currencyConversion.originalCurrency
|
||||||
originalCurrency: currencyConversion.originalCurrency,
|
order.value.originalAmount = currencyConversion.originalAmount
|
||||||
originalAmount: currencyConversion.originalAmount,
|
order.value.paymentCurrency = currencyConversion.paymentCurrency
|
||||||
paymentCurrency: currencyConversion.paymentCurrency,
|
order.value.paymentAmount = currencyConversion.paymentAmount
|
||||||
paymentAmount: currencyConversion.paymentAmount,
|
order.value.exchangeRate = currencyConversion.exchangeRate
|
||||||
exchangeRate: currencyConversion.exchangeRate,
|
order.value.rateLockedAt = currencyConversion.rateLockedAt
|
||||||
rateLockedAt: currencyConversion.rateLockedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示货币转换提示
|
// 如果页面加载时已经显示过转换信息,这里不需要再次提示
|
||||||
if (currencyConversion.conversionRequired) {
|
// 只在首次转换时显示提示(但页面加载时已经计算过了,所以这里通常不需要显示)
|
||||||
ElMessage.info({
|
console.log('PayPal订单创建成功,货币转换信息已确认,支付货币:', currencyConversion.paymentCurrency)
|
||||||
message: `${t('confirm.willPayIn', [getCurrencyName(currencyConversion.paymentCurrency)])},${t('confirm.actualCost')}:${currencyConversion.paymentCurrency} ${formatPrice(currencyConversion.paymentAmount)}`,
|
|
||||||
duration: 5000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 步骤3-4:获取approval_url并跳转到PayPal登录页
|
// 步骤3-4:获取approval_url并跳转到PayPal登录页
|
||||||
|
|||||||
545
src/views/OrderManage.vue
Normal file
545
src/views/OrderManage.vue
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-manage">
|
||||||
|
<!-- 查询表单 -->
|
||||||
|
<el-card class="search-card" style="margin-bottom: 20px">
|
||||||
|
<el-form :model="queryForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="订单号">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.orderNo"
|
||||||
|
placeholder="请输入订单号"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="PayPal订单ID">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.paypalOrderId"
|
||||||
|
placeholder="请输入PayPal订单ID"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单状态">
|
||||||
|
<el-select
|
||||||
|
v-model="queryForm.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="待支付" value="PENDING" />
|
||||||
|
<el-option label="已支付" value="PAID" />
|
||||||
|
<el-option label="已发货" value="SHIPPED" />
|
||||||
|
<el-option label="已完成" value="COMPLETED" />
|
||||||
|
<el-option label="已取消" value="CANCELLED" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="支付状态">
|
||||||
|
<el-select
|
||||||
|
v-model="queryForm.paymentStatus"
|
||||||
|
placeholder="请选择支付状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="未支付" value="UNPAID" />
|
||||||
|
<el-option label="已支付" value="PAID" />
|
||||||
|
<el-option label="支付失败" value="FAILED" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="PayPal状态">
|
||||||
|
<el-select
|
||||||
|
v-model="queryForm.paypalStatus"
|
||||||
|
placeholder="请选择PayPal状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="已创建" value="CREATED" />
|
||||||
|
<el-option label="已批准" value="APPROVED" />
|
||||||
|
<el-option label="已完成" value="COMPLETED" />
|
||||||
|
<el-option label="已取消" value="VOIDED" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="客户姓名">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.customerName"
|
||||||
|
placeholder="请输入客户姓名"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="客户电话">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.customerPhone"
|
||||||
|
placeholder="请输入客户电话"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品名称">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.productName"
|
||||||
|
placeholder="请输入商品名称"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<el-card>
|
||||||
|
<el-table :data="orderList" v-loading="loading" style="width: 100%">
|
||||||
|
<!-- 订单号 -->
|
||||||
|
<el-table-column label="订单号" prop="orderNo" width="180" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 商品信息 -->
|
||||||
|
<el-table-column label="商品信息" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>
|
||||||
|
<div class="product-name">{{ row.productName }}</div>
|
||||||
|
<div class="sku-name">SKU: {{ row.skuName }}</div>
|
||||||
|
<div class="quantity">数量: {{ row.quantity }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 订单金额 -->
|
||||||
|
<el-table-column label="订单金额" width="150" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.paymentCurrency && row.paymentCurrency !== row.originalCurrency">
|
||||||
|
<div class="original-amount">{{ row.originalAmount }} {{ row.originalCurrency }}</div>
|
||||||
|
<div class="payment-amount">{{ row.paymentAmount }} {{ row.paymentCurrency }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ row.totalAmount }} {{ row.originalCurrency || row.currency }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 客户信息 -->
|
||||||
|
<el-table-column label="客户信息" min-width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>
|
||||||
|
<div>{{ row.customerName }}</div>
|
||||||
|
<div class="customer-phone">{{ row.customerPhone }}</div>
|
||||||
|
<div class="customer-email" v-if="row.customerEmail">{{ row.customerEmail }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 收货地址 -->
|
||||||
|
<el-table-column label="收货地址" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div>
|
||||||
|
{{ row.shippingCountry }} {{ row.shippingCity }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 订单状态 -->
|
||||||
|
<el-table-column label="订单状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusTagType(row.status)" size="small">
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 支付状态 -->
|
||||||
|
<el-table-column label="支付状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getPaymentStatusTagType(row.paymentStatus)" size="small">
|
||||||
|
{{ getPaymentStatusText(row.paymentStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- PayPal信息 -->
|
||||||
|
<el-table-column label="PayPal信息" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.paypalOrderId">
|
||||||
|
<div class="paypal-order-id">订单ID: {{ row.paypalOrderId }}</div>
|
||||||
|
<el-tag v-if="row.paypalStatus" :type="getPaypalStatusTagType(row.paypalStatus)" size="small" style="margin-top: 4px">
|
||||||
|
{{ row.paypalStatus }}
|
||||||
|
</el-tag>
|
||||||
|
<div v-if="row.payerEmail" class="payer-email">{{ row.payerEmail }}</div>
|
||||||
|
</div>
|
||||||
|
<span v-else class="no-paypal">未关联PayPal订单</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 创建时间 -->
|
||||||
|
<el-table-column label="创建时间" width="180" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link size="small" @click="viewOrderDetail(row.id)">
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<div class="pagination-container" v-if="pagination.total > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.pageNum"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { queryOrders } from '../api/order'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const orderList = ref([])
|
||||||
|
|
||||||
|
// 查询表单
|
||||||
|
const queryForm = reactive({
|
||||||
|
orderNo: '',
|
||||||
|
paypalOrderId: '',
|
||||||
|
merchantOrderNo: '',
|
||||||
|
status: '',
|
||||||
|
paymentStatus: '',
|
||||||
|
paypalStatus: '',
|
||||||
|
paypalPaymentStatus: '',
|
||||||
|
customerName: '',
|
||||||
|
customerPhone: '',
|
||||||
|
customerEmail: '',
|
||||||
|
productName: '',
|
||||||
|
payerEmail: '',
|
||||||
|
payerName: '',
|
||||||
|
startTime: '',
|
||||||
|
endTime: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = ref({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 查询订单列表
|
||||||
|
const handleQuery = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const query = {}
|
||||||
|
if (queryForm.orderNo && queryForm.orderNo.trim()) {
|
||||||
|
query.orderNo = queryForm.orderNo.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.paypalOrderId && queryForm.paypalOrderId.trim()) {
|
||||||
|
query.paypalOrderId = queryForm.paypalOrderId.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.merchantOrderNo && queryForm.merchantOrderNo.trim()) {
|
||||||
|
query.merchantOrderNo = queryForm.merchantOrderNo.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.status) {
|
||||||
|
query.status = queryForm.status
|
||||||
|
}
|
||||||
|
if (queryForm.paymentStatus) {
|
||||||
|
query.paymentStatus = queryForm.paymentStatus
|
||||||
|
}
|
||||||
|
if (queryForm.paypalStatus) {
|
||||||
|
query.paypalStatus = queryForm.paypalStatus
|
||||||
|
}
|
||||||
|
if (queryForm.paypalPaymentStatus) {
|
||||||
|
query.paypalPaymentStatus = queryForm.paypalPaymentStatus
|
||||||
|
}
|
||||||
|
if (queryForm.customerName && queryForm.customerName.trim()) {
|
||||||
|
query.customerName = queryForm.customerName.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.customerPhone && queryForm.customerPhone.trim()) {
|
||||||
|
query.customerPhone = queryForm.customerPhone.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.customerEmail && queryForm.customerEmail.trim()) {
|
||||||
|
query.customerEmail = queryForm.customerEmail.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.productName && queryForm.productName.trim()) {
|
||||||
|
query.productName = queryForm.productName.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.payerEmail && queryForm.payerEmail.trim()) {
|
||||||
|
query.payerEmail = queryForm.payerEmail.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.payerName && queryForm.payerName.trim()) {
|
||||||
|
query.payerName = queryForm.payerName.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.startTime && queryForm.startTime.trim()) {
|
||||||
|
query.startTime = queryForm.startTime.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.endTime && queryForm.endTime.trim()) {
|
||||||
|
query.endTime = queryForm.endTime.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加分页参数
|
||||||
|
query.pageNum = pagination.value.pageNum
|
||||||
|
query.pageSize = pagination.value.pageSize
|
||||||
|
|
||||||
|
const response = await queryOrders(query)
|
||||||
|
if (response.code === '0000' && response.data) {
|
||||||
|
const pageResult = response.data
|
||||||
|
orderList.value = pageResult.records || []
|
||||||
|
|
||||||
|
// 更新分页信息
|
||||||
|
pagination.value.total = pageResult.total || 0
|
||||||
|
pagination.value.pageNum = pageResult.current || 1
|
||||||
|
pagination.value.pageSize = pageResult.size || 10
|
||||||
|
|
||||||
|
if (orderList.value.length === 0) {
|
||||||
|
ElMessage.info('未找到符合条件的订单')
|
||||||
|
} else {
|
||||||
|
ElMessage.success(`查询到 ${pagination.value.total} 条订单,当前第 ${pagination.value.pageNum} 页`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '查询订单列表失败')
|
||||||
|
orderList.value = []
|
||||||
|
pagination.value.total = 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询订单列表失败:', error)
|
||||||
|
ElMessage.error('查询订单列表失败')
|
||||||
|
orderList.value = []
|
||||||
|
pagination.value.total = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置查询条件
|
||||||
|
const handleReset = () => {
|
||||||
|
Object.assign(queryForm, {
|
||||||
|
orderNo: '',
|
||||||
|
paypalOrderId: '',
|
||||||
|
merchantOrderNo: '',
|
||||||
|
status: '',
|
||||||
|
paymentStatus: '',
|
||||||
|
paypalStatus: '',
|
||||||
|
paypalPaymentStatus: '',
|
||||||
|
customerName: '',
|
||||||
|
customerPhone: '',
|
||||||
|
customerEmail: '',
|
||||||
|
productName: '',
|
||||||
|
payerEmail: '',
|
||||||
|
payerName: '',
|
||||||
|
startTime: '',
|
||||||
|
endTime: ''
|
||||||
|
})
|
||||||
|
pagination.value = {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小改变
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.value.pageSize = size
|
||||||
|
pagination.value.pageNum = 1 // 重置到第一页
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码改变
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
pagination.value.pageNum = page
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看订单详情
|
||||||
|
const viewOrderDetail = (orderId) => {
|
||||||
|
router.push(`/order/detail/${orderId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单状态文本
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'PENDING': '待支付',
|
||||||
|
'PAID': '已支付',
|
||||||
|
'SHIPPED': '已发货',
|
||||||
|
'COMPLETED': '已完成',
|
||||||
|
'CANCELLED': '已取消'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单状态标签类型
|
||||||
|
const getStatusTagType = (status) => {
|
||||||
|
const typeMap = {
|
||||||
|
'PENDING': 'warning',
|
||||||
|
'PAID': 'success',
|
||||||
|
'SHIPPED': 'info',
|
||||||
|
'COMPLETED': 'success',
|
||||||
|
'CANCELLED': 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取支付状态文本
|
||||||
|
const getPaymentStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'UNPAID': '未支付',
|
||||||
|
'PAID': '已支付',
|
||||||
|
'FAILED': '支付失败'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取支付状态标签类型
|
||||||
|
const getPaymentStatusTagType = (status) => {
|
||||||
|
const typeMap = {
|
||||||
|
'UNPAID': 'warning',
|
||||||
|
'PAID': 'success',
|
||||||
|
'FAILED': 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取PayPal状态标签类型
|
||||||
|
const getPaypalStatusTagType = (status) => {
|
||||||
|
const typeMap = {
|
||||||
|
'CREATED': 'info',
|
||||||
|
'APPROVED': 'success',
|
||||||
|
'COMPLETED': 'success',
|
||||||
|
'VOIDED': 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
const formatDateTime = (dateTime) => {
|
||||||
|
if (!dateTime) return ''
|
||||||
|
const date = new Date(dateTime)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载订单列表
|
||||||
|
onMounted(() => {
|
||||||
|
handleQuery()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-manage {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-name {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-amount {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-amount {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-phone {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-email {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paypal-order-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payer-email {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-paypal {
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pagination-container {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container :deep(.el-pagination) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getOrderByOrderNo } from '../api/order'
|
import { getOrderByOrderNo } from '../api/order'
|
||||||
|
import { getProductUrl } from '../api/product'
|
||||||
import { formatAmount } from '../utils/helpers'
|
import { formatAmount } from '../utils/helpers'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -55,9 +56,38 @@ const continuePay = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回首页
|
// 返回商品详情页
|
||||||
const goHome = () => {
|
const goHome = async () => {
|
||||||
router.push('/')
|
if (order.value && order.value.productId) {
|
||||||
|
try {
|
||||||
|
// 获取商品链接
|
||||||
|
const urlResponse = await getProductUrl(order.value.productId)
|
||||||
|
if (urlResponse.code === '0000' && urlResponse.data) {
|
||||||
|
const productUrl = urlResponse.data
|
||||||
|
// 从完整URL中提取链接码(URL格式:/product/link/xxxxx)
|
||||||
|
const linkCodeMatch = productUrl.match(/\/product\/link\/([^\/]+)/)
|
||||||
|
if (linkCodeMatch && linkCodeMatch[1]) {
|
||||||
|
// 使用链接码跳转到商品详情页
|
||||||
|
router.push(`/product/link/${linkCodeMatch[1]}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 如果URL格式是 /product/id,直接使用
|
||||||
|
const idMatch = productUrl.match(/\/product\/(\d+)/)
|
||||||
|
if (idMatch && idMatch[1]) {
|
||||||
|
router.push(`/product/${idMatch[1]}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取商品链接失败,使用productId跳转:', error)
|
||||||
|
}
|
||||||
|
// 如果获取链接失败或无法解析,直接使用productId跳转到商品详情页
|
||||||
|
router.push(`/product/${order.value.productId}`)
|
||||||
|
} else {
|
||||||
|
// 如果没有订单信息或productId,仍然尝试跳转到商品详情页(使用默认方式)
|
||||||
|
// 这种情况理论上不应该发生,因为订单必须关联商品
|
||||||
|
console.warn('订单缺少productId,无法跳转到商品详情页')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载订单信息
|
// 加载订单信息
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { Loading } from '@element-plus/icons-vue'
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
import { getOrderByOrderNo } from '../api/order'
|
import { getOrderByOrderNo } from '../api/order'
|
||||||
import { getPayPalOrder, capturePayPalOrder } from '../api/paypal'
|
import { getPayPalOrder, capturePayPalOrder } from '../api/paypal'
|
||||||
|
import { getProductUrl } from '../api/product'
|
||||||
import { formatAmount } from '../utils/helpers'
|
import { formatAmount } from '../utils/helpers'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -94,9 +95,38 @@ const viewOrder = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回首页
|
// 返回商品详情页
|
||||||
const goHome = () => {
|
const goHome = async () => {
|
||||||
router.push('/')
|
if (order.value && order.value.productId) {
|
||||||
|
try {
|
||||||
|
// 获取商品链接
|
||||||
|
const urlResponse = await getProductUrl(order.value.productId)
|
||||||
|
if (urlResponse.code === '0000' && urlResponse.data) {
|
||||||
|
const productUrl = urlResponse.data
|
||||||
|
// 从完整URL中提取链接码(URL格式:/product/link/xxxxx)
|
||||||
|
const linkCodeMatch = productUrl.match(/\/product\/link\/([^\/]+)/)
|
||||||
|
if (linkCodeMatch && linkCodeMatch[1]) {
|
||||||
|
// 使用链接码跳转到商品详情页
|
||||||
|
router.push(`/product/link/${linkCodeMatch[1]}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 如果URL格式是 /product/id,直接使用
|
||||||
|
const idMatch = productUrl.match(/\/product\/(\d+)/)
|
||||||
|
if (idMatch && idMatch[1]) {
|
||||||
|
router.push(`/product/${idMatch[1]}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取商品链接失败,使用productId跳转:', error)
|
||||||
|
}
|
||||||
|
// 如果获取链接失败或无法解析,直接使用productId跳转到商品详情页
|
||||||
|
router.push(`/product/${order.value.productId}`)
|
||||||
|
} else {
|
||||||
|
// 如果没有订单信息或productId,仍然尝试跳转到商品详情页(使用默认方式)
|
||||||
|
// 这种情况理论上不应该发生,因为订单必须关联商品
|
||||||
|
console.warn('订单缺少productId,无法跳转到商品详情页')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理PayPal支付回调(步骤7-11)
|
// 处理PayPal支付回调(步骤7-11)
|
||||||
|
|||||||
@@ -4,13 +4,82 @@
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>商品管理</span>
|
<span>商品管理</span>
|
||||||
<el-button type="primary" @click="goToCreate">
|
<div class="header-actions">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-button @click="goToUserProfile">
|
||||||
新增商品
|
<el-icon><User /></el-icon>
|
||||||
</el-button>
|
用户信息
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="goToCreate">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
新增商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 查询表单 -->
|
||||||
|
<el-card class="search-card" style="margin-bottom: 20px">
|
||||||
|
<el-form :model="queryForm" :inline="true" class="search-form">
|
||||||
|
<el-form-item label="商品名称">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.name"
|
||||||
|
placeholder="请输入商品名称"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="链接码">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.linkCode"
|
||||||
|
placeholder="请输入链接码或完整链接"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品状态">
|
||||||
|
<el-select
|
||||||
|
v-model="queryForm.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="上架" value="ACTIVE" />
|
||||||
|
<el-option label="下架" value="INACTIVE" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="发售地区">
|
||||||
|
<el-select
|
||||||
|
v-model="queryForm.salesRegion"
|
||||||
|
placeholder="请选择地区"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="马来西亚" value="MYR" />
|
||||||
|
<el-option label="菲律宾" value="PHP" />
|
||||||
|
<el-option label="泰国" value="THB" />
|
||||||
|
<el-option label="越南" value="VND" />
|
||||||
|
<el-option label="新加坡" value="SGD" />
|
||||||
|
<el-option label="中国" value="CNY" />
|
||||||
|
<el-option label="美国" value="USD" />
|
||||||
|
<el-option label="欧洲" value="EUR" />
|
||||||
|
<el-option label="英国" value="GBP" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery">
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- 商品列表 -->
|
<!-- 商品列表 -->
|
||||||
<el-table :data="productList" v-loading="loading" style="width: 100%">
|
<el-table :data="productList" v-loading="loading" style="width: 100%">
|
||||||
<!-- 商品封面图 -->
|
<!-- 商品封面图 -->
|
||||||
@@ -109,12 +178,31 @@
|
|||||||
<el-button type="success" link size="small" @click="copyProductUrl(row.id, row.productUrl)">
|
<el-button type="success" link size="small" @click="copyProductUrl(row.id, row.productUrl)">
|
||||||
复制
|
复制
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="danger" link size="small" @click="deleteProduct(row.id)">
|
<el-button
|
||||||
删除
|
type="danger"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="offShelfProduct(row.id)"
|
||||||
|
:disabled="row.status === 'INACTIVE'"
|
||||||
|
>
|
||||||
|
{{ row.status === 'INACTIVE' ? '已下架' : '下架' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<div class="pagination-container" v-if="pagination.total > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.pageNum"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -124,19 +212,41 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus, User, Search, Refresh } from '@element-plus/icons-vue'
|
||||||
import { getProductList, getProductUrl } from '../api/product'
|
import { getProductList, getProductUrl, offShelfProductById, queryProducts } from '../api/product'
|
||||||
import { formatAmount } from '../utils/helpers'
|
import { formatAmount } from '../utils/helpers'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const productList = ref([])
|
const productList = ref([])
|
||||||
|
|
||||||
|
// 查询表单
|
||||||
|
const queryForm = ref({
|
||||||
|
name: '',
|
||||||
|
linkCode: '',
|
||||||
|
status: '',
|
||||||
|
salesRegion: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = ref({
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
// 跳转到新增商品页面
|
// 跳转到新增商品页面
|
||||||
const goToCreate = () => {
|
const goToCreate = () => {
|
||||||
router.push('/manage/product/create')
|
router.push('/manage/product/create')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳转到用户信息页面
|
||||||
|
const goToUserProfile = () => {
|
||||||
|
router.push('/manage/user/profile')
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化价格
|
// 格式化价格
|
||||||
const formatPrice = (price) => {
|
const formatPrice = (price) => {
|
||||||
return formatAmount(price)
|
return formatAmount(price)
|
||||||
@@ -180,30 +290,63 @@ const getSalesRegions = (row) => {
|
|||||||
return regions
|
return regions
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载商品列表
|
// 从完整URL中提取链接码
|
||||||
|
const extractLinkCodeFromUrl = (input) => {
|
||||||
|
if (!input || !input.trim()) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = input.trim()
|
||||||
|
|
||||||
|
// 如果包含 "/product/",说明是完整URL,提取链接码
|
||||||
|
const productIndex = trimmed.indexOf('/product/')
|
||||||
|
if (productIndex >= 0) {
|
||||||
|
let linkCode = trimmed.substring(productIndex + '/product/'.length)
|
||||||
|
// 移除可能的查询参数和锚点
|
||||||
|
const queryIndex = linkCode.indexOf('?')
|
||||||
|
if (queryIndex >= 0) {
|
||||||
|
linkCode = linkCode.substring(0, queryIndex)
|
||||||
|
}
|
||||||
|
const hashIndex = linkCode.indexOf('#')
|
||||||
|
if (hashIndex >= 0) {
|
||||||
|
linkCode = linkCode.substring(0, hashIndex)
|
||||||
|
}
|
||||||
|
// 移除尾部斜杠
|
||||||
|
linkCode = linkCode.replace(/\/$/, '')
|
||||||
|
return linkCode.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不包含 "/product/",可能是直接输入的链接码,直接返回
|
||||||
|
// 但也可能是其他格式的URL,尝试提取最后一段作为链接码
|
||||||
|
if (trimmed.includes('/')) {
|
||||||
|
const parts = trimmed.split('/')
|
||||||
|
if (parts.length > 0) {
|
||||||
|
let lastPart = parts[parts.length - 1]
|
||||||
|
// 移除查询参数和锚点
|
||||||
|
const queryIndex = lastPart.indexOf('?')
|
||||||
|
if (queryIndex >= 0) {
|
||||||
|
lastPart = lastPart.substring(0, queryIndex)
|
||||||
|
}
|
||||||
|
const hashIndex = lastPart.indexOf('#')
|
||||||
|
if (hashIndex >= 0) {
|
||||||
|
lastPart = lastPart.substring(0, hashIndex)
|
||||||
|
}
|
||||||
|
return lastPart.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回(可能是纯链接码)
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载商品列表(无查询条件)
|
||||||
const loadProductList = async () => {
|
const loadProductList = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await getProductList()
|
const response = await getProductList()
|
||||||
if (response.code === '0000' && response.data) {
|
if (response.code === '0000' && response.data) {
|
||||||
// 为每个商品获取链接URL
|
// 后端已经返回了productUrl,直接使用
|
||||||
const productsWithUrl = await Promise.all(
|
productList.value = response.data
|
||||||
response.data.map(async (product) => {
|
|
||||||
try {
|
|
||||||
const urlResponse = await getProductUrl(product.id)
|
|
||||||
if (urlResponse.code === '0000' && urlResponse.data && urlResponse.data.url) {
|
|
||||||
product.productUrl = urlResponse.data.url
|
|
||||||
} else {
|
|
||||||
product.productUrl = ''
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`获取商品${product.id}链接失败:`, error)
|
|
||||||
product.productUrl = ''
|
|
||||||
}
|
|
||||||
return product
|
|
||||||
})
|
|
||||||
)
|
|
||||||
productList.value = productsWithUrl
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(response.message || '获取商品列表失败')
|
ElMessage.error(response.message || '获取商品列表失败')
|
||||||
productList.value = []
|
productList.value = []
|
||||||
@@ -217,6 +360,91 @@ const loadProductList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询商品列表
|
||||||
|
const handleQuery = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 构建查询条件(只包含非空字段)
|
||||||
|
const query = {}
|
||||||
|
if (queryForm.value.name && queryForm.value.name.trim()) {
|
||||||
|
query.name = queryForm.value.name.trim()
|
||||||
|
}
|
||||||
|
if (queryForm.value.linkCode && queryForm.value.linkCode.trim()) {
|
||||||
|
// 自动从完整URL中提取链接码
|
||||||
|
const extractedLinkCode = extractLinkCodeFromUrl(queryForm.value.linkCode.trim())
|
||||||
|
query.linkCode = extractedLinkCode
|
||||||
|
}
|
||||||
|
if (queryForm.value.status) {
|
||||||
|
query.status = queryForm.value.status
|
||||||
|
}
|
||||||
|
if (queryForm.value.salesRegion) {
|
||||||
|
query.salesRegion = queryForm.value.salesRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加分页参数
|
||||||
|
query.pageNum = pagination.value.pageNum
|
||||||
|
query.pageSize = pagination.value.pageSize
|
||||||
|
|
||||||
|
const response = await queryProducts(query)
|
||||||
|
if (response.code === '0000' && response.data) {
|
||||||
|
const pageResult = response.data
|
||||||
|
productList.value = pageResult.records || []
|
||||||
|
|
||||||
|
// 更新分页信息
|
||||||
|
pagination.value.total = pageResult.total || 0
|
||||||
|
pagination.value.pageNum = pageResult.current || 1
|
||||||
|
pagination.value.pageSize = pageResult.size || 10
|
||||||
|
|
||||||
|
if (productList.value.length === 0) {
|
||||||
|
ElMessage.info('未找到符合条件的商品')
|
||||||
|
} else {
|
||||||
|
ElMessage.success(`查询到 ${pagination.value.total} 条商品,当前第 ${pagination.value.pageNum} 页`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '查询商品列表失败')
|
||||||
|
productList.value = []
|
||||||
|
pagination.value.total = 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询商品列表失败:', error)
|
||||||
|
ElMessage.error('查询商品列表失败')
|
||||||
|
productList.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置查询条件
|
||||||
|
const handleReset = () => {
|
||||||
|
queryForm.value = {
|
||||||
|
name: '',
|
||||||
|
linkCode: '',
|
||||||
|
status: '',
|
||||||
|
salesRegion: '',
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10
|
||||||
|
}
|
||||||
|
pagination.value = {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
loadProductList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小改变
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.value.pageSize = size
|
||||||
|
pagination.value.pageNum = 1 // 重置到第一页
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码改变
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
pagination.value.pageNum = page
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
// 编辑商品
|
// 编辑商品
|
||||||
const editProduct = (id) => {
|
const editProduct = (id) => {
|
||||||
// TODO: 实现编辑功能,跳转到编辑页面
|
// TODO: 实现编辑功能,跳转到编辑页面
|
||||||
@@ -228,22 +456,33 @@ const editProduct = (id) => {
|
|||||||
const copyProductUrl = async (id, url) => {
|
const copyProductUrl = async (id, url) => {
|
||||||
try {
|
try {
|
||||||
let urlToCopy = url
|
let urlToCopy = url
|
||||||
// 如果没有URL,先获取
|
// 如果没有URL,尝试获取(降级方案)
|
||||||
if (!urlToCopy) {
|
if (!urlToCopy) {
|
||||||
const response = await getProductUrl(id)
|
try {
|
||||||
if (response.code === '0000' && response.data && response.data.url) {
|
const response = await getProductUrl(id)
|
||||||
urlToCopy = response.data.url
|
if (response.code === '0000' && response.data && response.data.url) {
|
||||||
// 更新列表中的URL
|
urlToCopy = response.data.url
|
||||||
const product = productList.value.find(p => p.id === id)
|
// 更新列表中的URL
|
||||||
if (product) {
|
const product = productList.value.find(p => p.id === id)
|
||||||
product.productUrl = urlToCopy
|
if (product) {
|
||||||
|
product.productUrl = urlToCopy
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error('获取商品链接失败')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
|
console.error('获取商品链接失败:', error)
|
||||||
ElMessage.error('获取商品链接失败')
|
ElMessage.error('获取商品链接失败')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!urlToCopy) {
|
||||||
|
ElMessage.error('商品链接为空')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 复制到剪贴板
|
// 复制到剪贴板
|
||||||
await navigator.clipboard.writeText(urlToCopy)
|
await navigator.clipboard.writeText(urlToCopy)
|
||||||
ElMessage.success('商品链接已复制到剪贴板')
|
ElMessage.success('商品链接已复制到剪贴板')
|
||||||
@@ -253,24 +492,30 @@ const copyProductUrl = async (id, url) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除商品
|
// 下架商品
|
||||||
const deleteProduct = async (id) => {
|
const offShelfProduct = async (id) => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定要删除该商品吗?', '提示', {
|
await ElMessageBox.confirm(
|
||||||
confirmButtonText: '确定',
|
'确定要下架该商品吗?下架后商品所有SKU库存将改为0,链接将失效无法再被访问。',
|
||||||
cancelButtonText: '取消',
|
'提示',
|
||||||
type: 'warning'
|
{
|
||||||
})
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// TODO: 实现删除商品API
|
const response = await offShelfProductById(id)
|
||||||
ElMessage.info('删除功能待实现')
|
if (response.code === '0000') {
|
||||||
// await request.delete(`/api/product/${id}`)
|
ElMessage.success('商品下架成功')
|
||||||
// ElMessage.success('商品删除成功')
|
loadProductList()
|
||||||
// loadProductList()
|
} else {
|
||||||
|
ElMessage.error(response.message || '商品下架失败')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
console.error('删除商品失败:', error)
|
console.error('下架商品失败:', error)
|
||||||
ElMessage.error('删除商品失败')
|
ElMessage.error('下架商品失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,6 +541,11 @@ onMounted(() => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 商品名称单元格 */
|
/* 商品名称单元格 */
|
||||||
.product-name-cell {
|
.product-name-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -356,5 +606,53 @@ onMounted(() => {
|
|||||||
color: #c0c4cc;
|
color: #c0c4cc;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分页容器 */
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.pagination-container {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container :deep(.el-pagination) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 查询表单 */
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form .el-form-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-form {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form .el-form-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form .el-form-item .el-input,
|
||||||
|
.search-form .el-form-item .el-select {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
245
src/views/Register.vue
Normal file
245
src/views/Register.vue
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<template>
|
||||||
|
<div class="register-container">
|
||||||
|
<el-card class="register-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>用户注册</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="registerFormRef"
|
||||||
|
:model="registerForm"
|
||||||
|
:rules="registerRules"
|
||||||
|
label-width="100px"
|
||||||
|
@submit.prevent="handleRegister"
|
||||||
|
>
|
||||||
|
<el-form-item label="账号" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.username"
|
||||||
|
placeholder="请输入账号(3-50个字符,只能包含字母、数字和下划线)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码(6-20个字符)"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="店铺号" prop="storeCode">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.storeCode"
|
||||||
|
placeholder="请输入店铺号"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户名称" prop="nickName">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.nickName"
|
||||||
|
placeholder="请输入用户名称(可选)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.phone"
|
||||||
|
placeholder="请输入手机号(可选,格式:1开头11位数字)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="registerForm.email"
|
||||||
|
placeholder="请输入邮箱(可选)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleRegister"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<div class="register-footer">
|
||||||
|
<span>已有账号?</span>
|
||||||
|
<el-link type="primary" @click="goToLogin">
|
||||||
|
立即登录
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { register } from '../api/user'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const registerFormRef = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const registerForm = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
storeCode: '',
|
||||||
|
nickName: '',
|
||||||
|
phone: '',
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const validatePhone = (rule, value, callback) => {
|
||||||
|
if (value && value.trim() !== '') {
|
||||||
|
const phoneRegex = /^1[3-9]\d{9}$/
|
||||||
|
if (!phoneRegex.test(value)) {
|
||||||
|
callback(new Error('手机号格式不正确,应为1开头11位数字'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateEmail = (rule, value, callback) => {
|
||||||
|
if (value && value.trim() !== '') {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
if (!emailRegex.test(value)) {
|
||||||
|
callback(new Error('邮箱格式不正确'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerRules = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入账号', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 50, message: '账号长度必须在3-50个字符之间', trigger: 'blur' },
|
||||||
|
{ pattern: /^[a-zA-Z0-9_]+$/, message: '账号只能包含字母、数字和下划线', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度必须在6-20个字符之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
storeCode: [
|
||||||
|
{ required: true, message: '请输入店铺号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
phone: [
|
||||||
|
{ validator: validatePhone, trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ validator: validateEmail, trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!registerFormRef.value) return
|
||||||
|
|
||||||
|
await registerFormRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await register({
|
||||||
|
username: registerForm.username,
|
||||||
|
password: registerForm.password,
|
||||||
|
storeCode: registerForm.storeCode,
|
||||||
|
nickName: registerForm.nickName || undefined,
|
||||||
|
phone: registerForm.phone || undefined,
|
||||||
|
email: registerForm.email || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.code === '0000') {
|
||||||
|
ElMessage.success('注册成功')
|
||||||
|
|
||||||
|
// 注册成功后跳转到登录页
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push('/login')
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注册失败:', error)
|
||||||
|
// 错误消息已在request拦截器中显示
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToLogin = () => {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.register-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-footer {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-footer .el-link {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.register-card {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
width: 80px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__content) {
|
||||||
|
margin-left: 80px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
443
src/views/UserProfile.vue
Normal file
443
src/views/UserProfile.vue
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-profile">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
:icon="ArrowLeft"
|
||||||
|
@click="goBack"
|
||||||
|
class="back-button"
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<h2>用户信息</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<el-tab-pane label="基本信息" name="info">
|
||||||
|
<el-form
|
||||||
|
ref="infoFormRef"
|
||||||
|
:model="userForm"
|
||||||
|
:rules="infoRules"
|
||||||
|
label-width="120px"
|
||||||
|
style="max-width: 600px; margin-top: 20px"
|
||||||
|
>
|
||||||
|
<el-form-item label="账号">
|
||||||
|
<el-input v-model="userInfo.username" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户名称" prop="nickName">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.nickName"
|
||||||
|
placeholder="请输入用户名称(可选)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.phone"
|
||||||
|
placeholder="请输入手机号(可选,格式:1开头11位数字)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.email"
|
||||||
|
placeholder="请输入邮箱(可选)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="店铺号" prop="storeCode">
|
||||||
|
<el-input
|
||||||
|
v-model="userForm.storeCode"
|
||||||
|
placeholder="请输入店铺号"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="infoLoading"
|
||||||
|
@click="handleUpdateInfo"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetInfoForm">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 修改密码 -->
|
||||||
|
<el-tab-pane label="修改密码" name="password">
|
||||||
|
<el-form
|
||||||
|
ref="passwordFormRef"
|
||||||
|
:model="passwordForm"
|
||||||
|
:rules="passwordRules"
|
||||||
|
label-width="120px"
|
||||||
|
style="max-width: 600px; margin-top: 20px"
|
||||||
|
>
|
||||||
|
<el-form-item label="旧密码" prop="oldPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.oldPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入旧密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.newPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码(6-20个字符)"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="确认新密码" prop="confirmPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="passwordForm.confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="passwordLoading"
|
||||||
|
@click="handleChangePassword"
|
||||||
|
>
|
||||||
|
修改密码
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetPasswordForm">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 账户信息 -->
|
||||||
|
<el-tab-pane label="账户信息" name="account">
|
||||||
|
<el-descriptions :column="1" border style="max-width: 600px; margin-top: 20px">
|
||||||
|
<el-descriptions-item label="账号">
|
||||||
|
{{ userInfo.username }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户名称">
|
||||||
|
{{ userInfo.nickName || '未设置' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机号">
|
||||||
|
{{ userInfo.phone || '未设置' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">
|
||||||
|
{{ userInfo.email || '未设置' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺号">
|
||||||
|
{{ userInfo.storeCode }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="userInfo.status === 'ACTIVE' ? 'success' : 'danger'">
|
||||||
|
{{ userInfo.status === 'ACTIVE' ? '激活' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后登录时间">
|
||||||
|
{{ formatDateTime(userInfo.lastLoginTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后登录IP">
|
||||||
|
{{ userInfo.lastLoginIp || '未知' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
{{ formatDateTime(userInfo.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||||
|
import { getCurrentUserInfo, updateUserInfo, changePassword } from '../api/user'
|
||||||
|
import userStore from '../store/user'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const activeTab = ref('info')
|
||||||
|
const infoFormRef = ref(null)
|
||||||
|
const passwordFormRef = ref(null)
|
||||||
|
const infoLoading = ref(false)
|
||||||
|
const passwordLoading = ref(false)
|
||||||
|
|
||||||
|
const userInfo = ref({
|
||||||
|
id: null,
|
||||||
|
username: '',
|
||||||
|
nickName: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
storeCode: '',
|
||||||
|
status: '',
|
||||||
|
lastLoginTime: null,
|
||||||
|
lastLoginIp: '',
|
||||||
|
createTime: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const userForm = reactive({
|
||||||
|
nickName: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
storeCode: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const passwordForm = reactive({
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const validatePhone = (rule, value, callback) => {
|
||||||
|
if (value && value.trim() !== '') {
|
||||||
|
const phoneRegex = /^1[3-9]\d{9}$/
|
||||||
|
if (!phoneRegex.test(value)) {
|
||||||
|
callback(new Error('手机号格式不正确,应为1开头11位数字'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateEmail = (rule, value, callback) => {
|
||||||
|
if (value && value.trim() !== '') {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
if (!emailRegex.test(value)) {
|
||||||
|
callback(new Error('邮箱格式不正确'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateConfirmPassword = (rule, value, callback) => {
|
||||||
|
if (value !== passwordForm.newPassword) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoRules = {
|
||||||
|
phone: [
|
||||||
|
{ validator: validatePhone, trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ validator: validateEmail, trigger: 'blur' }
|
||||||
|
],
|
||||||
|
storeCode: [
|
||||||
|
{ required: true, message: '请输入店铺号', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordRules = {
|
||||||
|
oldPassword: [
|
||||||
|
{ required: true, message: '请输入旧密码', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
newPassword: [
|
||||||
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度必须在6-20个字符之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
||||||
|
{ validator: validateConfirmPassword, trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载用户信息
|
||||||
|
const loadUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getCurrentUserInfo()
|
||||||
|
if (response.code === '0000' && response.data) {
|
||||||
|
userInfo.value = response.data
|
||||||
|
|
||||||
|
// 填充表单
|
||||||
|
userForm.nickName = response.data.nickName || ''
|
||||||
|
userForm.phone = response.data.phone || ''
|
||||||
|
userForm.email = response.data.email || ''
|
||||||
|
userForm.storeCode = response.data.storeCode || ''
|
||||||
|
|
||||||
|
// 更新store中的用户信息
|
||||||
|
userStore.updateUser(response.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
const handleUpdateInfo = async () => {
|
||||||
|
if (!infoFormRef.value) return
|
||||||
|
|
||||||
|
await infoFormRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
infoLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await updateUserInfo({
|
||||||
|
nickName: userForm.nickName || undefined,
|
||||||
|
phone: userForm.phone || undefined,
|
||||||
|
email: userForm.email || undefined,
|
||||||
|
storeCode: userForm.storeCode
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.code === '0000') {
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
// 重新加载用户信息
|
||||||
|
await loadUserInfo()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用户信息失败:', error)
|
||||||
|
} finally {
|
||||||
|
infoLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
const handleChangePassword = async () => {
|
||||||
|
if (!passwordFormRef.value) return
|
||||||
|
|
||||||
|
await passwordFormRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
passwordLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await changePassword({
|
||||||
|
oldPassword: passwordForm.oldPassword,
|
||||||
|
newPassword: passwordForm.newPassword
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.code === '0000') {
|
||||||
|
ElMessage.success('密码修改成功')
|
||||||
|
resetPasswordForm()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码失败:', error)
|
||||||
|
} finally {
|
||||||
|
passwordLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置基本信息表单
|
||||||
|
const resetInfoForm = () => {
|
||||||
|
userForm.nickName = userInfo.value.nickName || ''
|
||||||
|
userForm.phone = userInfo.value.phone || ''
|
||||||
|
userForm.email = userInfo.value.email || ''
|
||||||
|
userForm.storeCode = userInfo.value.storeCode || ''
|
||||||
|
infoFormRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码表单
|
||||||
|
const resetPasswordForm = () => {
|
||||||
|
passwordForm.oldPassword = ''
|
||||||
|
passwordForm.newPassword = ''
|
||||||
|
passwordForm.confirmPassword = ''
|
||||||
|
passwordFormRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
const formatDateTime = (dateTime) => {
|
||||||
|
if (!dateTime) return '未知'
|
||||||
|
const date = new Date(dateTime)
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadUserInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-profile {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
color: #409eff;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.user-profile {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h2 {
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -12,11 +12,22 @@ export default defineConfig({
|
|||||||
'@': path.resolve(__dirname, 'src')
|
'@': path.resolve(__dirname, 'src')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 构建配置
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'assets',
|
||||||
|
// 确保资源路径正确
|
||||||
|
assetsInlineLimit: 4096,
|
||||||
|
// 生成source map(生产环境可以关闭)
|
||||||
|
sourcemap: false
|
||||||
|
},
|
||||||
|
// 基础路径(如果部署在子目录下需要配置)
|
||||||
|
base: '/',
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:8082', // 使用 127.0.0.1 而不是 localhost,避免 IPv6 问题
|
target: 'http://127.0.0.1:8082',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
ws: true,
|
ws: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user