Compare commits

9 Commits

Author SHA1 Message Date
6d725b51cb fix(config): 修正开发服务器代理配置
- 将代理目标端口从 18082 修正为 8082
- 更新错误提示信息中的端口号以保持一致
2025-12-26 16:21:32 +08:00
857f46ad17 chore(config): 更新生产环境API配置和代理设置
- 将生产环境API基础URL从绝对路径改为相对路径 /api
- 修改开发环境代理配置,后端目标端口从 8082 改为 18082
- 更新相关注释和错误提示信息
- 移除生产环境配置中的服务器IP地址硬编码
- 添加
2025-12-26 14:11:46 +08:00
e251153d14 chore(config): 更新生产环境配置和构建设置
- 修复 .env.production 文件中的注释编码问题
- 移除路由守卫中的调试日志输出
- 调整错误处理机制,生产环境静默处理错误
- 移除应用挂载时的调试日志和异常处理包装
- 添加构建配置和基础路径设置
2025-12-26 10:54:17 +08:00
3662ee072b feat(config): 添加生产环境配置文件
- 配置生产环境服务器地址 175.178.252.59
- 设置后端 API 地址为 http://175.178.252.59:8082/api
- 配置 PingPong 模式为 sandbox
- 添加生产环境相关环境变量
2025-12-26 10:27:09 +08:00
3bdb2ff5f3 feat(order): 添加订单管理功能
- 创建新的订单管理页面 OrderManage.vue,支持分页和多条件查询
- 添加订单查询API接口,支持复杂的订单筛选条件
- 在路由中添加订单管理页面路由配置
- 更新导航菜单,将订单查询改为订单管理并指向新页面
- 实现订单列表的表格展示,包含订单号、商品信息、金额、客户信息等
- 添加订单状态、支付状态、PayPal状态的标签显示和筛选功能
- 实现查询表单,支持订单号、PayPal订单ID、客户信息等多维度搜索
- 添加分页组件,支持每页显示数量调整和页码切换
- 实现订单详情查看功能和重置查询条件功能
2025-12-25 18:12:23 +08:00
b2bbbf8c44 feat(product): 添加商品管理分页功能并支持完整链接查询
- 添加分页组件支持,包括页码、页面大小切换功能
- 实现从完整URL中自动提取链接码的功能
- 更新查询表单提示文本为"请输入链接码或完整链接"
- 添加分页数据响应处理,显示总记录数和当前页码信息
- 增加分页相关的CSS样式适配移动端显示
- 重置查询时同步重置分页参数到默认值
2025-12-25 17:11:48 +08:00
01bda65010 feat(product): 添加商品下架和多条件查询功能
- 实现了商品下架功能,下架后SKU库存变为0且链接失效
- 添加了支持多条件查询的商品列表功能
- 增加了包含商品名称、链接码、状态、发售地区等查询条件
- 在商品管理页面添加了查询表单界面
- 实现了下架按钮的禁用逻辑和状态显示
- 添加了移动端响应式查询表单适配
- 集成了API接口和错误处理机制
2025-12-25 16:26:11 +08:00
a27327c7fa feat(auth): 添加用户认证功能模块
- 创建登录页面组件,包含账号密码输入和验证功能
- 创建注册页面组件,支持用户注册和信息填写验证
- 添加用户状态管理store,实现token和用户信息的本地存储
- 集成用户认证API接口,包括登录、注册、信息获取等功能
- 实现用户信息管理页面,支持个人信息修改和密码更改
- 添加表单验证规则,确保输入数据格式正确性
- 实现登录状态持久化和自动恢复功能
2025-12-25 15:55:01 +08:00
48156d72aa feat(auth): 添加用户认证系统和导航菜单重构
- 实现用户登录注册功能并添加路由守卫
- 将Element UI菜单替换为自定义导航菜单样式
- 添加多语言用户相关文本资源
- 重构订单确认页面货币转换逻辑
- 实现支付成功/取消页面返回商品详情功能
- 在商品管理页面添加用户信息入口
- 调整API请求添加ERP接口认证机制
- 优化移动端导航菜单适配样式
2025-12-25 15:09:32 +08:00
20 changed files with 2487 additions and 105 deletions

6
.env.production Normal file
View File

@@ -0,0 +1,6 @@
# 鐢熶骇鐜閰嶇疆
# 浣跨敤鐩稿璺緞锛岄€氳繃Nginx浠悊鍒板悗绔?
# API鍩虹URL锛堢浉瀵硅矾寰勶紝閫氳繃Nginx浠悊锛?# 閲嶈锛氬繀椤讳娇鐢ㄧ浉瀵硅矾寰?/api锛屼笉瑕佷娇鐢ㄥ畬鏁碪RL
VITE_API_BASE_URL=/api
# PingPong妯″紡锛坰andbox/production锛?VITE_PINGPONG_MODE=sandbox

View File

@@ -73,7 +73,7 @@ MTKJPAY-FRONT/
`src/api/request.js` 中配置后端API地址
```javascript
const service = axios.create({
baseURL: 'http://localhost:8082/api',
baseURL: '/api', // 使用相对路径通过Nginx代理
timeout: 10000
})
```

View File

@@ -9,14 +9,22 @@
<el-header>
<div class="header-content">
<h1>MT Pay 管理系统</h1>
<el-menu
mode="horizontal"
:default-active="activeIndex"
router
<div class="nav-menu">
<router-link
to="/manage/order"
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>
</el-header>
<el-main>
@@ -56,6 +64,7 @@ const activeIndex = computed(() => route.path)
color: white;
line-height: 60px;
padding: 0 20px;
overflow: visible;
}
.header-content {
@@ -64,6 +73,58 @@ const activeIndex = computed(() => route.path)
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 {
font-size: 20px;
margin: 0;

View File

@@ -42,3 +42,14 @@ export function calculateCurrencyConversion(data) {
})
}
/**
* 查询订单列表(支持分页和多条件查询)
*/
export function queryOrders(query) {
return request({
url: '/order/query',
method: 'post',
data: query
})
}

View File

@@ -65,3 +65,30 @@ export function uploadProductImage(file) {
// 注意:不设置 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
})
}

View File

@@ -17,6 +17,17 @@ request.interceptors.request.use(
if (config.data instanceof FormData) {
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
},
error => {
@@ -48,7 +59,32 @@ request.interceptors.response.use(
let message = '请求失败'
if (error.response) {
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) {
message = '网络错误,请检查网络连接'
} else {

76
src/api/user.js Normal file
View 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'
})
}

View File

@@ -165,6 +165,46 @@ const zh = {
currencySGD: '新加坡元',
currencyHKD: '港币',
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',
currencyHKD: 'Hong Kong Dollar',
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',
currencyHKD: 'Dolar Hong Kong',
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',
currencyHKD: 'Hong Kong Dollar',
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: 'ดอลลาร์สิงคโปร์',
currencyHKD: 'ดอลลาร์ฮ่องกง',
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',
currencyHKD: 'Đô La Hồng Kông',
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',
currencyHKD: 'Dolar Hong Kong',
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'
}
}

View File

@@ -17,18 +17,14 @@ app.use(router)
app.use(ElementPlus)
app.use(i18n)
// 添加错误处理
// 添加错误处理(生产环境静默处理)
app.config.errorHandler = (err, instance, info) => {
console.error('Vue错误:', err)
console.error('错误信息:', info)
console.error('组件实例:', instance)
// 生产环境可以记录到错误追踪服务
if (import.meta.env.DEV) {
console.error('Vue错误:', err, info)
}
}
// 挂载应用
try {
app.mount('#app')
console.log('Vue应用已成功挂载')
} catch (error) {
console.error('应用挂载失败:', error)
}
app.mount('#app')

View File

@@ -4,6 +4,9 @@ import Checkout from '../views/Checkout.vue'
import PaymentResult from '../views/PaymentResult.vue'
import OrderQuery from '../views/OrderQuery.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 = [
{
@@ -11,6 +14,18 @@ const routes = [
name: 'Home',
redirect: '/manage/product'
},
{
path: '/login',
name: 'Login',
component: Login,
meta: { requiresAuth: false }
},
{
path: '/register',
name: 'Register',
component: Register,
meta: { requiresAuth: false }
},
{
path: '/product/:id',
name: 'ProductDetail',
@@ -63,15 +78,29 @@ const routes = [
name: 'OrderQuery',
component: OrderQuery
},
{
path: '/manage/order',
name: 'OrderManage',
component: () => import('../views/OrderManage.vue'),
meta: { requiresAuth: true }
},
{
path: '/manage/product',
name: 'ProductManage',
component: () => import('../views/ProductManage.vue')
component: () => import('../views/ProductManage.vue'),
meta: { requiresAuth: true }
},
{
path: '/manage/product/create',
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
})
// 添加路由守卫,用于调试
// 路由守卫
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()
})

74
src/store/user.js Normal file
View 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
View 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>

View File

@@ -314,13 +314,17 @@ const loadOrder = async () => {
await loadTranslationByCurrency(order.value.currency)
}
// 如果订单未支付且需要货币转换,提前计算货币转换信息
// 如果订单未支付且需要货币转换,提前计算货币转换信息(页面加载时就转换)
if (order.value.paymentStatus === 'UNPAID' && order.value.currency) {
// 检查是否需要货币转换(PayPal支持的货币列表
const supportedCurrencies = ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'JPY', 'CNY', 'HKD', 'SGD', 'NZD',
// PayPal支持的货币列表注意CNY和MYR不支持需要转换为USD
const supportedCurrencies = ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'JPY', 'HKD', 'SGD', 'NZD',
'CHF', 'SEK', 'NOK', 'DKK', 'PLN', 'MXN', 'BRL', 'INR', 'KRW', 'THB']
const needsConversion = !supportedCurrencies.includes(order.value.currency)
// 检查是否需要货币转换:
// 1. CNY和MYR必须转换为USDPayPal不支持
// 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)) {
@@ -340,11 +344,12 @@ const loadOrder = async () => {
order.value.paymentAmount = conversion.paymentAmount
order.value.exchangeRate = conversion.exchangeRate
order.value.rateLockedAt = conversion.rateLockedAt
console.log('货币转换信息已计算并更新,支付状态:', order.value.paymentStatus)
console.log('货币转换信息已计算并更新(页面加载时),原始货币:', order.value.currency,
'支付货币:', order.value.paymentCurrency, '支付金额:', order.value.paymentAmount)
}
} catch (error) {
console.warn('计算货币转换信息失败:', error)
// 不显示错误,因为不影响订单显示
console.error('计算货币转换信息失败:', error)
ElMessage.warning('计算货币转换信息失败,请稍后重试')
}
}
}
@@ -377,6 +382,9 @@ const handlePay = async () => {
try {
// 步骤2构建PayPal订单创建请求
// 注意:如果已经有货币转换信息(页面加载时已计算),使用转换后的货币和金额
// 后端会根据currencyCode自动进行转换CNY/MYR转USD但这里使用原始货币和金额
// 后端会返回转换后的信息前端显示时使用order.value.paymentCurrency和paymentAmount
const paypalOrderData = {
intent: 'CAPTURE', // 立即捕获
referenceId: order.value.orderNo, // ERP订单号
@@ -407,25 +415,19 @@ const handlePay = async () => {
const paypalOrder = responseData.paypalOrder || responseData // 兼容新旧格式
const currencyConversion = responseData.currencyConversion
// 如果有货币转换信息,更新订单显示
// 如果有货币转换信息,更新订单显示(如果页面加载时已经计算过,这里只是确认)
if (currencyConversion) {
order.value = {
...order.value,
originalCurrency: currencyConversion.originalCurrency,
originalAmount: currencyConversion.originalAmount,
paymentCurrency: currencyConversion.paymentCurrency,
paymentAmount: currencyConversion.paymentAmount,
exchangeRate: currencyConversion.exchangeRate,
rateLockedAt: currencyConversion.rateLockedAt
}
// 更新订单显示信息(保留原有字段,只更新货币转换相关字段)
order.value.originalCurrency = currencyConversion.originalCurrency
order.value.originalAmount = currencyConversion.originalAmount
order.value.paymentCurrency = currencyConversion.paymentCurrency
order.value.paymentAmount = currencyConversion.paymentAmount
order.value.exchangeRate = currencyConversion.exchangeRate
order.value.rateLockedAt = currencyConversion.rateLockedAt
// 显示货币转换提示
if (currencyConversion.conversionRequired) {
ElMessage.info({
message: `${t('confirm.willPayIn', [getCurrencyName(currencyConversion.paymentCurrency)])}${t('confirm.actualCost')}${currencyConversion.paymentCurrency} ${formatPrice(currencyConversion.paymentAmount)}`,
duration: 5000
})
}
// 如果页面加载时已经显示过转换信息,这里不需要再次提示
// 只在首次转换时显示提示(但页面加载时已经计算过了,所以这里通常不需要显示)
console.log('PayPal订单创建成功货币转换信息已确认支付货币:', currencyConversion.paymentCurrency)
}
// 步骤3-4获取approval_url并跳转到PayPal登录页

545
src/views/OrderManage.vue Normal file
View 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>

View File

@@ -34,6 +34,7 @@ import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getOrderByOrderNo } from '../api/order'
import { getProductUrl } from '../api/product'
import { formatAmount } from '../utils/helpers'
const router = useRouter()
@@ -55,9 +56,38 @@ const continuePay = () => {
}
}
// 返回
const goHome = () => {
router.push('/')
// 返回商品详情
const goHome = async () => {
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无法跳转到商品详情页')
}
}
// 加载订单信息

View File

@@ -70,6 +70,7 @@ import { ElMessage } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import { getOrderByOrderNo } from '../api/order'
import { getPayPalOrder, capturePayPalOrder } from '../api/paypal'
import { getProductUrl } from '../api/product'
import { formatAmount } from '../utils/helpers'
const router = useRouter()
@@ -94,9 +95,38 @@ const viewOrder = () => {
}
}
// 返回
const goHome = () => {
router.push('/')
// 返回商品详情
const goHome = async () => {
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

View File

@@ -4,13 +4,82 @@
<template #header>
<div class="card-header">
<span>商品管理</span>
<div class="header-actions">
<el-button @click="goToUserProfile">
<el-icon><User /></el-icon>
用户信息
</el-button>
<el-button type="primary" @click="goToCreate">
<el-icon><Plus /></el-icon>
新增商品
</el-button>
</div>
</div>
</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%">
<!-- 商品封面图 -->
@@ -109,12 +178,31 @@
<el-button type="success" link size="small" @click="copyProductUrl(row.id, row.productUrl)">
复制
</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>
</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>
@@ -124,19 +212,41 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getProductList, getProductUrl } from '../api/product'
import { Plus, User, Search, Refresh } from '@element-plus/icons-vue'
import { getProductList, getProductUrl, offShelfProductById, queryProducts } from '../api/product'
import { formatAmount } from '../utils/helpers'
const router = useRouter()
const loading = ref(false)
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 = () => {
router.push('/manage/product/create')
}
// 跳转到用户信息页面
const goToUserProfile = () => {
router.push('/manage/user/profile')
}
// 格式化价格
const formatPrice = (price) => {
return formatAmount(price)
@@ -180,30 +290,63 @@ const getSalesRegions = (row) => {
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 () => {
loading.value = true
try {
const response = await getProductList()
if (response.code === '0000' && response.data) {
// 为每个商品获取链接URL
const productsWithUrl = await Promise.all(
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
// 后端已经返回了productUrl直接使用
productList.value = response.data
} else {
ElMessage.error(response.message || '获取商品列表失败')
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) => {
// TODO: 实现编辑功能,跳转到编辑页面
@@ -228,8 +456,9 @@ const editProduct = (id) => {
const copyProductUrl = async (id, url) => {
try {
let urlToCopy = url
// 如果没有URL先获取
// 如果没有URL尝试获取(降级方案)
if (!urlToCopy) {
try {
const response = await getProductUrl(id)
if (response.code === '0000' && response.data && response.data.url) {
urlToCopy = response.data.url
@@ -242,6 +471,16 @@ const copyProductUrl = async (id, url) => {
ElMessage.error('获取商品链接失败')
return
}
} catch (error) {
console.error('获取商品链接失败:', error)
ElMessage.error('获取商品链接失败')
return
}
}
if (!urlToCopy) {
ElMessage.error('商品链接为空')
return
}
// 复制到剪贴板
@@ -253,24 +492,30 @@ const copyProductUrl = async (id, url) => {
}
}
// 删除商品
const deleteProduct = async (id) => {
// 下架商品
const offShelfProduct = async (id) => {
try {
await ElMessageBox.confirm('确定要删除该商品吗?', '提示', {
await ElMessageBox.confirm(
'确定要下架该商品吗下架后商品所有SKU库存将改为0链接将失效无法再被访问。',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
}
)
// TODO: 实现删除商品API
ElMessage.info('删除功能待实现')
// await request.delete(`/api/product/${id}`)
// ElMessage.success('商品删除成功')
// loadProductList()
const response = await offShelfProductById(id)
if (response.code === '0000') {
ElMessage.success('商品下架成功')
loadProductList()
} else {
ElMessage.error(response.message || '商品下架失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除商品失败:', error)
ElMessage.error('删除商品失败')
console.error('下架商品失败:', error)
ElMessage.error('下架商品失败')
}
}
}
@@ -296,6 +541,11 @@ onMounted(() => {
font-weight: bold;
}
.header-actions {
display: flex;
gap: 10px;
}
/* 商品名称单元格 */
.product-name-cell {
display: flex;
@@ -356,5 +606,53 @@ onMounted(() => {
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;
}
}
/* 查询表单 */
.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>

245
src/views/Register.vue Normal file
View 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
View 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>

View File

@@ -12,11 +12,22 @@ export default defineConfig({
'@': path.resolve(__dirname, 'src')
}
},
// 构建配置
build: {
outDir: 'dist',
assetsDir: 'assets',
// 确保资源路径正确
assetsInlineLimit: 4096,
// 生成source map生产环境可以关闭
sourcemap: false
},
// 基础路径(如果部署在子目录下需要配置)
base: '/',
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://127.0.0.1:8082', // 使用 127.0.0.1 而不是 localhost避免 IPv6 问题
target: 'http://127.0.0.1:8082',
changeOrigin: true,
secure: false,
ws: true,