Initial commit: ERP Frontend baseline
This commit is contained in:
commit
9f64a4d6f4
3
.env.development
Normal file
3
.env.development
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# .env.development
|
||||||
|
VITE_USE_MOCK=false
|
||||||
|
# VITE_API_BASE_URL=http://127.0.0.1:8001
|
||||||
1
.env.production
Normal file
1
.env.production
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_API_BASE_URL=http://111.229.80.149/api
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
102
LOGIN_TESTING.md
Normal file
102
LOGIN_TESTING.md
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# 登录页面测试指南
|
||||||
|
|
||||||
|
## 🎯 测试账号(模拟模式)
|
||||||
|
|
||||||
|
当前登录页面支持以下测试账号(当 `VITE_USE_MOCK=true` 时):
|
||||||
|
|
||||||
|
| 账号 | 密码 | 角色 | 描述 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `admin@erp.com` | `password123` | `admin` | 系统管理员,拥有所有权限 |
|
||||||
|
| `user@erp.com` | `user123456` | `user` | 普通用户,基础权限 |
|
||||||
|
| `test@erp.com` | `test123456` | `test` | 测试用户,受限权限 |
|
||||||
|
|
||||||
|
## 🚀 如何使用
|
||||||
|
|
||||||
|
### 1. 启动开发服务器
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# 或
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 访问登录页面
|
||||||
|
打开浏览器访问:`http://localhost:5173/login`
|
||||||
|
|
||||||
|
### 3. 识别模拟模式
|
||||||
|
- **页面右上角**显示黄色"模拟模式"标签
|
||||||
|
- **页脚**显示可用的模拟账号
|
||||||
|
- **登录过程**为模拟延迟(800ms)
|
||||||
|
|
||||||
|
### 4. 登录测试
|
||||||
|
1. 选择"密码登录"方式
|
||||||
|
2. 输入任一测试账号和密码
|
||||||
|
3. 点击"登录"按钮
|
||||||
|
4. 成功后将跳转到首页 (`/`)
|
||||||
|
|
||||||
|
## ⚙️ 环境配置
|
||||||
|
|
||||||
|
### 启用/禁用模拟模式
|
||||||
|
在 `.env.development` 文件中:
|
||||||
|
```env
|
||||||
|
# 启用模拟登录(默认)
|
||||||
|
VITE_USE_MOCK=true
|
||||||
|
|
||||||
|
# 禁用模拟登录,使用真实API
|
||||||
|
VITE_USE_MOCK=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### API配置
|
||||||
|
```env
|
||||||
|
# 后端API地址
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080/api
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 模拟登录实现细节
|
||||||
|
|
||||||
|
### 前端模拟逻辑
|
||||||
|
- **位置**: `src/views/Auth/LoginEnhanced.vue` 中的 `mockLogin()` 函数
|
||||||
|
- **验证**: 检查账号密码是否匹配预设测试账号
|
||||||
|
- **用户数据**: 生成模拟用户信息(ID、角色、权限等)
|
||||||
|
- **状态存储**: 使用 Pinia store + localStorage(模拟记住我)
|
||||||
|
|
||||||
|
### 模拟用户权限
|
||||||
|
| 角色 | 权限 |
|
||||||
|
|------|------|
|
||||||
|
| `admin` | `['dashboard', 'goods', 'order', 'system']` |
|
||||||
|
| `user` | `['dashboard', 'goods']` |
|
||||||
|
| `test` | `['dashboard']` |
|
||||||
|
|
||||||
|
## 🎨 页面功能
|
||||||
|
|
||||||
|
### 登录方式
|
||||||
|
1. **密码登录** - 使用邮箱和密码
|
||||||
|
2. **短信登录** - 界面已完成,API待实现
|
||||||
|
|
||||||
|
### 特色功能
|
||||||
|
- ✅ 明/暗色主题切换(自动记忆)
|
||||||
|
- ✅ 响应式设计(支持移动端)
|
||||||
|
- ✅ 表单实时验证
|
||||||
|
- ✅ 社交登录(微信、企业微信、钉钉)
|
||||||
|
- ✅ 记住我功能
|
||||||
|
- ✅ 协议声明
|
||||||
|
|
||||||
|
## 🐛 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
1. **登录失败**:检查是否输入了正确的测试账号密码
|
||||||
|
2. **页面样式异常**:确保安装了所有依赖 `npm install`
|
||||||
|
3. **路由问题**:检查 `src/router/complete-fixed.ts` 和 `complete.ts` 中的路由配置
|
||||||
|
|
||||||
|
### 开发建议
|
||||||
|
- 模拟模式仅用于前端开发和测试
|
||||||
|
- 对接真实API时,设置 `VITE_USE_MOCK=false`
|
||||||
|
- 正式环境应使用 `.env.production` 配置文件
|
||||||
|
|
||||||
|
## 📁 相关文件
|
||||||
|
- `src/views/Auth/LoginEnhanced.vue` - 登录页面主文件
|
||||||
|
- `src/stores/user.ts` - 用户状态管理
|
||||||
|
- `src/api/auth.ts` - 登录相关API
|
||||||
|
- `.env.development` - 开发环境配置
|
||||||
|
|
||||||
|
---
|
||||||
|
*最后更新: 2026-03-24*
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
0
el.content
Normal file
0
el.content
Normal file
22
eslint.config.js
Normal file
22
eslint.config.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import pluginVue from 'eslint-plugin-vue';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
...pluginVue.configs['flat/recommended'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// 你可以在这里添加或覆盖规则
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>new-erp</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3571
package-lock.json
generated
Normal file
3571
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "new-erp",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"element-plus": "^2.13.5",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
|
"vue": "^3.5.25",
|
||||||
|
"vue-barcode": "^1.3.0",
|
||||||
|
"vue-router": "^5.0.3",
|
||||||
|
"vue3-barcode": "^1.0.1",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
|
"axios-mock-adapter": "^2.1.0",
|
||||||
|
"eslint": "^10.1.0",
|
||||||
|
"eslint-plugin-vue": "^10.8.0",
|
||||||
|
"globals": "^17.4.0",
|
||||||
|
"typescript-eslint": "^8.57.2",
|
||||||
|
"vite": "^8.0.0-beta.13"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"vite": "^8.0.0-beta.13"
|
||||||
|
}
|
||||||
|
}
|
||||||
314
public/diagnose-vue.html
Normal file
314
public/diagnose-vue.html
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vue应用诊断</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||||
|
.test { margin: 20px 0; padding: 15px; border: 1px solid #ccc; }
|
||||||
|
button { padding: 10px 15px; margin: 5px; }
|
||||||
|
pre { background: #f5f5f5; padding: 10px; overflow: auto; }
|
||||||
|
.success { color: green; }
|
||||||
|
.error { color: red; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Vue应用诊断</h1>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>1. 检查Vue应用加载</h3>
|
||||||
|
<button onclick="checkVueApp()">检查Vue应用</button>
|
||||||
|
<div id="vueResult"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>2. 检查环境变量</h3>
|
||||||
|
<button onclick="checkEnvVars()">检查环境变量</button>
|
||||||
|
<div id="envResult"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>3. 测试前端axios实例</h3>
|
||||||
|
<button onclick="testFrontendAxios()">测试前端axios</button>
|
||||||
|
<div id="axiosResult"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<h3>4. 模拟Vue登录页面请求</h3>
|
||||||
|
<button onclick="simulateVueLogin()">模拟Vue登录</button>
|
||||||
|
<div id="vueLoginResult"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/axios-mock-adapter/dist/axios-mock-adapter.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 模拟前端的axios配置
|
||||||
|
const frontendAxios = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟前端的请求拦截器
|
||||||
|
frontendAxios.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
const token = localStorage.getItem('erp_token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 模拟前端的响应拦截器
|
||||||
|
frontendAxios.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
if (response.status === 204) {
|
||||||
|
return { code: 200, message: 'success' };
|
||||||
|
}
|
||||||
|
if (response.data !== undefined && response.data !== null) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
if (response.status === 200) {
|
||||||
|
return { code: 200, message: 'success' };
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.error('请求错误:', error);
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('erp_token');
|
||||||
|
localStorage.removeItem('current_user');
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function showResult(elementId, message, isError = false) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
element.innerHTML = `<pre class="${isError ? 'error' : 'success'}">${message}</pre>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkVueApp() {
|
||||||
|
showResult('vueResult', '检查中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查Vue是否加载
|
||||||
|
const vueLoaded = typeof Vue !== 'undefined';
|
||||||
|
|
||||||
|
// 检查是否有Vue应用挂载
|
||||||
|
const appElement = document.getElementById('app');
|
||||||
|
|
||||||
|
let result = 'Vue应用检查:\n\n';
|
||||||
|
result += `Vue已加载: ${vueLoaded ? '✅ 是' : '❌ 否'}\n`;
|
||||||
|
result += `Vue版本: ${vueLoaded ? Vue.version : '未找到'}\n`;
|
||||||
|
result += `#app元素: ${appElement ? '✅ 找到' : '❌ 未找到'}\n`;
|
||||||
|
|
||||||
|
if (appElement) {
|
||||||
|
result += `#app内容: ${appElement.innerHTML.substring(0, 200)}...\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查路由
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
result += `\n当前路径: ${currentPath}\n`;
|
||||||
|
|
||||||
|
showResult('vueResult', result);
|
||||||
|
} catch (error) {
|
||||||
|
showResult('vueResult', `检查失败: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkEnvVars() {
|
||||||
|
showResult('envResult', '检查中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试获取Vite环境变量
|
||||||
|
const response = await fetch('/@vite/env');
|
||||||
|
let result = '环境变量检查:\n\n';
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
try {
|
||||||
|
const env = await response.json();
|
||||||
|
result += '✅ Vite环境变量可访问\n';
|
||||||
|
result += `VITE_USE_MOCK: ${env.VITE_USE_MOCK || '未找到'}\n`;
|
||||||
|
result += `VITE_API_BASE_URL: ${env.VITE_API_BASE_URL || '未找到'}\n`;
|
||||||
|
result += `MODE: ${env.MODE || '未找到'}\n`;
|
||||||
|
result += `DEV: ${env.DEV || '未找到'}\n`;
|
||||||
|
result += `\n完整环境变量:\n${JSON.stringify(env, null, 2)}`;
|
||||||
|
} catch (e) {
|
||||||
|
result += `❌ 解析环境变量失败: ${e.message}\n`;
|
||||||
|
const text = await response.text();
|
||||||
|
result += `响应文本: ${text.substring(0, 200)}...`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result += '❌ 无法访问Vite环境变量\n';
|
||||||
|
result += `状态码: ${response.status}\n`;
|
||||||
|
|
||||||
|
// 尝试从全局变量获取
|
||||||
|
if (window.__VITE_ENV__) {
|
||||||
|
result += `\n从全局变量找到环境变量:\n`;
|
||||||
|
result += JSON.stringify(window.__VITE_ENV__, null, 2);
|
||||||
|
} else {
|
||||||
|
result += '\n全局变量中未找到环境变量';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showResult('envResult', result);
|
||||||
|
} catch (error) {
|
||||||
|
showResult('envResult', `检查失败: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFrontendAxios() {
|
||||||
|
showResult('axiosResult', '测试中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先清除可能的Mock
|
||||||
|
const mock = new AxiosMockAdapter(frontendAxios, { delayResponse: 300 });
|
||||||
|
|
||||||
|
// 设置Mock
|
||||||
|
mock.onPost('/auth/login').reply((config) => {
|
||||||
|
console.log('前端Mock被调用:', config.data);
|
||||||
|
const data = JSON.parse(config.data);
|
||||||
|
const { email, password } = data;
|
||||||
|
|
||||||
|
if (email === 'admin@erp.com' && password === 'password123') {
|
||||||
|
return [200, {
|
||||||
|
code: 200,
|
||||||
|
message: '登录成功',
|
||||||
|
data: {
|
||||||
|
token: 'frontend-mock-token-' + Date.now(),
|
||||||
|
user: {
|
||||||
|
id: 'user-001',
|
||||||
|
email: 'admin@erp.com',
|
||||||
|
name: '管理员',
|
||||||
|
role: 'admin'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
return [401, {
|
||||||
|
code: 401,
|
||||||
|
message: '邮箱或密码错误'
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = '前端axios测试:\n\n';
|
||||||
|
|
||||||
|
// 测试登录
|
||||||
|
result += '1. 测试登录:\n';
|
||||||
|
try {
|
||||||
|
const loginRes = await frontendAxios.post('/auth/login', {
|
||||||
|
email: 'admin@erp.com',
|
||||||
|
password: 'password123'
|
||||||
|
});
|
||||||
|
|
||||||
|
result += ` 状态码: 200 (通过拦截器转换)\n`;
|
||||||
|
result += ` 响应code: ${loginRes.code}\n`;
|
||||||
|
result += ` 消息: ${loginRes.message}\n`;
|
||||||
|
result += ` Token: ${loginRes.data?.token?.substring(0, 30)}...\n`;
|
||||||
|
result += ` 用户: ${loginRes.data?.user?.name}\n`;
|
||||||
|
|
||||||
|
// 保存token测试拦截器
|
||||||
|
if (loginRes.data?.token) {
|
||||||
|
localStorage.setItem('erp_token', loginRes.data.token);
|
||||||
|
result += `\n✅ Token已保存到localStorage\n`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
result += ` ❌ 登录失败: ${error.message}\n`;
|
||||||
|
if (error.response) {
|
||||||
|
result += ` 实际状态码: ${error.response.status}\n`;
|
||||||
|
result += ` 响应数据: ${JSON.stringify(error.response.data, null, 2)}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试401处理
|
||||||
|
result += '\n2. 测试错误登录(401):\n';
|
||||||
|
try {
|
||||||
|
const errorRes = await frontendAxios.post('/auth/login', {
|
||||||
|
email: 'wrong@email.com',
|
||||||
|
password: 'wrongpassword'
|
||||||
|
});
|
||||||
|
result += ` ❌ 预期失败但成功了: ${JSON.stringify(errorRes, null, 2)}\n`;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
result += ` ✅ 正确返回401\n`;
|
||||||
|
result += ` 错误信息: ${error.response.data?.message}\n`;
|
||||||
|
} else {
|
||||||
|
result += ` ❌ 其他错误: ${error.message}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复Mock
|
||||||
|
mock.restore();
|
||||||
|
|
||||||
|
showResult('axiosResult', result);
|
||||||
|
} catch (error) {
|
||||||
|
showResult('axiosResult', `测试失败: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simulateVueLogin() {
|
||||||
|
showResult('vueLoginResult', '模拟中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟Vue登录页面的请求
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: 'admin@erp.com',
|
||||||
|
password: 'password123'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = '模拟Vue登录请求:\n\n';
|
||||||
|
result += `URL: ${response.url}\n`;
|
||||||
|
result += `状态码: ${response.status}\n`;
|
||||||
|
result += `状态文本: ${response.statusText}\n`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await response.text();
|
||||||
|
result += `\n响应文本: ${text}\n`;
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
result += `\n解析后的JSON:\n${JSON.stringify(data, null, 2)}`;
|
||||||
|
} catch (e) {
|
||||||
|
result += `\nJSON解析失败: ${e.message}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result += '\n响应为空';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
result += `\n读取响应失败: ${e.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showResult('vueLoginResult', result, !response.ok);
|
||||||
|
} catch (error) {
|
||||||
|
showResult('vueLoginResult', `模拟失败: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时自动检查
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
console.log('Vue诊断页面已加载');
|
||||||
|
checkVueApp();
|
||||||
|
checkEnvVars();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
340
public/diagnose.html
Normal file
340
public/diagnose.html
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ERP系统诊断页面</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.status-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
.status-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
border-left: 5px solid;
|
||||||
|
}
|
||||||
|
.status-card.success { border-color: #28a745; }
|
||||||
|
.status-card.error { border-color: #dc3545; }
|
||||||
|
.status-card.warning { border-color: #ffc107; }
|
||||||
|
.status-card.info { border-color: #17a2b8; }
|
||||||
|
.status-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.status-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.test-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.test-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.test-btn.primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.test-btn.secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.test-btn.success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.test-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.log-container {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.log-entry {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.log-entry.info { background: rgba(0, 123, 255, 0.1); }
|
||||||
|
.log-entry.success { background: rgba(40, 167, 69, 0.1); }
|
||||||
|
.log-entry.error { background: rgba(220, 53, 69, 0.1); }
|
||||||
|
.log-entry.warning { background: rgba(255, 193, 7, 0.1); }
|
||||||
|
.instructions {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 30px 0;
|
||||||
|
border-left: 5px solid #007bff;
|
||||||
|
}
|
||||||
|
.instructions h3 {
|
||||||
|
color: #0056b3;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.instructions ol {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
.instructions li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🔧 ERP系统诊断工具</h1>
|
||||||
|
<div class="subtitle">服务器状态检测与问题诊断</div>
|
||||||
|
|
||||||
|
<div class="status-grid">
|
||||||
|
<div class="status-card success" id="server-status">
|
||||||
|
<div class="status-title">
|
||||||
|
<span class="status-icon">🌐</span>
|
||||||
|
<span>服务器状态</span>
|
||||||
|
</div>
|
||||||
|
<div id="server-details">检测中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card info" id="port-status">
|
||||||
|
<div class="status-title">
|
||||||
|
<span class="status-icon">🔌</span>
|
||||||
|
<span>端口信息</span>
|
||||||
|
</div>
|
||||||
|
<div id="port-details">检测中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card warning" id="api-status">
|
||||||
|
<div class="status-title">
|
||||||
|
<span class="status-icon">🔄</span>
|
||||||
|
<span>API状态</span>
|
||||||
|
</div>
|
||||||
|
<div id="api-details">待测试</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card" id="vue-status">
|
||||||
|
<div class="status-title">
|
||||||
|
<span class="status-icon">⚡</span>
|
||||||
|
<span>Vue应用状态</span>
|
||||||
|
</div>
|
||||||
|
<div id="vue-details">待测试</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instructions">
|
||||||
|
<h3>📋 诊断步骤</h3>
|
||||||
|
<ol>
|
||||||
|
<li>点击下面的测试按钮检查各项功能</li>
|
||||||
|
<li>查看右侧日志区域的结果</li>
|
||||||
|
<li>如果测试失败,按F12查看浏览器控制台错误</li>
|
||||||
|
<li>将错误信息截图发送给技术支持</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-buttons">
|
||||||
|
<button class="test-btn primary" onclick="testServer()">测试服务器连接</button>
|
||||||
|
<button class="test-btn secondary" onclick="testStaticFiles()">测试静态文件</button>
|
||||||
|
<button class="test-btn success" onclick="testVueApp()">测试Vue应用</button>
|
||||||
|
<button class="test-btn" onclick="testMockAPI()">测试Mock API</button>
|
||||||
|
<button class="test-btn" onclick="clearLogs()">清空日志</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-container" id="log-output">
|
||||||
|
<div class="log-entry info">诊断工具已加载,点击按钮开始测试...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const logOutput = document.getElementById('log-output');
|
||||||
|
const serverDetails = document.getElementById('server-details');
|
||||||
|
const portDetails = document.getElementById('port-details');
|
||||||
|
const apiDetails = document.getElementById('api-details');
|
||||||
|
const vueDetails = document.getElementById('vue-details');
|
||||||
|
|
||||||
|
let logCount = 0;
|
||||||
|
|
||||||
|
function addLog(message, type = 'info') {
|
||||||
|
logCount++;
|
||||||
|
const logEntry = document.createElement('div');
|
||||||
|
logEntry.className = `log-entry ${type}`;
|
||||||
|
logEntry.innerHTML = `<strong>${logCount}.</strong> ${message}`;
|
||||||
|
logOutput.appendChild(logEntry);
|
||||||
|
logOutput.scrollTop = logOutput.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(cardId, status, message) {
|
||||||
|
const card = document.getElementById(cardId);
|
||||||
|
card.className = `status-card ${status}`;
|
||||||
|
const details = document.getElementById(`${cardId.replace('-status', '-details')}`);
|
||||||
|
details.innerHTML = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testServer() {
|
||||||
|
addLog('开始测试服务器连接...', 'info');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/');
|
||||||
|
if (response.ok) {
|
||||||
|
updateStatus('server-status', 'success', '✅ 服务器连接正常');
|
||||||
|
addLog('服务器连接测试成功', 'success');
|
||||||
|
} else {
|
||||||
|
updateStatus('server-status', 'error', '❌ 服务器响应异常');
|
||||||
|
addLog(`服务器响应异常: ${response.status} ${response.statusText}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateStatus('server-status', 'error', '❌ 服务器连接失败');
|
||||||
|
addLog(`服务器连接失败: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testStaticFiles() {
|
||||||
|
addLog('开始测试静态文件加载...', 'info');
|
||||||
|
const testFiles = [
|
||||||
|
'/favicon.ico',
|
||||||
|
'/test-simple.html',
|
||||||
|
'/diagnose.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (const file of testFiles) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(file);
|
||||||
|
if (response.ok) {
|
||||||
|
successCount++;
|
||||||
|
addLog(`✅ ${file} 加载成功`, 'success');
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
addLog(`❌ ${file} 加载失败: ${response.status}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failCount++;
|
||||||
|
addLog(`❌ ${file} 加载失败: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failCount === 0) {
|
||||||
|
updateStatus('port-status', 'success', `✅ 所有静态文件加载正常 (${successCount}/${testFiles.length})`);
|
||||||
|
} else {
|
||||||
|
updateStatus('port-status', 'warning', `⚠️ 部分文件加载失败 (${successCount}成功/${failCount}失败)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testVueApp() {
|
||||||
|
addLog('开始测试Vue应用...', 'info');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/');
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
if (html.includes('vue') || html.includes('Vue') || html.includes('router-view')) {
|
||||||
|
updateStatus('vue-status', 'success', '✅ Vue应用结构正常');
|
||||||
|
addLog('Vue应用HTML结构检测正常', 'success');
|
||||||
|
} else {
|
||||||
|
updateStatus('vue-status', 'warning', '⚠️ Vue应用结构异常');
|
||||||
|
addLog('Vue应用HTML结构异常,可能未正确加载', 'warning');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateStatus('vue-status', 'error', '❌ Vue应用测试失败');
|
||||||
|
addLog(`Vue应用测试失败: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testMockAPI() {
|
||||||
|
addLog('开始测试Mock API...', 'info');
|
||||||
|
const apiEndpoints = [
|
||||||
|
'/api/print/batches',
|
||||||
|
'/api/print/orders',
|
||||||
|
'/api/system/status'
|
||||||
|
];
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (const endpoint of apiEndpoints) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint);
|
||||||
|
if (response.ok) {
|
||||||
|
successCount++;
|
||||||
|
addLog(`✅ ${endpoint} 响应正常`, 'success');
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
addLog(`❌ ${endpoint} 响应异常: ${response.status}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failCount++;
|
||||||
|
addLog(`❌ ${endpoint} 请求失败: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
updateStatus('api-status', 'success', `✅ Mock API部分正常 (${successCount}成功/${failCount}失败)`);
|
||||||
|
} else {
|
||||||
|
updateStatus('api-status', 'error', `❌ Mock API全部失败 (${failCount}失败)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLogs() {
|
||||||
|
logOutput.innerHTML = '';
|
||||||
|
logCount = 0;
|
||||||
|
addLog('日志已清空', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时自动测试
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
const port = currentUrl.split(':')[2]?.split('/')[0] || '未知';
|
||||||
|
portDetails.innerHTML = `当前端口: ${port}<br>URL: ${currentUrl}`;
|
||||||
|
|
||||||
|
// 自动测试服务器
|
||||||
|
setTimeout(testServer, 1000);
|
||||||
|
setTimeout(testStaticFiles, 1500);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
122
public/emergency-print.html
Normal file
122
public/emergency-print.html
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
checkboxes.forEach(cb => cb.checked = false)
|
||||||
|
updateSelectionStatus()
|
||||||
|
updateStatus('选择已清除', 'success')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
function resetFilter() {
|
||||||
|
document.getElementById('statusFilter').value = '1'
|
||||||
|
loadOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开打印对话框
|
||||||
|
function openPrintDialog() {
|
||||||
|
if (selectedOrderIds.size === 0) {
|
||||||
|
alert('请选择订单')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中的订单
|
||||||
|
const selectedOrders = orders.filter(order => selectedOrderIds.has(order.id))
|
||||||
|
|
||||||
|
// 更新对话框内容
|
||||||
|
const ordersList = document.getElementById('selectedOrdersList')
|
||||||
|
ordersList.innerHTML = selectedOrders.slice(0, 10).map(order => `
|
||||||
|
<div class="order-item">
|
||||||
|
<span>${order.shortId}</span>
|
||||||
|
<span>${order.platformOrderSn}</span>
|
||||||
|
<span>${order.receiverName}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
|
|
||||||
|
if (selectedOrders.length > 10) {
|
||||||
|
ordersList.innerHTML += `<div class="order-item" style="text-align:center;color:#909399;">
|
||||||
|
还有 ${selectedOrders.length - 10} 个订单...
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
document.getElementById('expressSelect').value = ''
|
||||||
|
document.getElementById('templateSelect').value = ''
|
||||||
|
document.getElementById('remark').value = ''
|
||||||
|
|
||||||
|
// 显示对话框
|
||||||
|
document.getElementById('printDialog').style.display = 'flex'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭打印对话框
|
||||||
|
function closePrintDialog() {
|
||||||
|
document.getElementById('printDialog').style.display = 'none'
|
||||||
|
document.getElementById('printLoading').style.display = 'none'
|
||||||
|
document.getElementById('printError').style.display = 'none'
|
||||||
|
isPrinting = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始打印
|
||||||
|
function startPrint() {
|
||||||
|
if (isPrinting) return
|
||||||
|
|
||||||
|
const expressId = document.getElementById('expressSelect').value
|
||||||
|
const templateId = document.getElementById('templateSelect').value
|
||||||
|
|
||||||
|
if (!expressId || !templateId) {
|
||||||
|
alert('请选择快递公司和模板')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isPrinting = true
|
||||||
|
document.getElementById('startPrintBtn').disabled = true
|
||||||
|
document.getElementById('printLoading').style.display = 'block'
|
||||||
|
document.getElementById('printError').style.display = 'none'
|
||||||
|
|
||||||
|
// 模拟打印过程
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
// 模拟打印成功
|
||||||
|
const selectedCount = selectedOrderIds.size
|
||||||
|
|
||||||
|
// 清空选择
|
||||||
|
selectedOrderIds.clear()
|
||||||
|
updateSelectionStatus()
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
closePrintDialog()
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
updateStatus(`成功打印 ${selectedCount} 个订单`, 'success')
|
||||||
|
alert(`打印完成!成功打印 ${selectedCount} 个订单`)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('printError').textContent = '打印失败: ' + error.message
|
||||||
|
document.getElementById('printError').style.display = 'block'
|
||||||
|
updateStatus('打印失败', 'error')
|
||||||
|
} finally {
|
||||||
|
isPrinting = false
|
||||||
|
document.getElementById('startPrintBtn').disabled = false
|
||||||
|
document.getElementById('printLoading').style.display = 'none'
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加键盘快捷键
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
// ESC键关闭对话框
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closePrintDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+Enter 开始打印
|
||||||
|
if (event.ctrlKey && event.key === 'Enter') {
|
||||||
|
if (document.getElementById('printDialog').style.display === 'flex') {
|
||||||
|
startPrint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
console.log('🆘 紧急可用版初始化完成')
|
||||||
|
console.log('如果此页面正常但Vue页面空白,说明是Vue编译问题')
|
||||||
|
console.log('请检查浏览器控制台是否有Vue编译错误')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
426
public/login-fallback.html
Normal file
426
public/login-fallback.html
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ERP系统登录</title>
|
||||||
|
<!-- Element Plus CSS -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
|
||||||
|
<!-- Element Plus Icons -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@element-plus/icons-vue/dist/index.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-account {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-account p {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-message {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-box {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-options {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1 class="login-title">ERP管理系统</h1>
|
||||||
|
<p class="login-subtitle">欢迎回来,请登录您的账户</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="loginForm" class="login-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<el-input
|
||||||
|
v-model="email"
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
size="large"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<el-input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Lock /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-options">
|
||||||
|
<el-checkbox v-model="rememberMe" label="记住我" />
|
||||||
|
<el-link type="primary" @click="goToForgotPassword" class="forgot-password">
|
||||||
|
忘记密码?
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
class="login-btn"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
{{ loading ? '登录中...' : '登录' }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<div class="register-link">
|
||||||
|
还没有账号?
|
||||||
|
<el-link type="primary" @click="goToRegister">
|
||||||
|
立即注册
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
<div class="test-account">
|
||||||
|
<p><strong>测试账号:</strong> admin@erp.com / password123</p>
|
||||||
|
<p><strong>API地址:</strong> <span id="apiUrl">/api</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="copyright">
|
||||||
|
© 2024 ERP管理系统. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vue 3 -->
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
|
<!-- Element Plus -->
|
||||||
|
<script src="https://unpkg.com/element-plus/dist/index.full.js"></script>
|
||||||
|
<!-- Element Plus Icons -->
|
||||||
|
<script src="https://unpkg.com/@element-plus/icons-vue/dist/index.js"></script>
|
||||||
|
<!-- Axios -->
|
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 显示API地址
|
||||||
|
document.getElementById('apiUrl').textContent = window.location.origin + '/api';
|
||||||
|
|
||||||
|
// 创建Vue应用
|
||||||
|
const { createApp, ref } = Vue;
|
||||||
|
const { ElMessage, ElLoading } = ElementPlus;
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
setup() {
|
||||||
|
const email = ref('admin@erp.com');
|
||||||
|
const password = ref('password123');
|
||||||
|
const rememberMe = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 创建axios实例(模拟前端的配置)
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加请求拦截器
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
const token = localStorage.getItem('erp_token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加响应拦截器
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
if (response.status === 204) {
|
||||||
|
return { code: 200, message: 'success' };
|
||||||
|
}
|
||||||
|
if (response.data !== undefined && response.data !== null) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
if (response.status === 200) {
|
||||||
|
return { code: 200, message: 'success' };
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.error('请求错误:', error);
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('erp_token');
|
||||||
|
localStorage.removeItem('current_user');
|
||||||
|
|
||||||
|
// 如果当前不是登录页,跳转到登录页
|
||||||
|
if (!window.location.pathname.includes('/login')) {
|
||||||
|
ElMessage.error('会话已过期,请重新登录');
|
||||||
|
window.location.href = '/login-fallback.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!email.value || !password.value) {
|
||||||
|
ElMessage.warning('请输入邮箱和密码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
|
||||||
|
ElMessage.warning('请输入有效的邮箱地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.value.length < 6) {
|
||||||
|
ElMessage.warning('密码长度至少6位');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post('/auth/login', {
|
||||||
|
email: email.value,
|
||||||
|
password: password.value,
|
||||||
|
remember: rememberMe.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
ElMessage.success('登录成功');
|
||||||
|
|
||||||
|
// 保存token和用户信息
|
||||||
|
localStorage.setItem('erp_token', response.data.token);
|
||||||
|
localStorage.setItem('current_user', JSON.stringify(response.data.user));
|
||||||
|
if (rememberMe.value) {
|
||||||
|
localStorage.setItem('remember_me', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到首页
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '登录失败');
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error);
|
||||||
|
|
||||||
|
if (error.response?.data?.message) {
|
||||||
|
ElMessage.error(error.response.data.message);
|
||||||
|
} else if (error.message) {
|
||||||
|
ElMessage.error(`登录失败: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
ElMessage.error('登录失败,请检查网络或服务器状态');
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToRegister = () => {
|
||||||
|
window.location.href = '/register';
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToForgotPassword = () => {
|
||||||
|
window.location.href = '/forgot-password';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查是否已登录
|
||||||
|
const checkLoginStatus = () => {
|
||||||
|
const token = localStorage.getItem('erp_token');
|
||||||
|
if (token) {
|
||||||
|
// 如果已登录,跳转到首页
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面加载时检查登录状态
|
||||||
|
checkLoginStatus();
|
||||||
|
|
||||||
|
// 回车键登录
|
||||||
|
const handleKeyup = (event) => {
|
||||||
|
if (event.key === 'Enter' && !loading.value) {
|
||||||
|
handleLogin();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加键盘事件监听
|
||||||
|
window.addEventListener('keyup', handleKeyup);
|
||||||
|
|
||||||
|
// 组件卸载时清理事件监听
|
||||||
|
return {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
rememberMe,
|
||||||
|
loading,
|
||||||
|
handleLogin,
|
||||||
|
goToRegister,
|
||||||
|
goToForgotPassword
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册Element Plus
|
||||||
|
app.use(ElementPlus);
|
||||||
|
|
||||||
|
// 注册Element Plus图标
|
||||||
|
for (const [key, component] of Object.entries(window.ElementPlusIconsVue || {})) {
|
||||||
|
app.component(key, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 挂载应用
|
||||||
|
app.mount('#app');
|
||||||
|
|
||||||
|
// 页面加载完成后的初始化
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
console.log('✅ 备用登录页面已加载');
|
||||||
|
console.log('🔧 Mock状态: 已启用');
|
||||||
|
console.log('📡 API地址: /api');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
public/plugins/cainiao-install.bat
Normal file
35
public/plugins/cainiao-install.bat
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 > nul
|
||||||
|
echo ═══════════════════════════════════════════════
|
||||||
|
echo 菜鸟打印插件 模拟安装程序 v2.5.8
|
||||||
|
echo ═══════════════════════════════════════════════
|
||||||
|
echo.
|
||||||
|
echo [1/4] 检查系统环境...
|
||||||
|
timeout /t 1 /nobreak > nul
|
||||||
|
echo ✓ Windows 版本检测通过
|
||||||
|
echo.
|
||||||
|
echo [2/4] 检测打印机驱动...
|
||||||
|
timeout /t 1 /nobreak > nul
|
||||||
|
echo ✓ 打印机驱动就绪
|
||||||
|
echo.
|
||||||
|
echo [3/4] 安装核心组件...
|
||||||
|
timeout /t 2 /nobreak > nul
|
||||||
|
echo ✓ C-Lodop 驱动安装成功
|
||||||
|
echo.
|
||||||
|
echo [4/4] 注册系统服务...
|
||||||
|
timeout /t 1 /nobreak > nul
|
||||||
|
echo ✓ 服务注册完成
|
||||||
|
echo.
|
||||||
|
echo ═══════════════════════════════════════════════
|
||||||
|
echo ✅ 安装成功!
|
||||||
|
echo ═══════════════════════════════════════════════
|
||||||
|
echo.
|
||||||
|
echo 插件信息:
|
||||||
|
名称: 菜鸟打印插件
|
||||||
|
版本: v2.5.8
|
||||||
|
设备ID: DEMO-DEVICE-%random%
|
||||||
|
安装时间: %date% %time%
|
||||||
|
echo.
|
||||||
|
echo 请访问 http://localhost:5174/print/plugin 进行插件注册
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
14
public/plugins/cainiao-setup.exe
Normal file
14
public/plugins/cainiao-setup.exe
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@echo off
|
||||||
|
echo =============================================
|
||||||
|
echo 菜鸟打印插件 安装程序 v2.5.8
|
||||||
|
echo =============================================
|
||||||
|
echo.
|
||||||
|
echo 正在安装...
|
||||||
|
timeout /t 2 /nobreak > nul
|
||||||
|
echo.
|
||||||
|
echo [========================================]
|
||||||
|
echo 安装完成!
|
||||||
|
echo [========================================]
|
||||||
|
echo.
|
||||||
|
echo 按任意键退出...
|
||||||
|
pause > nul
|
||||||
27
public/plugins/pdd-install.bat
Normal file
27
public/plugins/pdd-install.bat
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 > nul
|
||||||
|
echo ═══════════════════════════════════════════════
|
||||||
|
echo 拼多多打印插件 模拟安装程序 v1.8.3
|
||||||
|
echo ═══════════════════════════════════════════════
|
||||||
|
echo.
|
||||||
|
echo [1/4] 检查系统环境...
|
||||||
|
timeout /t 1 /nobreak > nul
|
||||||
|
echo ✓ Windows 版本检测通过
|
||||||
|
echo.
|
||||||
|
echo [2/4] 检测打印机驱动...
|
||||||
|
timeout /t 1 /nobreak > nul
|
||||||
|
echo ✓ 打印机驱动就绪
|
||||||
|
echo.
|
||||||
|
echo [3/4] 安装核心组件...
|
||||||
|
timeout /t 2 /nobreak > nul
|
||||||
|
echo ✓ 拼多多打印服务安装成功
|
||||||
|
echo.
|
||||||
|
echo [4/4] 注册系统服务...
|
||||||
|
timeout /t 1 /nobreak > nul
|
||||||
|
echo ✓ 服务注册完成
|
||||||
|
echo.
|
||||||
|
echo ═══════════════════════════════════════════════
|
||||||
|
echo ✅ 安装成功!
|
||||||
|
echo ═══════════════════════════════════════════════
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
280
public/plugins/print-plugin-client.js
Normal file
280
public/plugins/print-plugin-client.js
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* 打印插件客户端 - 模拟版本
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 注册到ERP系统
|
||||||
|
* 2. 发送心跳
|
||||||
|
* 3. 获取打印任务
|
||||||
|
* 4. 模拟打印
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* node print-plugin-client.js <命令> [参数]
|
||||||
|
*
|
||||||
|
* 示例:
|
||||||
|
* node print-plugin-client.js register cainiao
|
||||||
|
* node print-plugin-client.js heartbeat
|
||||||
|
* node print-plugin-client.js getjob
|
||||||
|
*/
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
const CONFIG = {
|
||||||
|
API_BASE: 'http://localhost:10088/api', // 后端API地址
|
||||||
|
DEVICE_ID: 'PLUGIN-' + Date.now().toString(36).toUpperCase(),
|
||||||
|
DEVICE_NAME: process.env.COMPUTERNAME || 'Unknown-PC',
|
||||||
|
OS_VERSION: process.release.name + ' ' + process.platform,
|
||||||
|
POLL_INTERVAL: 5000, // 5秒轮询一次
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟插件信息
|
||||||
|
const PLUGIN_INFO = {
|
||||||
|
cainiao: { code: 'cainiao', name: '菜鸟打印插件', version: '2.5.8' },
|
||||||
|
pdd: { code: 'pdd', name: '拼多多打印插件', version: '1.8.3' },
|
||||||
|
douyin: { code: 'douyin', name: '抖音小店打印插件', version: '3.2.1' },
|
||||||
|
kuaishou: { code: 'kuaishou', name: '快手小店打印插件', version: '2.1.5' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// API请求封装
|
||||||
|
function apiRequest(method, path, data = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(path, CONFIG.API_BASE);
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || 80,
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(body));
|
||||||
|
} catch {
|
||||||
|
resolve(body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
// API不可用时返回模拟数据
|
||||||
|
resolve({ code: 200, data: getMockResponse(method, path, data) });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
req.write(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟API响应
|
||||||
|
function getMockResponse(method, path, data) {
|
||||||
|
if (path.includes('/heartbeat')) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
if (path.includes('/register')) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
installation_id: Date.now(),
|
||||||
|
has_update: false,
|
||||||
|
latest_version: '2.5.8'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (path.includes('/next-job')) {
|
||||||
|
// 模拟返回打印任务
|
||||||
|
return {
|
||||||
|
id: Math.floor(Math.random() * 1000),
|
||||||
|
job_no: 'PJ' + Date.now().toString().slice(-10),
|
||||||
|
platform: 'pdd',
|
||||||
|
plugin_code: 'pdd',
|
||||||
|
print_data: {
|
||||||
|
receiverName: '张三',
|
||||||
|
receiverPhone: '138****8000',
|
||||||
|
receiverAddress: '北京市朝阳区某某街道某某小区',
|
||||||
|
expressNo: 'SF' + Math.random().toString().slice(2, 14),
|
||||||
|
goodsName: '商品测试',
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
status: 'pending',
|
||||||
|
priority: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印任务到控制台
|
||||||
|
function printJob(job) {
|
||||||
|
console.log('\n╔══════════════════════════════════════════════╗');
|
||||||
|
console.log('║ 🎯 收到打印任务 ║');
|
||||||
|
console.log('╠══════════════════════════════════════════════╣');
|
||||||
|
console.log(`║ 任务编号: ${job.job_no.padEnd(30)}║`);
|
||||||
|
console.log(`║ 平台: ${job.platform.padEnd(36)}║`);
|
||||||
|
console.log(`║ 收件人: ${(job.print_data?.receiverName || '-').padEnd(34)}║`);
|
||||||
|
console.log(`║ 电话: ${(job.print_data?.receiverPhone || '-').padEnd(35)}║`);
|
||||||
|
console.log(`║ 地址: ${(job.print_data?.receiverAddress || '-').substring(0, 30).padEnd(30)}║`);
|
||||||
|
console.log(`║ 快递单号: ${(job.print_data?.expressNo || '-').padEnd(31)}║`);
|
||||||
|
console.log('╠══════════════════════════════════════════════╣');
|
||||||
|
console.log('║ 📄 正在模拟打印... ║');
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('║ ✅ 打印完成! ║');
|
||||||
|
console.log('╚══════════════════════════════════════════════╝\n');
|
||||||
|
resolve(true);
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令处理
|
||||||
|
async function handleCommand(cmd, args) {
|
||||||
|
switch (cmd) {
|
||||||
|
case 'register':
|
||||||
|
await registerPlugin(args[0] || 'cainiao');
|
||||||
|
break;
|
||||||
|
case 'heartbeat':
|
||||||
|
await sendHeartbeat();
|
||||||
|
break;
|
||||||
|
case 'getjob':
|
||||||
|
await getNextJob();
|
||||||
|
break;
|
||||||
|
case 'daemon':
|
||||||
|
await runDaemon(args[0] || 'cainiao');
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
showInfo();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
showHelp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册插件
|
||||||
|
async function registerPlugin(pluginCode) {
|
||||||
|
const plugin = PLUGIN_INFO[pluginCode] || PLUGIN_INFO.cainiao;
|
||||||
|
|
||||||
|
console.log(`\n📦 正在注册 ${plugin.name}...`);
|
||||||
|
console.log(` 设备ID: ${CONFIG.DEVICE_ID}`);
|
||||||
|
console.log(` 设备名称: ${CONFIG.DEVICE_NAME}`);
|
||||||
|
console.log(` 插件版本: ${plugin.version}`);
|
||||||
|
|
||||||
|
const result = await apiRequest('POST', '/print-plugins/auth/register', {
|
||||||
|
plugin_code: plugin.code,
|
||||||
|
version: plugin.version,
|
||||||
|
device_id: CONFIG.DEVICE_ID,
|
||||||
|
device_name: CONFIG.DEVICE_NAME,
|
||||||
|
os_version: CONFIG.OS_VERSION,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success || result.code === 200) {
|
||||||
|
console.log('✅ 注册成功!');
|
||||||
|
console.log(` 安装ID: ${result.installation_id}`);
|
||||||
|
if (result.has_update) {
|
||||||
|
console.log(` ⚠️ 有新版本: ${result.latest_version}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('❌ 注册失败:', result.message || '未知错误');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送心跳
|
||||||
|
async function sendHeartbeat() {
|
||||||
|
const result = await apiRequest('POST', '/print-plugins/auth/heartbeat', {
|
||||||
|
device_id: CONFIG.DEVICE_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success || result.code === 200) {
|
||||||
|
console.log('💓 心跳发送成功');
|
||||||
|
} else {
|
||||||
|
console.log('❌ 心跳失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取打印任务
|
||||||
|
async function getNextJob() {
|
||||||
|
const result = await apiRequest('GET', `/print-device/next-job?device_id=${CONFIG.DEVICE_ID}`);
|
||||||
|
|
||||||
|
if (result && result.id) {
|
||||||
|
await printJob(result);
|
||||||
|
|
||||||
|
// 标记完成
|
||||||
|
await apiRequest('POST', '/print-device/complete', {
|
||||||
|
job_id: result.id,
|
||||||
|
device_id: CONFIG.DEVICE_ID,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('📭 暂无打印任务');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行守护进程
|
||||||
|
async function runDaemon(pluginCode) {
|
||||||
|
const plugin = PLUGIN_INFO[pluginCode] || PLUGIN_INFO.cainiao;
|
||||||
|
|
||||||
|
console.log('\n🚀 启动打印插件守护进程...');
|
||||||
|
console.log(` 插件: ${plugin.name}`);
|
||||||
|
console.log(` 轮询间隔: ${CONFIG.POLL_INTERVAL / 1000}秒`);
|
||||||
|
console.log(' 按 Ctrl+C 停止\n');
|
||||||
|
|
||||||
|
// 先注册
|
||||||
|
await registerPlugin(pluginCode);
|
||||||
|
|
||||||
|
// 启动心跳
|
||||||
|
setInterval(async () => {
|
||||||
|
await sendHeartbeat();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// 轮询打印任务
|
||||||
|
setInterval(async () => {
|
||||||
|
await getNextJob();
|
||||||
|
}, CONFIG.POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示信息
|
||||||
|
function showInfo() {
|
||||||
|
console.log('\n📋 插件客户端信息');
|
||||||
|
console.log('═══════════════════════════════');
|
||||||
|
console.log(` 设备ID: ${CONFIG.DEVICE_ID}`);
|
||||||
|
console.log(` 设备名称: ${CONFIG.DEVICE_NAME}`);
|
||||||
|
console.log(` 操作系统: ${CONFIG.OS_VERSION}`);
|
||||||
|
console.log(` API地址: ${CONFIG.API_BASE}`);
|
||||||
|
console.log(` 轮询间隔: ${CONFIG.POLL_INTERVAL / 1000}秒`);
|
||||||
|
console.log('═══════════════════════════════════════\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 帮助
|
||||||
|
function showHelp() {
|
||||||
|
console.log(`
|
||||||
|
🔌 打印插件客户端 - 使用说明
|
||||||
|
|
||||||
|
用法: node print-plugin-client.js <命令> [参数]
|
||||||
|
|
||||||
|
命令:
|
||||||
|
register <插件> 注册插件 (cainiao|pdd|douyin|kuaishou)
|
||||||
|
heartbeat 发送心跳
|
||||||
|
getjob 获取一个打印任务
|
||||||
|
daemon <插件> 运行守护进程模式
|
||||||
|
info 显示客户端信息
|
||||||
|
help 显示帮助
|
||||||
|
|
||||||
|
示例:
|
||||||
|
node print-plugin-client.js register cainiao
|
||||||
|
node print-plugin-client.js daemon pdd
|
||||||
|
node print-plugin-client.js info
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 默认API地址为 http://localhost:10088/api
|
||||||
|
- 如需修改,请编辑 CONFIG.API_BASE
|
||||||
|
- 守护进程模式会自动注册并每30秒发送心跳
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主入口
|
||||||
|
const [,, cmd, ...args] = process.argv;
|
||||||
|
handleCommand(cmd, args).catch(console.error);
|
||||||
449
public/simple-login.html
Normal file
449
public/simple-login.html
Normal file
@ -0,0 +1,449 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ERP系统登录</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.error {
|
||||||
|
border-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-me {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgot-password:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-account {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-account p {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: #f0f9eb;
|
||||||
|
color: #67c23a;
|
||||||
|
border: 1px solid #e1f3d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #fef0f0;
|
||||||
|
color: #f56c6c;
|
||||||
|
border: 1px solid #fde2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-box {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-options {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1 class="login-title">ERP管理系统</h1>
|
||||||
|
<p class="login-subtitle">欢迎回来,请登录您的账户</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message" class="message"></div>
|
||||||
|
|
||||||
|
<form id="loginForm" class="login-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">邮箱</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
value="admin@erp.com"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<div id="emailError" class="error-message">请输入有效的邮箱地址</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">密码</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
value="password123"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<div id="passwordError" class="error-message">密码不能少于6位</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-options">
|
||||||
|
<div class="remember-me">
|
||||||
|
<input type="checkbox" id="rememberMe">
|
||||||
|
<label for="rememberMe">记住我</label>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="forgot-password">忘记密码?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="login-btn" id="loginBtn">
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="register-link">
|
||||||
|
还没有账号? <a href="/register">立即注册</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
<div class="test-account">
|
||||||
|
<p><strong>测试账号:</strong> admin@erp.com / password123</p>
|
||||||
|
<p><strong>API地址:</strong> <span id="apiUrl">/api</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="copyright">
|
||||||
|
© 2024 ERP管理系统. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 显示API地址
|
||||||
|
document.getElementById('apiUrl').textContent = window.location.origin + '/api';
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
function validateForm() {
|
||||||
|
let isValid = true;
|
||||||
|
const email = document.getElementById('email').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
// 验证邮箱
|
||||||
|
const emailError = document.getElementById('emailError');
|
||||||
|
const emailInput = document.getElementById('email');
|
||||||
|
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
emailError.style.display = 'block';
|
||||||
|
emailInput.classList.add('error');
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
emailError.style.display = 'none';
|
||||||
|
emailInput.classList.remove('error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
const passwordError = document.getElementById('passwordError');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
if (!password || password.length < 6) {
|
||||||
|
passwordError.style.display = 'block';
|
||||||
|
passwordInput.classList.add('error');
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
passwordError.style.display = 'none';
|
||||||
|
passwordInput.classList.remove('error');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示消息
|
||||||
|
function showMessage(text, type = 'success') {
|
||||||
|
const messageEl = document.getElementById('message');
|
||||||
|
messageEl.textContent = text;
|
||||||
|
messageEl.className = `message ${type}`;
|
||||||
|
messageEl.style.display = 'block';
|
||||||
|
|
||||||
|
if (type === 'success') {
|
||||||
|
setTimeout(() => {
|
||||||
|
messageEl.style.display = 'none';
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录
|
||||||
|
async function handleLogin(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = document.getElementById('email').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const rememberMe = document.getElementById('rememberMe').checked;
|
||||||
|
|
||||||
|
const loginBtn = document.getElementById('loginBtn');
|
||||||
|
loginBtn.disabled = true;
|
||||||
|
loginBtn.textContent = '登录中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
remember: rememberMe
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.code === 200) {
|
||||||
|
showMessage('登录成功!正在跳转...', 'success');
|
||||||
|
|
||||||
|
// 保存token
|
||||||
|
localStorage.setItem('erp_token', data.data.token);
|
||||||
|
localStorage.setItem('current_user', JSON.stringify(data.data.user));
|
||||||
|
if (rememberMe) {
|
||||||
|
localStorage.setItem('remember_me', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到首页
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/';
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
showMessage(data.message || '登录失败', 'error');
|
||||||
|
loginBtn.disabled = false;
|
||||||
|
loginBtn.textContent = '登录';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error);
|
||||||
|
showMessage('网络错误,请检查API服务', 'error');
|
||||||
|
loginBtn.disabled = false;
|
||||||
|
loginBtn.textContent = '登录';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', handleLogin);
|
||||||
|
|
||||||
|
// 实时验证
|
||||||
|
document.getElementById('email').addEventListener('input', validateForm);
|
||||||
|
document.getElementById('password').addEventListener('input', validateForm);
|
||||||
|
|
||||||
|
// 检查是否已登录
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const token = localStorage.getItem('erp_token');
|
||||||
|
if (token) {
|
||||||
|
// 如果已登录,跳转到首页
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试API连接
|
||||||
|
testApiConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试API连接
|
||||||
|
async function testApiConnection() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: 'test', password: 'test' })
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('API测试响应:', response.status);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.log('✅ Mock API正常工作 (返回401表示Mock已启用)');
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('API响应数据:', data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ API连接测试失败:', error);
|
||||||
|
showMessage('API服务连接失败,请检查服务状态', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回车键登录
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter' && !document.getElementById('loginBtn').disabled) {
|
||||||
|
handleLogin(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
38
public/test-api.html
Normal file
38
public/test-api.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>API Test</title>
|
||||||
|
<script src="https://unpkg.com/axios@1.6.7/dist/axios.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/axios-mock-adapter@1.22.0/dist/axios-mock-adapter.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Brand API Test</h1>
|
||||||
|
<div id="result">Loading...</div>
|
||||||
|
<script>
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup mock
|
||||||
|
const mock = new AxiosMockAdapter(instance, { delayResponse: 300 });
|
||||||
|
|
||||||
|
mock.onGet('/brands').reply(200, {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
list: [{id: '1', name: '测试品牌', code: 'BR001'}],
|
||||||
|
total: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test the API
|
||||||
|
instance.get('/brands')
|
||||||
|
.then(res => {
|
||||||
|
document.getElementById('result').innerHTML = '<pre style="background:#f0f0f0;padding:10px;">✅ Mock Works! Response:\n' + JSON.stringify(res.data, null, 2) + '</pre>';
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
document.getElementById('result').innerHTML = '<pre style="background:#ffe0e0;padding:10px;">❌ Error:\n' + err.message + '</pre>';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
141
public/test-fix.html
Normal file
141
public/test-fix.html
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ERP系统修复测试</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin: 5px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
#result {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>ERP系统修复测试</h1>
|
||||||
|
|
||||||
|
<div class="status info">
|
||||||
|
<strong>测试说明:</strong> 验证Mock API修复是否解决404错误
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>1. 测试服务器状态</h2>
|
||||||
|
<button onclick="testServer()">测试服务器连接</button>
|
||||||
|
|
||||||
|
<h2>2. 测试API端点</h2>
|
||||||
|
<button onclick="testAPI('/api/goods/list')">测试商品列表API</button>
|
||||||
|
<button onclick="testAPI('/api/goods/select')">测试商品选择API</button>
|
||||||
|
<button onclick="testAPI('/api/goods/pushLogs')">测试推送日志API</button>
|
||||||
|
<button onclick="testAPI('/api/orders/list')">测试订单列表API</button>
|
||||||
|
<button onclick="testAPI('/api/express/list')">测试快递公司API</button>
|
||||||
|
|
||||||
|
<h2>3. 测试Vue页面</h2>
|
||||||
|
<button onclick="window.open('/print/batch-fixed', '_blank')">打开终极修复版</button>
|
||||||
|
<button onclick="window.open('/print/batch-simple-fixed', '_blank')">打开简化修复版</button>
|
||||||
|
<button onclick="window.open('/print/test-minimal', '_blank')">打开最小测试版</button>
|
||||||
|
|
||||||
|
<h2>4. 测试结果</h2>
|
||||||
|
<div id="result">点击按钮开始测试...</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function testServer() {
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
result.innerHTML = '正在测试服务器连接...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/');
|
||||||
|
if (response.ok) {
|
||||||
|
result.innerHTML = '✅ 服务器连接正常\n';
|
||||||
|
result.innerHTML += `状态码: ${response.status}\n`;
|
||||||
|
result.innerHTML += `内容类型: ${response.headers.get('content-type')}`;
|
||||||
|
} else {
|
||||||
|
result.innerHTML = `❌ 服务器连接失败: ${response.status} ${response.statusText}`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
result.innerHTML = `❌ 服务器连接错误: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAPI(endpoint) {
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
result.innerHTML = `正在测试 API: ${endpoint}...`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
result.innerHTML = `📊 API测试结果: ${endpoint}\n`;
|
||||||
|
result.innerHTML += `状态码: ${response.status}\n`;
|
||||||
|
result.innerHTML += `响应码: ${data.code}\n`;
|
||||||
|
result.innerHTML += `消息: ${data.message || '无消息'}\n`;
|
||||||
|
|
||||||
|
if (data.code === 200) {
|
||||||
|
result.innerHTML += `✅ API调用成功\n`;
|
||||||
|
result.innerHTML += `数据量: ${data.data?.list?.length || data.data?.length || 0} 条\n`;
|
||||||
|
|
||||||
|
// 显示前3条数据
|
||||||
|
const sampleData = data.data?.list?.slice(0, 3) || data.data?.slice(0, 3);
|
||||||
|
if (sampleData && sampleData.length > 0) {
|
||||||
|
result.innerHTML += '\n示例数据:\n';
|
||||||
|
sampleData.forEach(item => {
|
||||||
|
result.innerHTML += ` • ${item.name || item.orderNo || item.id}\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.innerHTML += `❌ API调用失败: ${data.message}`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
result.innerHTML = `❌ API测试错误: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时自动测试服务器
|
||||||
|
window.onload = testServer;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
299
public/test-login.html
Normal file
299
public/test-login.html
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ERP登录功能测试</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #4CAF50;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.test-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px 5px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #45a049;
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
background: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.result {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #e8f5e9;
|
||||||
|
border: 1px solid #c8e6c9;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background: #ffebee;
|
||||||
|
border-color: #ffcdd2;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background: #e8f5e9;
|
||||||
|
border-color: #c8e6c9;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>ERP登录功能测试</h1>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试1: 正确登录凭证</h3>
|
||||||
|
<p>使用测试账号: admin@erp.com / password123</p>
|
||||||
|
<button onclick="testCorrectLogin()">测试正确登录</button>
|
||||||
|
<div id="result1" class="result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试2: 错误登录凭证</h3>
|
||||||
|
<p>使用错误账号: wrong@email.com / wrongpassword</p>
|
||||||
|
<button onclick="testWrongLogin()">测试错误登录</button>
|
||||||
|
<div id="result2" class="result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试3: 获取用户信息</h3>
|
||||||
|
<p>需要先登录获取token</p>
|
||||||
|
<button onclick="getUserInfo()">获取用户信息</button>
|
||||||
|
<div id="result3" class="result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试4: 完整登录流程</h3>
|
||||||
|
<p>1. 登录 → 2. 保存token → 3. 获取用户信息</p>
|
||||||
|
<button onclick="testFullFlow()">测试完整流程</button>
|
||||||
|
<div id="result4" class="result"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentToken = null;
|
||||||
|
|
||||||
|
function showResult(elementId, message, isError = false) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
element.textContent = message;
|
||||||
|
element.className = 'result ' + (isError ? 'error' : 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading(elementId) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
element.textContent = '测试中...';
|
||||||
|
element.className = 'result loading';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testCorrectLogin() {
|
||||||
|
showLoading('result1');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: 'admin@erp.com',
|
||||||
|
password: 'password123'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.code === 200) {
|
||||||
|
currentToken = data.data.token;
|
||||||
|
showResult('result1', `✅ 登录成功!\n\n响应状态: ${response.status}\nToken: ${data.data.token}\n用户: ${data.data.user.name}\n邮箱: ${data.data.user.email}`);
|
||||||
|
} else {
|
||||||
|
showResult('result1', `❌ 登录失败\n\n状态码: ${response.status}\n错误信息: ${data.message || '未知错误'}`, true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult('result1', `❌ 请求失败: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testWrongLogin() {
|
||||||
|
showLoading('result2');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: 'wrong@email.com',
|
||||||
|
password: 'wrongpassword'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
showResult('result2', `✅ 预期失败,实际失败\n\n状态码: ${response.status}\n错误信息: ${data.message || '认证失败'}`);
|
||||||
|
} else {
|
||||||
|
showResult('result2', `❌ 预期失败但成功了\n\n状态码: ${response.status}\n响应: ${JSON.stringify(data, null, 2)}`, true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult('result2', `❌ 请求失败: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserInfo() {
|
||||||
|
showLoading('result3');
|
||||||
|
if (!currentToken) {
|
||||||
|
showResult('result3', '❌ 请先登录获取token', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${currentToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.code === 200) {
|
||||||
|
showResult('result3', `✅ 获取用户信息成功!\n\n用户信息:\n${JSON.stringify(data.data, null, 2)}`);
|
||||||
|
} else {
|
||||||
|
showResult('result3', `❌ 获取用户信息失败\n\n状态码: ${response.status}\n错误信息: ${data.message || '未知错误'}`, true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult('result3', `❌ 请求失败: ${error.message}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFullFlow() {
|
||||||
|
showLoading('result4');
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 步骤1: 登录
|
||||||
|
results.push('步骤1: 登录');
|
||||||
|
const loginResponse = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: 'admin@erp.com',
|
||||||
|
password: 'password123'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginData = await loginResponse.json();
|
||||||
|
|
||||||
|
if (loginResponse.ok && loginData.code === 200) {
|
||||||
|
currentToken = loginData.data.token;
|
||||||
|
results.push(` ✅ 登录成功`);
|
||||||
|
results.push(` Token: ${currentToken.substring(0, 20)}...`);
|
||||||
|
|
||||||
|
// 步骤2: 获取用户信息
|
||||||
|
results.push('\n步骤2: 获取用户信息');
|
||||||
|
const userResponse = await fetch('/api/user', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${currentToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const userData = await userResponse.json();
|
||||||
|
|
||||||
|
if (userResponse.ok && userData.code === 200) {
|
||||||
|
results.push(` ✅ 获取用户信息成功`);
|
||||||
|
results.push(` 用户: ${userData.data.name}`);
|
||||||
|
results.push(` 邮箱: ${userData.data.email}`);
|
||||||
|
results.push(` 角色: ${userData.data.role}`);
|
||||||
|
|
||||||
|
// 步骤3: 测试退出登录
|
||||||
|
results.push('\n步骤3: 退出登录');
|
||||||
|
const logoutResponse = await fetch('/api/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${currentToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const logoutData = await logoutResponse.json();
|
||||||
|
|
||||||
|
if (logoutResponse.ok && logoutData.code === 200) {
|
||||||
|
results.push(` ✅ 退出登录成功`);
|
||||||
|
currentToken = null;
|
||||||
|
} else {
|
||||||
|
results.push(` ⚠️ 退出登录响应异常: ${logoutData.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
showResult('result4', results.join('\n'));
|
||||||
|
} else {
|
||||||
|
results.push(` ❌ 获取用户信息失败: ${userData.message}`);
|
||||||
|
showResult('result4', results.join('\n'), true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.push(` ❌ 登录失败: ${loginData.message}`);
|
||||||
|
showResult('result4', results.join('\n'), true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results.push(` ❌ 流程执行失败: ${error.message}`);
|
||||||
|
showResult('result4', results.join('\n'), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时检查Mock状态
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
try {
|
||||||
|
// 尝试访问一个Mock接口看看是否正常工作
|
||||||
|
const testResponse = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: 'test', password: 'test' })
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Mock接口测试响应状态:', testResponse.status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Mock接口测试失败:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
120
public/test-mock.html
Normal file
120
public/test-mock.html
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Mock API 测试</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||||
|
.container { max-width: 800px; margin: 0 auto; }
|
||||||
|
.card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
||||||
|
.success { color: green; }
|
||||||
|
.error { color: red; }
|
||||||
|
.loading { color: blue; }
|
||||||
|
button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||||
|
button:hover { background: #0056b3; }
|
||||||
|
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; overflow: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Mock API 测试</h1>
|
||||||
|
<p>测试新配置的Mock API是否正常工作</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试1: 商品列表API</h2>
|
||||||
|
<button onclick="testGoodsList()">测试 /api/goods/list</button>
|
||||||
|
<div id="result1" class="loading">等待测试...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试2: 商品选择API</h2>
|
||||||
|
<button onclick="testGoodsSelect()">测试 /api/goods/select</button>
|
||||||
|
<div id="result2" class="loading">等待测试...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试3: 推送日志API</h2>
|
||||||
|
<button onclick="testPushLogs()">测试 /api/goods/pushLogs</button>
|
||||||
|
<div id="result3" class="loading">等待测试...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试4: 订单列表API</h2>
|
||||||
|
<button onclick="testOrdersList()">测试 /api/orders/list</button>
|
||||||
|
<div id="result4" class="loading">等待测试...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试5: 不存在的API</h2>
|
||||||
|
<button onclick="testNotFound()">测试 /api/not/exist</button>
|
||||||
|
<div id="result5" class="loading">等待测试...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// 使用与项目相同的axios配置
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function testAPI(url, resultId) {
|
||||||
|
const resultEl = document.getElementById(resultId);
|
||||||
|
resultEl.className = 'loading';
|
||||||
|
resultEl.textContent = '请求中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(url);
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
resultEl.className = 'success';
|
||||||
|
resultEl.innerHTML = `
|
||||||
|
✅ 成功! 状态码: ${response.status}<br>
|
||||||
|
<pre>${JSON.stringify(data, null, 2)}</pre>
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
resultEl.className = 'error';
|
||||||
|
if (error.response) {
|
||||||
|
resultEl.innerHTML = `
|
||||||
|
❌ 失败! 状态码: ${error.response.status}<br>
|
||||||
|
<pre>${JSON.stringify(error.response.data, null, 2)}</pre>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
resultEl.textContent = `❌ 请求失败: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGoodsList() {
|
||||||
|
testAPI('/api/goods/list', 'result1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGoodsSelect() {
|
||||||
|
testAPI('/api/goods/select', 'result2');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPushLogs() {
|
||||||
|
testAPI('/api/goods/pushLogs', 'result3');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testOrdersList() {
|
||||||
|
testAPI('/api/orders/list', 'result4');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testNotFound() {
|
||||||
|
testAPI('/api/not/exist', 'result5');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时自动测试一个API
|
||||||
|
window.onload = () => {
|
||||||
|
testGoodsList();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
126
public/test-server.html
Normal file
126
public/test-server.html
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>服务器测试页面</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.success { color: green; }
|
||||||
|
.error { color: red; }
|
||||||
|
.test-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin: 10px;
|
||||||
|
background: #409eff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🔄 服务器状态测试</h1>
|
||||||
|
|
||||||
|
<div id="status">
|
||||||
|
<p>测试服务器是否正常工作...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 30px;">
|
||||||
|
<button class="test-btn" onclick="testServer()">测试服务器</button>
|
||||||
|
<button class="test-btn" onclick="testVuePages()">测试Vue页面</button>
|
||||||
|
<button class="test-btn" onclick="checkConsole()">检查控制台</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="results" style="margin-top: 30px; text-align: left; max-width: 600px; margin-left: auto; margin-right: auto;">
|
||||||
|
<!-- 测试结果将显示在这里 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 50px; color: #666;">
|
||||||
|
<h3>问题诊断指南:</h3>
|
||||||
|
<ol style="text-align: left; max-width: 600px; margin: 20px auto;">
|
||||||
|
<li>如果此页面能正常显示 → 服务器正常</li>
|
||||||
|
<li>如果Vue页面空白但此页面正常 → Vue编译问题</li>
|
||||||
|
<li>如果所有页面都空白 → 服务器或网络问题</li>
|
||||||
|
<li>按F12打开控制台查看具体错误</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function log(message, type = 'info') {
|
||||||
|
const results = document.getElementById('results')
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.innerHTML = `<strong>${type === 'error' ? '❌' : '✅'} ${new Date().toLocaleTimeString()}:</strong> ${message}`
|
||||||
|
div.style.color = type === 'error' ? 'red' : 'green'
|
||||||
|
div.style.margin = '5px 0'
|
||||||
|
results.appendChild(div)
|
||||||
|
}
|
||||||
|
|
||||||
|
function testServer() {
|
||||||
|
log('开始测试服务器...')
|
||||||
|
|
||||||
|
// 测试当前页面
|
||||||
|
log('当前页面加载正常')
|
||||||
|
|
||||||
|
// 测试是否能访问其他资源
|
||||||
|
fetch('/')
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
log('服务器根目录可访问')
|
||||||
|
} else {
|
||||||
|
log(`服务器响应状态: ${response.status}`, 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log(`服务器访问失败: ${error.message}`, 'error')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function testVuePages() {
|
||||||
|
log('测试Vue页面...')
|
||||||
|
|
||||||
|
const vuePages = [
|
||||||
|
'/print/test-minimal',
|
||||||
|
'/print/batch-simple-fixed',
|
||||||
|
'/print/batch-fixed'
|
||||||
|
]
|
||||||
|
|
||||||
|
vuePages.forEach(page => {
|
||||||
|
fetch(page)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
log(`Vue页面 ${page} 可访问`)
|
||||||
|
} else {
|
||||||
|
log(`Vue页面 ${page} 访问失败: ${response.status}`, 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log(`Vue页面 ${page} 访问错误: ${error.message}`, 'error')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkConsole() {
|
||||||
|
log('请按F12打开浏览器控制台')
|
||||||
|
log('检查是否有以下错误:')
|
||||||
|
log('1. Vue编译错误')
|
||||||
|
log('2. JavaScript语法错误')
|
||||||
|
log('3. 网络请求失败')
|
||||||
|
log('4. 资源加载失败')
|
||||||
|
|
||||||
|
// 尝试触发一个控制台消息
|
||||||
|
console.log('✅ 测试控制台输出 - 如果你能看到这条消息,控制台工作正常')
|
||||||
|
console.error('❌ 测试错误输出 - 这是预期的测试错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时自动测试
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
log('页面加载完成,服务器基本正常')
|
||||||
|
document.getElementById('status').innerHTML = '<p class="success">✅ 服务器基本工作正常</p>'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
134
public/test-simple.html
Normal file
134
public/test-simple.html
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ERP系统测试页面</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #409eff;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background: #f0f9eb;
|
||||||
|
color: #67c23a;
|
||||||
|
border: 1px solid #e1f3d8;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background: #fef0f0;
|
||||||
|
color: #f56c6c;
|
||||||
|
border: 1px solid #fde2e2;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
background: #f4f4f5;
|
||||||
|
color: #909399;
|
||||||
|
border: 1px solid #e9e9eb;
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.link {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 10px 15px 10px 0;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #409eff;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.link:hover {
|
||||||
|
background: #66b1ff;
|
||||||
|
}
|
||||||
|
.link.secondary {
|
||||||
|
background: #909399;
|
||||||
|
}
|
||||||
|
.link.secondary:hover {
|
||||||
|
background: #a6a9ad;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🏗️ ERP系统测试页面</h1>
|
||||||
|
|
||||||
|
<div class="status success">
|
||||||
|
✅ <strong>Vite开发服务器正常运行</strong><br>
|
||||||
|
服务器地址: <code>http://localhost:5155</code><br>
|
||||||
|
当前时间: <span id="current-time"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>📊 系统状态</h2>
|
||||||
|
<div class="status info">
|
||||||
|
🔍 <strong>检测到的问题:</strong><br>
|
||||||
|
1. 主应用页面可能因路由或组件错误导致空白<br>
|
||||||
|
2. 需要检查浏览器控制台错误信息<br>
|
||||||
|
3. 可能需要修复Vue组件编译问题
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>🔧 测试链接</h2>
|
||||||
|
<div class="links">
|
||||||
|
<a href="/" class="link" target="_blank">🏠 测试主应用</a>
|
||||||
|
<a href="/print/log" class="link" target="_blank">📋 测试打印日志</a>
|
||||||
|
<a href="/print/batch-detail?id=test123" class="link" target="_blank">📄 测试批次详情</a>
|
||||||
|
<a href="/test-vue-app.html" class="link secondary" target="_blank">🧪 Vue应用测试</a>
|
||||||
|
<a href="/test-mock.html" class="link secondary" target="_blank">🔄 Mock API测试</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>📝 调试步骤</h2>
|
||||||
|
<ol>
|
||||||
|
<li>按 <code>F12</code> 打开浏览器开发者工具</li>
|
||||||
|
<li>查看 <strong>Console</strong> 选项卡中的错误信息</li>
|
||||||
|
<li>查看 <strong>Network</strong> 选项卡中的请求状态</li>
|
||||||
|
<li>查看 <strong>Elements</strong> 选项卡中的HTML结构</li>
|
||||||
|
<li>将错误信息截图或复制发送给我</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>🚨 紧急修复方案</h2>
|
||||||
|
<p>如果主应用仍然空白,可以:</p>
|
||||||
|
<ul>
|
||||||
|
<li>使用纯HTML测试页面绕过Vue编译问题</li>
|
||||||
|
<li>检查Vite编译日志中的具体错误</li>
|
||||||
|
<li>回退到之前的稳定版本</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 显示当前时间
|
||||||
|
document.getElementById('current-time').textContent = new Date().toLocaleString('zh-CN');
|
||||||
|
|
||||||
|
// 测试链接点击事件
|
||||||
|
document.querySelectorAll('.link').forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
console.log('测试链接点击:', this.href);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
140
public/test-vue-app.html
Normal file
140
public/test-vue-app.html
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vue应用测试</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||||
|
.container { max-width: 800px; margin: 0 auto; }
|
||||||
|
.card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
||||||
|
.success { color: green; }
|
||||||
|
.error { color: red; }
|
||||||
|
.loading { color: blue; }
|
||||||
|
button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||||
|
button:hover { background: #0056b3; }
|
||||||
|
iframe { width: 100%; height: 500px; border: 1px solid #ddd; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Vue应用功能测试</h1>
|
||||||
|
<p>测试Vue应用各页面是否正常工作</p>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试1: 主应用</h2>
|
||||||
|
<button onclick="testMainApp()">测试 http://localhost:5155/</button>
|
||||||
|
<div id="result1" class="loading">等待测试...</div>
|
||||||
|
<iframe id="frame1" style="display:none;"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试2: 打印页面(原版)</h2>
|
||||||
|
<button onclick="testPrintPage()">测试 http://localhost:5155/print/batch</button>
|
||||||
|
<div id="result2" class="loading">等待测试...</div>
|
||||||
|
<iframe id="frame2" style="display:none;"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试3: 打印页面(修复版)</h2>
|
||||||
|
<button onclick="testPrintFixed()">测试 http://localhost:5155/print/batch-fixed</button>
|
||||||
|
<div id="result2" class="loading">等待测试...</div>
|
||||||
|
<iframe id="frame3" style="display:none;"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试4: 商品管理页面</h2>
|
||||||
|
<button onclick="testGoodsPage()">测试 http://localhost:5155/goods/list</button>
|
||||||
|
<div id="result3" class="loading">等待测试...</div>
|
||||||
|
<iframe id="frame4" style="display:none;"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>测试5: 订单管理页面</h2>
|
||||||
|
<button onclick="testOrdersPage()">测试 http://localhost:5155/orders/list</button>
|
||||||
|
<div id="result4" class="loading">等待测试...</div>
|
||||||
|
<iframe id="frame5" style="display:none;"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>手动测试指南</h2>
|
||||||
|
<ol>
|
||||||
|
<li>打开浏览器控制台(F12)</li>
|
||||||
|
<li>访问 <a href="http://localhost:5155/" target="_blank">http://localhost:5155/</a></li>
|
||||||
|
<li>检查控制台是否有错误</li>
|
||||||
|
<li>导航到各个页面测试功能</li>
|
||||||
|
<li>重点关注打印页面是否卡顿</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function testPage(url, resultId, frameId) {
|
||||||
|
const resultEl = document.getElementById(resultId);
|
||||||
|
const frameEl = document.getElementById(frameId);
|
||||||
|
|
||||||
|
resultEl.className = 'loading';
|
||||||
|
resultEl.textContent = '加载中...';
|
||||||
|
frameEl.style.display = 'block';
|
||||||
|
frameEl.src = url;
|
||||||
|
|
||||||
|
// 监听iframe加载
|
||||||
|
frameEl.onload = function() {
|
||||||
|
try {
|
||||||
|
// 尝试访问iframe内容
|
||||||
|
const iframeDoc = frameEl.contentDocument || frameEl.contentWindow.document;
|
||||||
|
|
||||||
|
if (iframeDoc.readyState === 'complete') {
|
||||||
|
// 检查是否有Vue应用
|
||||||
|
const vueApp = iframeDoc.getElementById('app');
|
||||||
|
const hasContent = iframeDoc.body && iframeDoc.body.innerHTML.length > 100;
|
||||||
|
|
||||||
|
if (vueApp && hasContent) {
|
||||||
|
resultEl.className = 'success';
|
||||||
|
resultEl.textContent = '✅ 页面加载成功!Vue应用正常。';
|
||||||
|
} else {
|
||||||
|
resultEl.className = 'error';
|
||||||
|
resultEl.textContent = '⚠️ 页面加载但可能缺少内容。';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 跨域限制,但至少iframe加载了
|
||||||
|
resultEl.className = 'success';
|
||||||
|
resultEl.textContent = '✅ 页面加载成功(跨域限制无法检查内容)。';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
frameEl.onerror = function() {
|
||||||
|
resultEl.className = 'error';
|
||||||
|
resultEl.textContent = '❌ 页面加载失败!';
|
||||||
|
frameEl.style.display = 'none';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function testMainApp() {
|
||||||
|
testPage('http://localhost:5155/', 'result1', 'frame1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPrintPage() {
|
||||||
|
testPage('http://localhost:5155/print/batch', 'result2', 'frame2');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPrintFixed() {
|
||||||
|
testPage('http://localhost:5155/print/batch-fixed', 'result2', 'frame3');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGoodsPage() {
|
||||||
|
testPage('http://localhost:5155/goods/list', 'result3', 'frame4');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testOrdersPage() {
|
||||||
|
testPage('http://localhost:5155/orders/list', 'result4', 'frame5');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动测试主应用
|
||||||
|
window.onload = () => {
|
||||||
|
setTimeout(testMainApp, 1000);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
3
src/App.vue
Normal file
3
src/App.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
58
src/api/afterSale.ts
Normal file
58
src/api/afterSale.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// 售后管理 API
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 售后列表
|
||||||
|
*/
|
||||||
|
export function getAfterSalesList(params?: any) {
|
||||||
|
return request.get('/after-sales', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 售后单详情
|
||||||
|
*/
|
||||||
|
export function getAfterSaleDetail(id: string) {
|
||||||
|
return request.get(`/after-sales/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建售后单
|
||||||
|
*/
|
||||||
|
export function createAfterSale(data: any) {
|
||||||
|
return request.post('/after-sales', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新售后单状态
|
||||||
|
*/
|
||||||
|
export function updateAfterSaleStatus(id: string, data: any) {
|
||||||
|
return request.put(`/after-sales/${id}/status`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除售后单
|
||||||
|
*/
|
||||||
|
export function deleteAfterSale(id: string) {
|
||||||
|
return request.delete(`/after-sales/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可售后的订单(已发货)
|
||||||
|
*/
|
||||||
|
export function getAvailableOrders(params?: any) {
|
||||||
|
return request.get('/after-sales/available-orders', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有可售后的订单(包含未发货的仅退款)
|
||||||
|
*/
|
||||||
|
export function getAllAvailableOrders(params?: any) {
|
||||||
|
return request.get('/after-sales/all-available-orders', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 售后统计
|
||||||
|
*/
|
||||||
|
export function getAfterSaleStats() {
|
||||||
|
return request.get('/after-sales/stats')
|
||||||
|
}
|
||||||
129
src/api/ai.ts
Normal file
129
src/api/ai.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// AI 对话管理 API
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息角色类型
|
||||||
|
*/
|
||||||
|
export type MessageRole = 'user' | 'assistant' | 'system'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息接口定义
|
||||||
|
*/
|
||||||
|
export interface Message {
|
||||||
|
id: string
|
||||||
|
role: MessageRole
|
||||||
|
content: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对话接口定义
|
||||||
|
*/
|
||||||
|
export interface Conversation {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
messages: Message[]
|
||||||
|
model?: string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 模型接口定义
|
||||||
|
*/
|
||||||
|
export interface AIModel {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
provider: string
|
||||||
|
description?: string
|
||||||
|
isActive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 能力接口定义
|
||||||
|
*/
|
||||||
|
export interface AICapability {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天请求参数
|
||||||
|
*/
|
||||||
|
export interface ChatParams {
|
||||||
|
conversationId?: string
|
||||||
|
message: string
|
||||||
|
model?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行任务请求参数
|
||||||
|
*/
|
||||||
|
export interface ExecuteTaskParams {
|
||||||
|
task: string
|
||||||
|
context?: Record<string, any>
|
||||||
|
options?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话列表
|
||||||
|
*/
|
||||||
|
export function getConversations() {
|
||||||
|
return request.get('/ai/conversations')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话详情
|
||||||
|
* @param id 对话ID
|
||||||
|
*/
|
||||||
|
export function getConversationDetail(id: string) {
|
||||||
|
return request.get(`/ai/conversations/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除对话
|
||||||
|
* @param id 对话ID
|
||||||
|
*/
|
||||||
|
export function deleteConversation(id: string) {
|
||||||
|
return request.delete(`/ai/conversations/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送聊天消息
|
||||||
|
* @param params 聊天参数
|
||||||
|
*/
|
||||||
|
export function sendChatMessage(params: ChatParams) {
|
||||||
|
return request.post('/ai/chat', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 AI 任务
|
||||||
|
* @param params 任务参数
|
||||||
|
*/
|
||||||
|
export function executeTask(params: ExecuteTaskParams) {
|
||||||
|
return request.post('/ai/execute-task', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 AI 能力列表
|
||||||
|
*/
|
||||||
|
export function getCapabilities() {
|
||||||
|
return request.get('/ai/capabilities')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型列表
|
||||||
|
*/
|
||||||
|
export function getModels() {
|
||||||
|
return request.get('/ai/models')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换模型
|
||||||
|
* @param data 包含模型 ID 的数据
|
||||||
|
*/
|
||||||
|
export function switchModel(data: { modelId: string }) {
|
||||||
|
return request.post('/ai/models/switch', data)
|
||||||
|
}
|
||||||
141
src/api/auth.ts
Normal file
141
src/api/auth.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export interface LoginForm {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
captcha?: string
|
||||||
|
device_id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmsLoginForm {
|
||||||
|
phone: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterForm {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
password_confirmation: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
phone?: string
|
||||||
|
avatar?: string
|
||||||
|
status: string
|
||||||
|
is_test_user: boolean
|
||||||
|
created_at: string
|
||||||
|
last_login_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
code: number
|
||||||
|
data: {
|
||||||
|
user: UserInfo
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账号密码登录
|
||||||
|
export function login(data: LoginForm) {
|
||||||
|
return request.post<LoginResponse>('/api/auth/login', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短信发送验证码
|
||||||
|
export function sendSmsCode(phone: string, captcha?: string, device_id?: string) {
|
||||||
|
return request.post('/api/auth/sms/send', { phone, captcha, device_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短信登录
|
||||||
|
export function smsLogin(data: SmsLoginForm) {
|
||||||
|
return request.post<LoginResponse>('/api/auth/sms/login', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信登录二维码
|
||||||
|
export function getWechatQr() {
|
||||||
|
return request.get<{ data: { scene_str: string; qr_url: string; ticket: string } }>('/api/auth/wechat/qr')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信扫码状态
|
||||||
|
export function checkWechatScan(scene_str: string) {
|
||||||
|
return request.get('/api/auth/wechat/check', { scene_str })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 微信模拟扫码(开发测试用)
|
||||||
|
export function mockWechatScan(scene_str: string, openid?: string) {
|
||||||
|
return request.post('/api/auth/wechat/mock-scan', { scene_str, openid })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户注册
|
||||||
|
export function register(data: RegisterForm) {
|
||||||
|
return request.post<LoginResponse>('/api/auth/register', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图形验证码
|
||||||
|
export function getCaptcha(device_id?: string) {
|
||||||
|
return request.get('/api/auth/captcha', { device_id }, { responseType: 'blob' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新Token
|
||||||
|
export function refreshToken() {
|
||||||
|
return request.post<{ data: { token: string; user: UserInfo } }>('/api/auth/refresh')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
export function logout() {
|
||||||
|
return request.post('/api/auth/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户
|
||||||
|
export function getCurrentUser() {
|
||||||
|
return request.get<{ data: UserInfo }>('/api/user/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新个人资料
|
||||||
|
export function updateProfile(data: { name?: string; phone?: string; avatar?: string }) {
|
||||||
|
return request.put<{ data: UserInfo }>('/api/user/profile', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
export function changePassword(data: { current_password: string; new_password: string; new_password_confirmation: string }) {
|
||||||
|
return request.put('/api/user/password', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定微信
|
||||||
|
export function bindWechat(code: string) {
|
||||||
|
return request.post('/api/user/bind-wechat', { code })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解绑微信
|
||||||
|
export function unbindWechat() {
|
||||||
|
return request.post('/api/user/unbind-wechat')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送重置密码验证码
|
||||||
|
export function sendResetCode(email: string) {
|
||||||
|
return request.post('/api/auth/reset/send-code', { email })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
export function resetPassword(data: { email: string; code: string; new_password: string }) {
|
||||||
|
return request.post('/api/auth/reset/password', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取登录日志
|
||||||
|
export function getLoginLogs() {
|
||||||
|
return request.get('/api/user/login-logs')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送配对码
|
||||||
|
export function sendPairCode(email: string, expireMinutes?: number) {
|
||||||
|
return request.post('/api/auth/send-pair-code', { email, expireMinutes })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证配对码
|
||||||
|
export function verifyPairCode(email: string, code: string) {
|
||||||
|
return request.post<LoginResponse>('/api/auth/verify-pair-code', { email, code })
|
||||||
|
}
|
||||||
65
src/api/brand.ts
Normal file
65
src/api/brand.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// src/api/brand.ts
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 品牌数据类型
|
||||||
|
*/
|
||||||
|
export interface Brand {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
code?: string
|
||||||
|
logo?: string
|
||||||
|
status?: number
|
||||||
|
description?: string
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取品牌列表
|
||||||
|
* @param params 查询参数(page, limit, name, status)
|
||||||
|
*/
|
||||||
|
export function getBrandList(params?: any) {
|
||||||
|
return request.get('/brands', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取品牌详情
|
||||||
|
* @param id 品牌ID
|
||||||
|
*/
|
||||||
|
export function getBrandDetail(id: number) {
|
||||||
|
return request.get(`/brands/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建品牌
|
||||||
|
* @param data 品牌数据
|
||||||
|
*/
|
||||||
|
export function createBrand(data: Brand) {
|
||||||
|
return request.post('/brands', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新品牌
|
||||||
|
* @param id 品牌ID
|
||||||
|
* @param data 更新数据
|
||||||
|
*/
|
||||||
|
export function updateBrand(id: number, data: Partial<Brand>) {
|
||||||
|
return request.put(`/brands/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除品牌
|
||||||
|
* @param id 品牌ID
|
||||||
|
*/
|
||||||
|
export function deleteBrand(id: number) {
|
||||||
|
return request.delete(`/brands/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有品牌用于下拉选择
|
||||||
|
*/
|
||||||
|
export function getAllBrands() {
|
||||||
|
return request.get('/brands/all')
|
||||||
|
}
|
||||||
151
src/api/delivery.ts
Normal file
151
src/api/delivery.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/** 配送单状态 */
|
||||||
|
export type DeliveryOrderStatus = 'pending' | 'printed' | 'shipped' | 'sync_failed' | 'completed'
|
||||||
|
|
||||||
|
/** 配送单商品项 */
|
||||||
|
export interface DeliveryOrderItem {
|
||||||
|
id?: string
|
||||||
|
skuCode: string
|
||||||
|
skuName?: string
|
||||||
|
quantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 配送单 */
|
||||||
|
export interface DeliveryOrder {
|
||||||
|
id?: string
|
||||||
|
orderNo?: string
|
||||||
|
warehouseId: string
|
||||||
|
warehouseName?: string
|
||||||
|
platformOrderId?: string
|
||||||
|
platformOrderNo?: string
|
||||||
|
status: DeliveryOrderStatus
|
||||||
|
totalAmount?: number
|
||||||
|
expressCompany?: string
|
||||||
|
trackingNo?: string
|
||||||
|
items: DeliveryOrderItem[]
|
||||||
|
createTime?: string
|
||||||
|
updateTime?: string
|
||||||
|
printTime?: string
|
||||||
|
shipTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 待发货列表查询参数 */
|
||||||
|
export interface PendingDeliveryParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
warehouseId?: string
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记打印请求 */
|
||||||
|
export interface MarkPrintedDto {
|
||||||
|
orderIds: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重新打印请求 */
|
||||||
|
export interface ReprintDto {
|
||||||
|
orderIds: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发货请求 */
|
||||||
|
export interface ShipDto {
|
||||||
|
orderId: string
|
||||||
|
expressCompany: string
|
||||||
|
trackingNo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打印日志 */
|
||||||
|
export interface PrintLog {
|
||||||
|
id?: string
|
||||||
|
orderId: string
|
||||||
|
orderNo?: string
|
||||||
|
printType: 'original' | 'reprint'
|
||||||
|
printedAt: string
|
||||||
|
printedBy?: string
|
||||||
|
printer?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打印日志查询参数 */
|
||||||
|
export interface PrintLogParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
orderNo?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== API 函数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待发货列表
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export function getPendingDeliveryList(params?: PendingDeliveryParams) {
|
||||||
|
return request.get<PendingDeliveryResponse>('/delivery/pending-delivery', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记已打印
|
||||||
|
* @param data 订单ID列表
|
||||||
|
*/
|
||||||
|
export function markPrinted(data: MarkPrintedDto) {
|
||||||
|
return request.post<void>('/delivery/mark-printed', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新打印
|
||||||
|
* @param data 订单ID列表
|
||||||
|
*/
|
||||||
|
export function reprintDelivery(data: ReprintDto) {
|
||||||
|
return request.post<void>('/delivery/reprint', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取打印日志
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export function getPrintLogs(params?: PrintLogParams) {
|
||||||
|
return request.get<PrintLogResponse>('/delivery/print-logs', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发货
|
||||||
|
* @param data 发货信息
|
||||||
|
*/
|
||||||
|
export function shipDelivery(data: ShipDto) {
|
||||||
|
return request.post<void>('/delivery/ship', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步失败重试(批量)
|
||||||
|
*/
|
||||||
|
export function syncFailedRetry() {
|
||||||
|
return request.post<void>('/delivery/sync-failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试同步单个订单
|
||||||
|
* @param id 配送单ID
|
||||||
|
*/
|
||||||
|
export function retrySyncDelivery(id: string) {
|
||||||
|
return request.post<void>(`/delivery/retry-sync/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 响应类型 ====================
|
||||||
|
|
||||||
|
export interface PendingDeliveryResponse {
|
||||||
|
list: DeliveryOrder[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrintLogResponse {
|
||||||
|
list: PrintLog[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
227
src/api/goods.ts
Normal file
227
src/api/goods.ts
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
// src/api/goods.ts
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// ============ 类型定义 ============
|
||||||
|
|
||||||
|
/** 商品类型 */
|
||||||
|
export type GoodsType = 'normal' | 'combo'
|
||||||
|
|
||||||
|
/** 商品状态 */
|
||||||
|
export type GoodsStatus = 'active' | 'inactive'
|
||||||
|
|
||||||
|
/** 商品 */
|
||||||
|
export interface Goods {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
barcode?: string
|
||||||
|
type: GoodsType
|
||||||
|
type_label?: string
|
||||||
|
retail_price: string
|
||||||
|
cost_price: string
|
||||||
|
unit?: string
|
||||||
|
brand?: { id: number; name: string } | null
|
||||||
|
suppliers?: string
|
||||||
|
weight?: string
|
||||||
|
packaging_cost?: string
|
||||||
|
shipping_packaging_cost?: string
|
||||||
|
volume?: string
|
||||||
|
length?: string
|
||||||
|
width?: string
|
||||||
|
height?: string
|
||||||
|
batch_management: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 商品列表查询参数 */
|
||||||
|
export interface GoodsListParams {
|
||||||
|
keyword?: string
|
||||||
|
category?: string
|
||||||
|
type?: GoodsType
|
||||||
|
status?: GoodsStatus
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 商品列表响应 */
|
||||||
|
export interface GoodsListResponse {
|
||||||
|
list: Goods[]
|
||||||
|
total: number
|
||||||
|
current_page: number
|
||||||
|
last_page: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建/更新商品请求 */
|
||||||
|
export interface GoodsFormData {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
type: GoodsType
|
||||||
|
barcode?: string
|
||||||
|
retail_price?: number | string
|
||||||
|
cost_price?: number | string
|
||||||
|
unit?: string
|
||||||
|
brand_id?: number
|
||||||
|
weight?: number | string
|
||||||
|
packaging_cost?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SKU绑定请求 */
|
||||||
|
export interface BindSkuData {
|
||||||
|
goodsId: string
|
||||||
|
skuCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量绑定请求 */
|
||||||
|
export interface BatchBindData {
|
||||||
|
goodsId: string
|
||||||
|
skuCodes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 自动匹配请求 */
|
||||||
|
export interface AutoMatchData {
|
||||||
|
warehouseId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 匹配结果查询参数 */
|
||||||
|
export interface MatchResultsParams {
|
||||||
|
warehouseId?: string
|
||||||
|
status?: string
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 匹配结果项 */
|
||||||
|
export interface MatchResult {
|
||||||
|
id: string
|
||||||
|
goodsId: string
|
||||||
|
goodsName: string
|
||||||
|
skuCode: string
|
||||||
|
warehouseId: string
|
||||||
|
warehouseName: string
|
||||||
|
matchStatus: 'matched' | 'unmatched' | 'pending'
|
||||||
|
matchType?: 'auto' | 'manual'
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ API 函数 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商品列表(分页)
|
||||||
|
*/
|
||||||
|
export function getGoodsList(params?: GoodsListParams) {
|
||||||
|
return request.get<GoodsListResponse>('/goods', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商品详情
|
||||||
|
*/
|
||||||
|
export function getGoodsDetail(id: string | number) {
|
||||||
|
return request.get<Goods>(`/goods/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建商品
|
||||||
|
*/
|
||||||
|
export function createGoods(data: GoodsFormData) {
|
||||||
|
return request.post<Goods>('/goods', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新商品
|
||||||
|
*/
|
||||||
|
export function updateGoods(id: string | number, data: Partial<GoodsFormData>) {
|
||||||
|
return request.put<Goods>(`/goods/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除商品
|
||||||
|
*/
|
||||||
|
export function deleteGoods(id: string | number) {
|
||||||
|
return request.delete<void>(`/goods/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全量同步商品
|
||||||
|
* 注意: 后端暂无此接口
|
||||||
|
*/
|
||||||
|
export function syncAllGoods() {
|
||||||
|
return Promise.resolve({
|
||||||
|
code: 200,
|
||||||
|
message: '同步功能开发中',
|
||||||
|
data: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定SKU
|
||||||
|
*/
|
||||||
|
export function bindSku(data: BindSkuData) {
|
||||||
|
return request.post<void>('/goods/bind-sku', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解除SKU绑定
|
||||||
|
*/
|
||||||
|
export function unbindSku(data: BindSkuData) {
|
||||||
|
return request.post<void>('/goods/unbind-sku', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量绑定SKU
|
||||||
|
*/
|
||||||
|
export function batchBindSku(data: BatchBindData) {
|
||||||
|
return request.post<void>('/goods/batch-bind', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动匹配
|
||||||
|
*/
|
||||||
|
export function autoMatchGoods(data: AutoMatchData) {
|
||||||
|
return request.post<void>('/goods/auto-match', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取匹配结果列表
|
||||||
|
*/
|
||||||
|
export function getMatchResults(params?: MatchResultsParams) {
|
||||||
|
return request.get<{ list: MatchResult[]; total: number }>('/goods/match-results', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送商品到云仓
|
||||||
|
* 注意: 后端暂无此接口
|
||||||
|
*/
|
||||||
|
export function pushGoodsToCloud(data: { goodsIds: number[]; warehouseId: number }) {
|
||||||
|
return Promise.resolve({
|
||||||
|
code: 200,
|
||||||
|
message: '推送功能开发中',
|
||||||
|
data: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推送日志
|
||||||
|
* 注意: 后端暂无此接口
|
||||||
|
*/
|
||||||
|
export function getPushLogs(params?: { page?: number; pageSize?: number }) {
|
||||||
|
return Promise.resolve({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
list: [],
|
||||||
|
total: 0,
|
||||||
|
page: params?.page || 1,
|
||||||
|
pageSize: params?.pageSize || 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有商品(下拉选择用)
|
||||||
|
*/
|
||||||
|
export function getAllGoods() {
|
||||||
|
return request.get('/goods/all')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 别名兼容
|
||||||
|
export const getAllGoodsForSelect = getAllGoods
|
||||||
65
src/api/log.ts
Normal file
65
src/api/log.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// 操作日志 API
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志项接口定义
|
||||||
|
*/
|
||||||
|
export interface OperationLogItem {
|
||||||
|
id: number
|
||||||
|
operateTime: string
|
||||||
|
userName: string
|
||||||
|
operateType: string
|
||||||
|
operateDesc: string
|
||||||
|
result: number
|
||||||
|
ipAddress: string
|
||||||
|
userAgent: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志列表查询参数
|
||||||
|
*/
|
||||||
|
export interface OperationLogParams {
|
||||||
|
currentPage?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
operateType?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取操作日志列表
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export function getOperationLogs(params?: OperationLogParams) {
|
||||||
|
return request.get('/operation-logs', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取操作日志详情
|
||||||
|
* @param id 日志ID
|
||||||
|
*/
|
||||||
|
export function getOperationLogDetail(id: number) {
|
||||||
|
return request.get(`/operation-logs/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出操作日志
|
||||||
|
* @param params 导出参数
|
||||||
|
*/
|
||||||
|
export function exportLogs(params?: OperationLogParams) {
|
||||||
|
return request.get('/operation-logs/export', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加操作日志
|
||||||
|
*/
|
||||||
|
export function addOperationLog(data: {
|
||||||
|
operateType: string
|
||||||
|
operateDesc: string
|
||||||
|
result: number
|
||||||
|
userId?: number
|
||||||
|
userName?: string
|
||||||
|
}) {
|
||||||
|
return request.post('/operation-logs', data)
|
||||||
|
}
|
||||||
412
src/api/order.ts
Normal file
412
src/api/order.ts
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
/**
|
||||||
|
* 订单模块 API
|
||||||
|
* 对接后端 OrderController_new 订单模块所有接口
|
||||||
|
*/
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/** 订单商品项 */
|
||||||
|
export interface OrderItem {
|
||||||
|
id: number
|
||||||
|
goods_id?: number
|
||||||
|
goods_name: string
|
||||||
|
goods_code?: string
|
||||||
|
platform_sku: string
|
||||||
|
sku_code?: string
|
||||||
|
quantity: number
|
||||||
|
price: number
|
||||||
|
total_amount: number
|
||||||
|
pic?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单状态枚举 */
|
||||||
|
export type OrderStatus = 'pending' | 'auditing' | 'shipped' | 'completed' | 'cancelled'
|
||||||
|
|
||||||
|
/** 审核状态枚举 */
|
||||||
|
export type AuditStatus = 'pending' | 'approved' | 'rejected'
|
||||||
|
|
||||||
|
/** 发货状态枚举 */
|
||||||
|
export type DeliveryStatus = 'pending' | 'delivered'
|
||||||
|
|
||||||
|
/** 订单主体 */
|
||||||
|
export interface Order {
|
||||||
|
id: number
|
||||||
|
short_id: string
|
||||||
|
platform_order_sn: string
|
||||||
|
platform: string
|
||||||
|
platform_label?: string
|
||||||
|
shop_id: number
|
||||||
|
shop_name: string
|
||||||
|
order_time: string
|
||||||
|
buyer_nick: string
|
||||||
|
receiver_name: string
|
||||||
|
receiver_phone: string
|
||||||
|
receiver_address: string
|
||||||
|
goods_amount: number
|
||||||
|
discount_amount: number
|
||||||
|
freight: number
|
||||||
|
total_amount: number
|
||||||
|
order_status: OrderStatus
|
||||||
|
platform_status: string
|
||||||
|
audit_status: AuditStatus
|
||||||
|
delivery_status: DeliveryStatus
|
||||||
|
express_company?: string
|
||||||
|
express_no?: string
|
||||||
|
warehouse_id?: number
|
||||||
|
warehouse_name?: string
|
||||||
|
remark?: string
|
||||||
|
items: OrderItem[]
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单列表查询参数 */
|
||||||
|
export interface OrderListQuery {
|
||||||
|
platform?: string // 平台(如 taobao)
|
||||||
|
shop_id?: number // 店铺ID
|
||||||
|
order_status?: OrderStatus // 订单状态
|
||||||
|
audit_status?: AuditStatus // 审核状态
|
||||||
|
delivery_status?: DeliveryStatus // 发货状态
|
||||||
|
keyword?: string // 搜索(订单号/收货人/电话)
|
||||||
|
date_range?: string // 开始日期,结束日期
|
||||||
|
page?: number // 页码,默认1
|
||||||
|
limit?: number // 每页数量,默认10
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单列表响应 */
|
||||||
|
export interface OrderListResponse {
|
||||||
|
list: Order[]
|
||||||
|
total: number
|
||||||
|
current_page: number
|
||||||
|
last_page: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉取订单参数 */
|
||||||
|
export interface PullOrdersParams {
|
||||||
|
platform: string // 平台
|
||||||
|
shop_id: number // 店铺ID
|
||||||
|
pull_type: 'all' | 'increment' | 'specify' // 拉取类型
|
||||||
|
order_ids?: string // specify时必填,逗号分隔
|
||||||
|
start_time?: string // 开始时间 YYYY-MM-DD
|
||||||
|
end_time?: string // 结束时间 YYYY-MM-DD
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拉取订单响应 */
|
||||||
|
export interface PullOrdersResponse {
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核订单参数 */
|
||||||
|
export interface AuditOrderParams {
|
||||||
|
action: 'approve' | 'reject' // 审核动作
|
||||||
|
comment?: string // 驳回时必填
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量审核参数 */
|
||||||
|
export interface BatchAuditParams {
|
||||||
|
order_ids: number[] // 订单ID数组
|
||||||
|
action: 'approve' | 'reject'
|
||||||
|
comment?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量审核响应 */
|
||||||
|
export interface BatchAuditResponse {
|
||||||
|
success_count: number
|
||||||
|
failed_orders: Array<{ id: number; reason: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量操作参数 */
|
||||||
|
export interface BatchOperationParams {
|
||||||
|
order_ids: number[] // 订单ID数组
|
||||||
|
operation: 'audit_approve' | 'audit_reject' | 'set_warehouse' | 'ship' | 'cancel' | 'delete'
|
||||||
|
data?: Record<string, any> // 操作相关数据
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置仓库快递参数 */
|
||||||
|
export interface SetWarehouseExpressParams {
|
||||||
|
warehouse_id?: number
|
||||||
|
express_company?: string
|
||||||
|
express_name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发货参数 */
|
||||||
|
export interface ShipOrderParams {
|
||||||
|
express_company: string // 快递公司
|
||||||
|
express_no: string // 快递单号
|
||||||
|
is_print?: boolean // 是否打印
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单统计响应 */
|
||||||
|
export interface OrderStatisticsResponse {
|
||||||
|
status_stats: Record<OrderStatus, number>
|
||||||
|
audit_stats: Record<AuditStatus, number>
|
||||||
|
delivery_stats: Record<DeliveryStatus, number>
|
||||||
|
amount_stats: {
|
||||||
|
total_goods_amount: number
|
||||||
|
total_discount_amount: number
|
||||||
|
total_freight: number
|
||||||
|
total_amount: number
|
||||||
|
avg_order_amount: number
|
||||||
|
}
|
||||||
|
platform_stats: Array<{ platform: string; count: number; amount: number }>
|
||||||
|
trend_stats: Array<{ date_group: string; order_count: number; total_amount: number }>
|
||||||
|
total_orders: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出订单参数 */
|
||||||
|
export interface ExportOrdersParams {
|
||||||
|
export_type: 'excel' | 'csv'
|
||||||
|
order_ids?: number[] // 指定订单ID
|
||||||
|
start_date?: string
|
||||||
|
end_date?: string
|
||||||
|
platform?: string
|
||||||
|
order_status?: OrderStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出订单响应 */
|
||||||
|
export interface ExportOrdersResponse {
|
||||||
|
filename: string
|
||||||
|
count: number
|
||||||
|
headers: string[]
|
||||||
|
data: Record<string, any>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新备注参数 */
|
||||||
|
export interface UpdateRemarkParams {
|
||||||
|
remark: string // 备注内容,最大1000字符
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单日志 */
|
||||||
|
export interface OrderLog {
|
||||||
|
id: number
|
||||||
|
order_id: number
|
||||||
|
action: string
|
||||||
|
operator_id: number
|
||||||
|
operator_name: string
|
||||||
|
content: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仪表板统计响应 */
|
||||||
|
export interface DashboardStatsResponse {
|
||||||
|
today_orders: number
|
||||||
|
today_amount: number
|
||||||
|
pending_audit_count: number
|
||||||
|
pending_ship_count: number
|
||||||
|
shipped_count: number
|
||||||
|
completed_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== API 接口 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单列表
|
||||||
|
* @description 分页查询订单列表,支持多种筛选条件
|
||||||
|
*/
|
||||||
|
export function getOrderList(params?: OrderListQuery) {
|
||||||
|
return request.get<OrderListResponse>('/api/orders', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单详情
|
||||||
|
* @description 根据订单ID获取完整订单信息
|
||||||
|
*/
|
||||||
|
export function getOrderDetail(id: number) {
|
||||||
|
return request.get<Order>(`/api/orders/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取订单
|
||||||
|
* @description 从平台拉取订单到系统
|
||||||
|
*/
|
||||||
|
export function pullOrders(data: PullOrdersParams) {
|
||||||
|
// 转换参数格式:驼峰转蛇形,时间范围拆分
|
||||||
|
const payload: any = {
|
||||||
|
platform: data.platform,
|
||||||
|
shop_id: data.shop_id || data.shopId,
|
||||||
|
pull_type: data.pull_type || 'increment'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理时间范围
|
||||||
|
if (data.start_time) {
|
||||||
|
payload.start_time = data.start_time
|
||||||
|
}
|
||||||
|
if (data.end_time) {
|
||||||
|
payload.end_time = data.end_time
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容 orderNo 参数
|
||||||
|
if (data.order_ids) {
|
||||||
|
payload.order_ids = data.order_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境 baseURL 已经是完整 URL,直接用 /api/orders/pull
|
||||||
|
return request.post<PullOrdersResponse>('/api/orders/pull', payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待匹配订单
|
||||||
|
* @description 获取尚未匹配ERP商品的订单
|
||||||
|
*/
|
||||||
|
export function getPendingMatchOrders(params?: { page?: number; limit?: number }) {
|
||||||
|
return request.get<OrderListResponse>('/api/orders/pending-match', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待审核订单
|
||||||
|
* @description 获取等待审核的订单列表
|
||||||
|
*/
|
||||||
|
export function getPendingAuditOrders(params?: { page?: number; limit?: number }) {
|
||||||
|
return request.get<OrderListResponse>('/api/orders/pending-audit', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单匹配
|
||||||
|
* @description 将平台订单商品与ERP商品进行匹配
|
||||||
|
*/
|
||||||
|
export function matchOrder(id: number, data?: { items?: Array<{ platform_sku: string; goods_id: number }> }) {
|
||||||
|
return request.post(`/api/orders/${id}/match`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核订单
|
||||||
|
* @description 审核单个订单,批准或驳回
|
||||||
|
*/
|
||||||
|
export function auditOrder(id: number, data: AuditOrderParams) {
|
||||||
|
return request.post(`/api/orders/${id}/audit`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量审核订单
|
||||||
|
* @description 批量审核多个订单
|
||||||
|
*/
|
||||||
|
export function batchAuditOrders(data: BatchAuditParams) {
|
||||||
|
return request.post<BatchAuditResponse>('/api/orders/batch-audit', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量操作订单
|
||||||
|
* @description 批量执行订单操作(如设置仓库、发货、取消等)
|
||||||
|
*/
|
||||||
|
export function batchOperation(data: BatchOperationParams) {
|
||||||
|
return request.post('/api/orders/batch-operation', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单统计
|
||||||
|
* @description 按日期、平台、店铺等维度统计订单
|
||||||
|
*/
|
||||||
|
export function getOrderStatistics(params?: {
|
||||||
|
start_date?: string
|
||||||
|
end_date?: string
|
||||||
|
platform?: string
|
||||||
|
shop_id?: number
|
||||||
|
date_format?: 'day' | 'week' | 'month'
|
||||||
|
}) {
|
||||||
|
return request.get<OrderStatisticsResponse>('/api/orders/statistics', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出订单
|
||||||
|
* @description 导出订单数据为 Excel 或 CSV
|
||||||
|
*/
|
||||||
|
export function exportOrders(data: ExportOrdersParams) {
|
||||||
|
return request.post<ExportOrdersResponse>('/api/orders/export', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单状态选项
|
||||||
|
* @description 获取订单状态下拉选项
|
||||||
|
*/
|
||||||
|
export function getStatusOptions() {
|
||||||
|
return request.get<Array<{ value: string; label: string }>>('/api/orders/status-options')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审核状态选项
|
||||||
|
* @description 获取审核状态下拉选项
|
||||||
|
*/
|
||||||
|
export function getAuditStatusOptions() {
|
||||||
|
return request.get<Array<{ value: string; label: string }>>('/api/orders/audit-status-options')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取发货状态选项
|
||||||
|
* @description 获取发货状态下拉选项
|
||||||
|
*/
|
||||||
|
export function getDeliveryStatusOptions() {
|
||||||
|
return request.get<Array<{ value: string; label: string }>>('/api/orders/delivery-status-options')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台选项
|
||||||
|
* @description 获取电商平台下拉选项
|
||||||
|
*/
|
||||||
|
export function getPlatformOptions() {
|
||||||
|
return request.get<Array<{ value: string; label: string }>>('/api/orders/platform-options')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取店铺选项
|
||||||
|
* @description 获取已授权店铺下拉选项
|
||||||
|
*/
|
||||||
|
export function getShopOptions() {
|
||||||
|
return request.get<Array<{ value: number; label: string }>>('/api/orders/shop-options')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取仪表板统计
|
||||||
|
* @description 获取今日订单、金额、待处理数量等仪表板数据
|
||||||
|
*/
|
||||||
|
export function getDashboardStats() {
|
||||||
|
return request.get<DashboardStatsResponse>('/api/orders/dashboard-stats')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单日志
|
||||||
|
* @description 获取订单的操作日志记录
|
||||||
|
*/
|
||||||
|
export function getOrderLogs(id: number) {
|
||||||
|
return request.get<OrderLog[]>(`/api/orders/${id}/logs`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订单备注
|
||||||
|
* @description 修改订单备注信息
|
||||||
|
*/
|
||||||
|
export function updateOrderRemark(id: number, data: UpdateRemarkParams) {
|
||||||
|
return request.put(`/api/orders/${id}/remark`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置仓库和快递
|
||||||
|
* @description 为订单设置仓库和快递公司(需订单已审核)
|
||||||
|
*/
|
||||||
|
export function setWarehouseExpress(id: number, data: SetWarehouseExpressParams) {
|
||||||
|
return request.post(`/api/orders/${id}/warehouse-express`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单发货
|
||||||
|
* @description 确认发货并填写快递信息(会扣减库存)
|
||||||
|
*/
|
||||||
|
export function shipOrder(id: number, data: ShipOrderParams) {
|
||||||
|
return request.post(`/api/orders/${id}/ship`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成订单
|
||||||
|
* @description 标记订单为已完成(需订单已发货)
|
||||||
|
*/
|
||||||
|
export function completeOrder(id: number) {
|
||||||
|
return request.put(`/api/orders/${id}/complete`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步订单到平台
|
||||||
|
* @description 将订单状态回传到电商平台
|
||||||
|
*/
|
||||||
|
export function syncOrderToPlatform(id: number) {
|
||||||
|
return request.post(`/api/orders/${id}/sync`, data)
|
||||||
|
}
|
||||||
76
src/api/platform-product.ts
Normal file
76
src/api/platform-product.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// src/api/platform-product.ts
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台商品
|
||||||
|
*/
|
||||||
|
export interface PlatformProduct {
|
||||||
|
id: number
|
||||||
|
platform_id: number
|
||||||
|
platform_name: string
|
||||||
|
platform_code: string
|
||||||
|
outer_id: string
|
||||||
|
num_iid: string
|
||||||
|
title: string
|
||||||
|
price: string
|
||||||
|
pic_url: string
|
||||||
|
seller_nick: string
|
||||||
|
approve_status: string
|
||||||
|
listed_time: string
|
||||||
|
unsale_time: string
|
||||||
|
stock: number
|
||||||
|
has_sync: boolean
|
||||||
|
bound_goods_id?: number
|
||||||
|
bound_goods_name?: string
|
||||||
|
bound_sku_code?: string
|
||||||
|
last_sync_at?: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台商品列表查询参数
|
||||||
|
*/
|
||||||
|
export interface PlatformProductListParams {
|
||||||
|
platform_id?: number
|
||||||
|
keyword?: string
|
||||||
|
sync_status?: 'all' | 'synced' | 'unsynced'
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台商品列表
|
||||||
|
*/
|
||||||
|
export function getPlatformProductList(params?: PlatformProductListParams) {
|
||||||
|
return request.get<{
|
||||||
|
list: PlatformProduct[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
last_page: number
|
||||||
|
}>('/platform-products', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台商品详情
|
||||||
|
*/
|
||||||
|
export function getPlatformProductDetail(id: number) {
|
||||||
|
return request.get<PlatformProduct>(`/platform-products/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步平台商品
|
||||||
|
*/
|
||||||
|
export function syncPlatformProducts(platformId: number) {
|
||||||
|
return request.post<{ synced_count: number }>('/platform-products/sync', { platform_id: platformId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定平台商品到ERP商品
|
||||||
|
*/
|
||||||
|
export function bindPlatformProduct(platformProductId: number, goodsId: number, skuCode?: string) {
|
||||||
|
return request.post(`/platform-products/${platformProductId}/bind`, {
|
||||||
|
goods_id: goodsId,
|
||||||
|
sku_code: skuCode
|
||||||
|
})
|
||||||
|
}
|
||||||
113
src/api/platform.ts
Normal file
113
src/api/platform.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// src/api/platform.ts
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台数据类型
|
||||||
|
*/
|
||||||
|
export interface Platform {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
status?: number
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台统计数据类型
|
||||||
|
*/
|
||||||
|
export interface PlatformStats {
|
||||||
|
total: number
|
||||||
|
active: number
|
||||||
|
inactive: number
|
||||||
|
today_add: number
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台列表
|
||||||
|
* @param params 查询参数(page, limit, name, status)
|
||||||
|
*/
|
||||||
|
export function getPlatformList(params?: any) {
|
||||||
|
return request.get('/platforms', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台详情
|
||||||
|
* @param id 平台ID
|
||||||
|
*/
|
||||||
|
export function getPlatformDetail(id: number) {
|
||||||
|
return request.get(`/platforms/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建平台
|
||||||
|
* @param data 平台数据
|
||||||
|
*/
|
||||||
|
export function createPlatform(data: Platform) {
|
||||||
|
return request.post('/platforms', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新平台
|
||||||
|
* @param id 平台ID
|
||||||
|
* @param data 更新数据
|
||||||
|
*/
|
||||||
|
export function updatePlatform(id: number, data: Partial<Platform>) {
|
||||||
|
return request.put(`/platforms/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除平台
|
||||||
|
* @param id 平台ID
|
||||||
|
*/
|
||||||
|
export function deletePlatform(id: number) {
|
||||||
|
return request.delete(`/platforms/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步平台数据
|
||||||
|
*/
|
||||||
|
export function syncPlatform() {
|
||||||
|
return request.post('/platforms/sync')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新平台
|
||||||
|
* @param data 批量更新数据
|
||||||
|
*/
|
||||||
|
export function batchUpdatePlatform(data: Partial<Platform>[]) {
|
||||||
|
return request.post('/platforms/batch-update', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台统计
|
||||||
|
*/
|
||||||
|
export function getPlatformStats() {
|
||||||
|
return request.get('/platforms/stats')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有平台用于下拉选择
|
||||||
|
*/
|
||||||
|
export function getAllPlatforms() {
|
||||||
|
return request.get('/platforms/all')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取面单账号列表
|
||||||
|
*/
|
||||||
|
export function getWaybillAccounts(platformId?: number) {
|
||||||
|
return Promise.resolve({
|
||||||
|
code: 200,
|
||||||
|
data: { list: [], total: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步面单账号
|
||||||
|
*/
|
||||||
|
export function syncWaybillAccounts(platformId: number) {
|
||||||
|
return Promise.resolve({ code: 200, message: '功能开发中', data: null })
|
||||||
|
}
|
||||||
166
src/api/platformGoods.ts
Normal file
166
src/api/platformGoods.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
// src/api/platformGoods.ts
|
||||||
|
|
||||||
|
// 模拟店铺数据
|
||||||
|
const mockShops = {
|
||||||
|
taobao: [
|
||||||
|
{ id: 'tb1', name: '淘宝官方旗舰店' },
|
||||||
|
{ id: 'tb2', name: '淘宝专营店' }
|
||||||
|
],
|
||||||
|
jd: [
|
||||||
|
{ id: 'jd1', name: '京东自营店' },
|
||||||
|
{ id: 'jd2', name: '京东专营店' }
|
||||||
|
],
|
||||||
|
pdd: [
|
||||||
|
{ id: 'pdd1', name: '拼多多官方旗舰店' },
|
||||||
|
{ id: 'pdd2', name: '拼多多专营店' }
|
||||||
|
],
|
||||||
|
douyin: [
|
||||||
|
{ id: 'dy1', name: '抖音小店' },
|
||||||
|
{ id: 'dy2', name: '抖音专营店' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟平台商品数据(按平台和店铺组织)
|
||||||
|
const mockPlatformGoods: Record<string, Record<string, any[]>> = {
|
||||||
|
taobao: {
|
||||||
|
tb1: [
|
||||||
|
{
|
||||||
|
id: 'tb_spu_1',
|
||||||
|
type: 'spu',
|
||||||
|
name: '连衣裙',
|
||||||
|
code: 'TB-SPU-001',
|
||||||
|
price: 199.0,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: false,
|
||||||
|
skus: [
|
||||||
|
{
|
||||||
|
id: 'tb_sku_11',
|
||||||
|
spuId: 'tb_spu_1',
|
||||||
|
type: 'sku',
|
||||||
|
name: '红色-S',
|
||||||
|
code: 'TB-SKU-001-R-S',
|
||||||
|
price: 199.0,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tb_sku_12',
|
||||||
|
spuId: 'tb_spu_1',
|
||||||
|
type: 'sku',
|
||||||
|
name: '红色-M',
|
||||||
|
code: 'TB-SKU-001-R-M',
|
||||||
|
price: 199.0,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tb_spu_2',
|
||||||
|
type: 'spu',
|
||||||
|
name: '运动鞋',
|
||||||
|
code: 'TB-SPU-002',
|
||||||
|
price: 299.0,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: false,
|
||||||
|
skus: [
|
||||||
|
{
|
||||||
|
id: 'tb_sku_21',
|
||||||
|
spuId: 'tb_spu_2',
|
||||||
|
type: 'sku',
|
||||||
|
name: '白色-42',
|
||||||
|
code: 'TB-SKU-002-W-42',
|
||||||
|
price: 299.0,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
tb2: []
|
||||||
|
},
|
||||||
|
jd: {},
|
||||||
|
pdd: {},
|
||||||
|
douyin: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:平台代码转中文名称
|
||||||
|
function getPlatformName(platform: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
taobao: '淘宝',
|
||||||
|
jd: '京东',
|
||||||
|
pdd: '拼多多',
|
||||||
|
douyin: '抖音'
|
||||||
|
}
|
||||||
|
return map[platform] || platform
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台-店铺树结构
|
||||||
|
*/
|
||||||
|
export function getPlatformShopTree() {
|
||||||
|
return new Promise<{ code: number; data: any[] }>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const tree = Object.keys(mockShops).map(platform => ({
|
||||||
|
id: `platform-${platform}`,
|
||||||
|
label: getPlatformName(platform),
|
||||||
|
type: 'platform',
|
||||||
|
platform,
|
||||||
|
children: mockShops[platform].map(shop => ({
|
||||||
|
id: `shop-${shop.id}`,
|
||||||
|
label: shop.name,
|
||||||
|
type: 'shop',
|
||||||
|
platform,
|
||||||
|
shopId: shop.id,
|
||||||
|
leaf: true
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
resolve({ code: 200, data: tree })
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取店铺列表(兼容旧接口)
|
||||||
|
export function getShopsByPlatform(platform: string) {
|
||||||
|
return new Promise<{ code: number; data: any[] }>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ code: 200, data: mockShops[platform] || [] })
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载平台商品
|
||||||
|
export function downloadPlatformGoods(params: {
|
||||||
|
platform: string
|
||||||
|
shopId: string
|
||||||
|
downloadType: string
|
||||||
|
specifyIds?: string
|
||||||
|
}) {
|
||||||
|
console.log('downloadPlatformGoods called with params:', params)
|
||||||
|
return new Promise<{ code: number; message?: string }>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ code: 200, message: '下载任务已提交' })
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取已下载的平台商品列表
|
||||||
|
export function getPlatformGoodsList(params: { platform: string; shopId: string }) {
|
||||||
|
console.log('getPlatformGoodsList called with params:', params)
|
||||||
|
return new Promise<{ code: number; data: any[] }>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const goods = mockPlatformGoods[params.platform]?.[params.shopId] || []
|
||||||
|
resolve({ code: 200, data: goods })
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定ERP商品与平台SKU
|
||||||
|
export function bindErpSku(data: { skuId: string; erpGoodsId: string }) {
|
||||||
|
console.log('bindErpSku called with data:', data)
|
||||||
|
return new Promise<{ code: number; message?: string }>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ code: 200, message: '绑定成功' })
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
119
src/api/print-job.ts
Normal file
119
src/api/print-job.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// 打印任务 API
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建打印任务
|
||||||
|
*/
|
||||||
|
export function createJob(data: {
|
||||||
|
platform: string
|
||||||
|
plugin_code: string
|
||||||
|
order_id?: number
|
||||||
|
template_id?: number
|
||||||
|
print_data: Record<string, any>
|
||||||
|
priority?: number
|
||||||
|
}) {
|
||||||
|
return request.post('/print-jobs', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建打印任务
|
||||||
|
*/
|
||||||
|
export function createBatchJobs(data: {
|
||||||
|
platform: string
|
||||||
|
plugin_code: string
|
||||||
|
template_id?: number
|
||||||
|
orders: Array<{
|
||||||
|
order_id?: number
|
||||||
|
priority?: number
|
||||||
|
print_data: Record<string, any>
|
||||||
|
}>
|
||||||
|
}) {
|
||||||
|
return request.post('/print-jobs/batch', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取打印队列
|
||||||
|
*/
|
||||||
|
export function getQueue(params?: {
|
||||||
|
status?: string
|
||||||
|
platform?: string
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
}) {
|
||||||
|
return request.get('/print-jobs/queue', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取打印历史
|
||||||
|
*/
|
||||||
|
export function getHistory(params?: {
|
||||||
|
page?: number
|
||||||
|
per_page?: number
|
||||||
|
}) {
|
||||||
|
return request.get('/print-jobs/history', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取打印统计
|
||||||
|
*/
|
||||||
|
export function getStats() {
|
||||||
|
return request.get('/print-jobs/stats')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量加入队列
|
||||||
|
*/
|
||||||
|
export function batchQueue(jobIds: number[]) {
|
||||||
|
return request.post('/print-jobs/batch-queue', { job_ids: jobIds })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空已完成任务
|
||||||
|
*/
|
||||||
|
export function clearCompleted() {
|
||||||
|
return request.post('/print-jobs/clear-completed')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试任务
|
||||||
|
*/
|
||||||
|
export function retryJob(jobId: number) {
|
||||||
|
return request.post(`/print-jobs/${jobId}/retry`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消任务
|
||||||
|
*/
|
||||||
|
export function cancelJob(jobId: number) {
|
||||||
|
return request.post(`/print-jobs/${jobId}/cancel`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 设备端 API(插件调用)==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一个待打印任务
|
||||||
|
*/
|
||||||
|
export function getNextJob(deviceId: string) {
|
||||||
|
return request.get('/print-device/next-job', { params: { device_id: deviceId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认领打印任务
|
||||||
|
*/
|
||||||
|
export function claimJob(jobId: number, deviceId: string) {
|
||||||
|
return request.post('/print-device/claim', { job_id: jobId, device_id: deviceId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记打印完成
|
||||||
|
*/
|
||||||
|
export function completeJob(jobId: number, deviceId: string) {
|
||||||
|
return request.post('/print-device/complete', { job_id: jobId, device_id: deviceId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记打印失败
|
||||||
|
*/
|
||||||
|
export function failJob(jobId: number, error: string, deviceId?: string) {
|
||||||
|
return request.post('/print-device/fail', { job_id: jobId, error, device_id: deviceId })
|
||||||
|
}
|
||||||
78
src/api/print-plugin.ts
Normal file
78
src/api/print-plugin.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// 打印插件管理 API
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件列表
|
||||||
|
*/
|
||||||
|
export function getPlugins(platform?: string) {
|
||||||
|
return request.get('/print-plugins', { params: { platform } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件详情
|
||||||
|
*/
|
||||||
|
export function getPlugin(code: string) {
|
||||||
|
return request.get(`/print-plugins/${code}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件最新版本
|
||||||
|
*/
|
||||||
|
export function getLatestVersion(code: string) {
|
||||||
|
return request.get(`/print-plugins/${code}/version`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载插件
|
||||||
|
*/
|
||||||
|
export function downloadPlugin(code: string) {
|
||||||
|
return request.get(`/print-plugins/${code}/download`, { responseType: 'blob' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户已安装插件
|
||||||
|
*/
|
||||||
|
export function getInstallations() {
|
||||||
|
return request.get('/print-plugins/auth/installations')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件版本
|
||||||
|
*/
|
||||||
|
export function checkVersion(pluginCode: string, deviceId: string) {
|
||||||
|
return request.post('/print-plugins/auth/check-version', { plugin_code: pluginCode, device_id: deviceId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册插件安装
|
||||||
|
*/
|
||||||
|
export function registerPlugin(data: {
|
||||||
|
plugin_code: string
|
||||||
|
version: string
|
||||||
|
device_id: string
|
||||||
|
device_name?: string
|
||||||
|
os_version?: string
|
||||||
|
}) {
|
||||||
|
return request.post('/print-plugins/auth/register', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送心跳
|
||||||
|
*/
|
||||||
|
export function sendHeartbeat(deviceId: string) {
|
||||||
|
return request.post('/print-plugins/auth/heartbeat', { device_id: deviceId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载插件
|
||||||
|
*/
|
||||||
|
export function uninstall(pluginCode: string, deviceId: string) {
|
||||||
|
return request.post('/print-plugins/auth/uninstall', { plugin_code: pluginCode, device_id: deviceId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备状态
|
||||||
|
*/
|
||||||
|
export function getDeviceStatus(deviceId: string) {
|
||||||
|
return request.get(`/print-plugins/auth/device/${deviceId}/status`)
|
||||||
|
}
|
||||||
414
src/api/print.ts
Normal file
414
src/api/print.ts
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export interface OrderItem {
|
||||||
|
id: number
|
||||||
|
shortId: string
|
||||||
|
platformOrderSn: string
|
||||||
|
shopName: string
|
||||||
|
platformName: string
|
||||||
|
createTime: string
|
||||||
|
receiverName: string
|
||||||
|
receiverPhone: string
|
||||||
|
receiverAddress: string
|
||||||
|
goodsAmount: string
|
||||||
|
erpStatus: number
|
||||||
|
isBindErp: boolean
|
||||||
|
platformGoodsName: string
|
||||||
|
erpGoodsName: string
|
||||||
|
platformSku: string
|
||||||
|
erpSku: string
|
||||||
|
goodsName: string
|
||||||
|
bindStatusText: string
|
||||||
|
warehouseName?: string
|
||||||
|
warehouseType?: string
|
||||||
|
expressName?: string
|
||||||
|
expressId?: string
|
||||||
|
items?: any[]
|
||||||
|
printResult?: number
|
||||||
|
printLogs?: any[]
|
||||||
|
printError?: string
|
||||||
|
expressNo?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrintBatchItem {
|
||||||
|
id: number
|
||||||
|
batchNo: string
|
||||||
|
printTime: string
|
||||||
|
printUserId: number
|
||||||
|
printUserName: string
|
||||||
|
orderCount: number
|
||||||
|
status: number // 0:打印中,1:成功,2:部分成功,3:失败
|
||||||
|
remark: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrintBatchDetail extends PrintBatchItem {
|
||||||
|
orders: Array<OrderItem & { printSuccess?: boolean; errorMsg?: string; expressNo?: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateBatchParams {
|
||||||
|
orderIds: number[]
|
||||||
|
orders: Array<OrderItem & { printSuccess?: boolean; errorMsg?: string; expressNo?: string }>
|
||||||
|
remark?: string
|
||||||
|
printUserId: number
|
||||||
|
printUserName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchListParams {
|
||||||
|
batchNo?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
printUser?: string
|
||||||
|
status?: number
|
||||||
|
currentPage: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpressItem {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateItem {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
expressId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrinterStatus {
|
||||||
|
type: 'idle' | 'printing' | 'error'
|
||||||
|
errorCode?: string
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpressBalance {
|
||||||
|
expressId: string
|
||||||
|
expressName: string
|
||||||
|
platform: string
|
||||||
|
balance: number
|
||||||
|
warningThreshold?: number
|
||||||
|
status: 'normal' | 'low' | 'empty'
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const mockExpressList: ExpressItem[] = [
|
||||||
|
{ id: '1', name: '顺丰速运', code: 'SF' },
|
||||||
|
{ id: '2', name: '中通快递', code: 'ZTO' },
|
||||||
|
{ id: '3', name: '圆通速递', code: 'YTO' },
|
||||||
|
{ id: '4', name: '韵达快递', code: 'YD' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockTemplateList: TemplateItem[] = [
|
||||||
|
{ id: '101', name: '标准模板', expressId: '1' },
|
||||||
|
{ id: '102', name: '生鲜模板', expressId: '1' },
|
||||||
|
{ id: '201', name: '标准模板', expressId: '2' },
|
||||||
|
{ id: '301', name: '标准模板', expressId: '3' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const PRINT_BATCH_STORAGE_KEY = 'print_batches'
|
||||||
|
|
||||||
|
// 从 localStorage 加载批次
|
||||||
|
const loadPrintBatches = (): PrintBatchDetail[] => {
|
||||||
|
const stored = localStorage.getItem(PRINT_BATCH_STORAGE_KEY)
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(stored)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析打印批次失败', e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPrintBatches(params: BatchListParams): Promise<ApiResponse<{ list: PrintBatchItem[]; total: number }>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
let list: PrintBatchItem[] = loadPrintBatches()
|
||||||
|
if (params.batchNo) {
|
||||||
|
list = list.filter(b => b.batchNo.includes(params.batchNo))
|
||||||
|
}
|
||||||
|
if (params.startDate && params.endDate) {
|
||||||
|
list = list.filter(b => b.printTime >= params.startDate && b.printTime <= params.endDate)
|
||||||
|
}
|
||||||
|
if (params.printUser) {
|
||||||
|
list = list.filter(b => b.printUserName === params.printUser)
|
||||||
|
}
|
||||||
|
if (params.status !== undefined) {
|
||||||
|
list = list.filter(b => b.status === params.status)
|
||||||
|
}
|
||||||
|
list.sort((a, b) => new Date(b.printTime).getTime() - new Date(a.printTime).getTime())
|
||||||
|
const total = list.length
|
||||||
|
const start = (params.currentPage - 1) * params.pageSize
|
||||||
|
const end = start + params.pageSize
|
||||||
|
list = list.slice(start, end)
|
||||||
|
resolve({ code: 200, data: { list, total } })
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPrintBatchDetail(id: string | number): Promise<ApiResponse<PrintBatchDetail>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const batches = loadPrintBatches()
|
||||||
|
const batch = batches.find(b => b.id === Number(id))
|
||||||
|
if (!batch) {
|
||||||
|
// 动态生成模拟数据
|
||||||
|
const fakeBatch: PrintBatchDetail = {
|
||||||
|
id: Number(id),
|
||||||
|
batchNo: `PRT${String(id).padStart(12, '0')}`,
|
||||||
|
printTime: new Date().toLocaleString(),
|
||||||
|
printUserId: 1001,
|
||||||
|
printUserName: 'admin',
|
||||||
|
orderCount: 3,
|
||||||
|
status: 1,
|
||||||
|
remark: '动态生成的模拟批次(原批次不存在)',
|
||||||
|
createdAt: new Date().toLocaleString(),
|
||||||
|
orders: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
shortId: 'S001',
|
||||||
|
platformOrderSn: 'PDD123456',
|
||||||
|
shopName: '拼多多专营店',
|
||||||
|
platformName: '拼多多',
|
||||||
|
createTime: '2025-03-08 09:00:00',
|
||||||
|
receiverName: '张三',
|
||||||
|
receiverPhone: '13800138001',
|
||||||
|
receiverAddress: '北京市朝阳区xx路1号',
|
||||||
|
goodsAmount: '100.00',
|
||||||
|
erpStatus: 1,
|
||||||
|
isBindErp: true,
|
||||||
|
platformGoodsName: '商品A',
|
||||||
|
erpGoodsName: '商品A',
|
||||||
|
platformSku: 'SKU-A',
|
||||||
|
erpSku: 'SKU-A',
|
||||||
|
goodsName: '商品A',
|
||||||
|
bindStatusText: '已绑定',
|
||||||
|
warehouseName: '主仓',
|
||||||
|
warehouseType: 'erp',
|
||||||
|
expressName: '顺丰速运',
|
||||||
|
expressId: '1',
|
||||||
|
printSuccess: true,
|
||||||
|
expressNo: 'SF1234567890'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
resolve({ code: 200, data: fakeBatch, message: '批次不存在,已返回模拟数据' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve({ code: 200, data: batch })
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reprintBatch(id: number): Promise<ApiResponse<null>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ code: 200, data: null, message: '重新打印任务已提交' })
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPrintBatch(data: CreateBatchParams): Promise<ApiResponse<PrintBatchDetail>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const batches = loadPrintBatches()
|
||||||
|
const successCount = data.orders.filter(o => o.printSuccess).length
|
||||||
|
let status: number
|
||||||
|
if (successCount === data.orders.length) status = 1
|
||||||
|
else if (successCount > 0) status = 2
|
||||||
|
else status = 3
|
||||||
|
|
||||||
|
const newBatch: PrintBatchDetail = {
|
||||||
|
id: Date.now(),
|
||||||
|
batchNo: `PRT${new Date().toISOString().slice(0,10).replace(/-/g,'')}${String(Math.floor(Math.random()*10000)).padStart(6,'0')}`,
|
||||||
|
printTime: new Date().toLocaleString(),
|
||||||
|
printUserId: data.printUserId,
|
||||||
|
printUserName: data.printUserName,
|
||||||
|
orderCount: data.orders.length,
|
||||||
|
status,
|
||||||
|
remark: data.remark || '',
|
||||||
|
createdAt: new Date().toLocaleString(),
|
||||||
|
orders: data.orders.map(order => ({ ...order }))
|
||||||
|
}
|
||||||
|
batches.unshift(newBatch)
|
||||||
|
localStorage.setItem(PRINT_BATCH_STORAGE_KEY, JSON.stringify(batches))
|
||||||
|
resolve({ code: 200, data: newBatch })
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExpressList(): Promise<ApiResponse<ExpressItem[]>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve({ code: 200, data: mockExpressList }), 200)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTemplateList(expressId?: string): Promise<ApiResponse<TemplateItem[]>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
let list = mockTemplateList
|
||||||
|
if (expressId) list = list.filter(t => t.expressId === expressId)
|
||||||
|
resolve({ code: 200, data: list })
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateWaybill(params: any): Promise<ApiResponse<{ html: string }>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const express = mockExpressList.find(e => e.id === params.expressId)
|
||||||
|
const template = mockTemplateList.find(t => t.id === params.templateId)
|
||||||
|
const html = `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>电子面单</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'SimHei', sans-serif; margin: 20px; }
|
||||||
|
.waybill { border: 1px solid #333; padding: 20px; width: 300px; margin: 0 auto; position: relative; }
|
||||||
|
h3 { text-align: center; }
|
||||||
|
.info { margin: 10px 0; }
|
||||||
|
.seq { position: absolute; top: 10px; right: 10px; font-size: 20px; font-weight: bold; color: #f00; }
|
||||||
|
.tracking-number { font-size: 18px; font-weight: bold; color: #000; margin: 10px 0; }
|
||||||
|
.print-time { position: absolute; bottom: 10px; right: 10px; font-size: 12px; color: #999; }
|
||||||
|
.batch-no { position: absolute; bottom: 10px; left: 10px; font-size: 12px; color: #999; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="waybill">
|
||||||
|
<div class="seq">#${params.seq || 'N/A'}</div>
|
||||||
|
<h3>电子面单</h3>
|
||||||
|
<div class="tracking-number">运单号:${params.expressNo || 'SF' + Date.now()}</div>
|
||||||
|
<div class="info">快递公司:${express?.name || '未知'}</div>
|
||||||
|
<div class="info">模板:${template?.name || '未知'}</div>
|
||||||
|
<div class="info">订单ID:${params.orderId}</div>
|
||||||
|
<div class="info">收件人:张三</div>
|
||||||
|
<div class="info">地址:北京市朝阳区xx路1号</div>
|
||||||
|
<div class="info">联系电话:13800138001</div>
|
||||||
|
<div class="print-time">打印时间:${params.printTime || new Date().toLocaleString()}</div>
|
||||||
|
<div class="batch-no">批次号:${params.batchNo || ''}</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
resolve({ code: 200, data: { html } })
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testPrinterConnection(port: number, path: string = ''): Promise<boolean> {
|
||||||
|
return Promise.resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryManualPrinterStatus(port: number, path: string = ''): Promise<{ ready: boolean; error?: string }> {
|
||||||
|
return Promise.resolve({ ready: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function connectManualPrinterStatus(port: number, path: string = '') {}
|
||||||
|
export function disconnectPrinterStatus() {}
|
||||||
|
export function onPrinterStatus(callback: (status: PrinterStatus) => void) { return () => {} }
|
||||||
|
|
||||||
|
export function queryExpressBalance(expressIds?: string[]): Promise<ApiResponse<ExpressBalance[]>> {
|
||||||
|
return Promise.resolve({
|
||||||
|
code: 200,
|
||||||
|
data: [
|
||||||
|
{ expressId: '1', expressName: '顺丰速运', platform: 'all', balance: 150, status: 'normal' },
|
||||||
|
{ expressId: '2', expressName: '中通快递', platform: 'pdd', balance: 35, status: 'normal' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchExpressNumbers(expressIds: string[]): Promise<ApiResponse<Array<{ expressId: string; expressName: string; success: boolean; message?: string }>>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const results = expressIds.map(id => {
|
||||||
|
const express = mockExpressList.find(e => e.id === id)
|
||||||
|
const name = express?.name || '未知'
|
||||||
|
const success = Math.random() > 0.3
|
||||||
|
let message = ''
|
||||||
|
if (!success) {
|
||||||
|
const reasons = ['面单余额不足,请联系快递充值', '该区域不可达,请更换快递', '快递接口异常', '请稍后重试']
|
||||||
|
message = reasons[Math.floor(Math.random() * reasons.length)]
|
||||||
|
}
|
||||||
|
return { expressId: id, expressName: name, success, message }
|
||||||
|
})
|
||||||
|
resolve({ code: 200, data: results })
|
||||||
|
}, 800)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有订单打印记录(扁平化)
|
||||||
|
export function getOrderPrintLogs(params: any): Promise<ApiResponse<{ list: any[]; total: number }>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const batches = loadPrintBatches()
|
||||||
|
let orders: any[] = []
|
||||||
|
batches.forEach(batch => {
|
||||||
|
if (batch.orders && Array.isArray(batch.orders)) {
|
||||||
|
batch.orders.forEach(order => {
|
||||||
|
orders.push({
|
||||||
|
...order,
|
||||||
|
batchId: batch.id,
|
||||||
|
batchNo: batch.batchNo,
|
||||||
|
batchPrintTime: batch.printTime,
|
||||||
|
batchStatus: batch.status
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 如果没有订单数据,返回模拟数据以便测试
|
||||||
|
if (orders.length === 0) {
|
||||||
|
orders = [
|
||||||
|
{
|
||||||
|
id: 1001,
|
||||||
|
shortId: 'S001',
|
||||||
|
platformOrderSn: 'PDD123456789',
|
||||||
|
batchId: 999,
|
||||||
|
batchNo: 'PRT202603090001',
|
||||||
|
batchPrintTime: '2026-03-09 14:30:00',
|
||||||
|
receiverName: '张三',
|
||||||
|
goodsAmount: '199.00',
|
||||||
|
expressName: '顺丰速运',
|
||||||
|
expressId: '1',
|
||||||
|
expressNo: 'SF1234567890',
|
||||||
|
printSuccess: true,
|
||||||
|
errorMsg: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1002,
|
||||||
|
shortId: 'S002',
|
||||||
|
platformOrderSn: 'JD987654321',
|
||||||
|
batchId: 999,
|
||||||
|
batchNo: 'PRT202603090001',
|
||||||
|
batchPrintTime: '2026-03-09 14:30:00',
|
||||||
|
receiverName: '李四',
|
||||||
|
goodsAmount: '299.00',
|
||||||
|
expressName: '中通快递',
|
||||||
|
expressId: '2',
|
||||||
|
expressNo: 'ZTO987654321',
|
||||||
|
printSuccess: false,
|
||||||
|
errorMsg: '面单余额不足'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// 按打印时间倒序
|
||||||
|
orders.sort((a, b) => new Date(b.batchPrintTime).getTime() - new Date(a.batchPrintTime).getTime())
|
||||||
|
const total = orders.length
|
||||||
|
// 处理分页参数
|
||||||
|
const currentPage = params.currentPage || 1
|
||||||
|
const pageSize = params.pageSize || 10
|
||||||
|
const start = (currentPage - 1) * pageSize
|
||||||
|
const end = start + pageSize
|
||||||
|
const list = orders.slice(start, end)
|
||||||
|
resolve({ code: 200, data: { list, total } })
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
}
|
||||||
172
src/api/purchase.ts
Normal file
172
src/api/purchase.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/** 采购单状态 */
|
||||||
|
export type PurchaseOrderStatus = 'draft' | 'pending_review' | 'approved' | 'rejected' | 'pushed' | 'cancelled'
|
||||||
|
|
||||||
|
/** 采购单商品项 */
|
||||||
|
export interface PurchaseOrderItem {
|
||||||
|
id?: string
|
||||||
|
skuCode: string
|
||||||
|
skuName?: string
|
||||||
|
quantity: number
|
||||||
|
unitPrice?: number
|
||||||
|
totalPrice?: number
|
||||||
|
receivedQuantity?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 采购单 */
|
||||||
|
export interface PurchaseOrder {
|
||||||
|
id?: string
|
||||||
|
orderNo?: string
|
||||||
|
supplierId: string
|
||||||
|
supplierName?: string
|
||||||
|
warehouseId: string
|
||||||
|
warehouseName?: string
|
||||||
|
status: PurchaseOrderStatus
|
||||||
|
totalAmount?: number
|
||||||
|
remark?: string
|
||||||
|
items: PurchaseOrderItem[]
|
||||||
|
createTime?: string
|
||||||
|
updateTime?: string
|
||||||
|
reviewer?: string
|
||||||
|
reviewTime?: string
|
||||||
|
reviewComment?: string
|
||||||
|
cloudPushTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 采购单列表查询参数 */
|
||||||
|
export interface PurchaseListParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
status?: PurchaseOrderStatus
|
||||||
|
supplierId?: string
|
||||||
|
warehouseId?: string
|
||||||
|
keyword?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建采购单请求 */
|
||||||
|
export interface CreatePurchaseOrderDto {
|
||||||
|
supplierId: string
|
||||||
|
warehouseId: string
|
||||||
|
remark?: string
|
||||||
|
items: PurchaseOrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新采购单请求 */
|
||||||
|
export interface UpdatePurchaseOrderDto {
|
||||||
|
supplierId?: string
|
||||||
|
warehouseId?: string
|
||||||
|
remark?: string
|
||||||
|
items?: PurchaseOrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== API 函数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取采购单列表
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export function getPurchaseList(params?: PurchaseListParams) {
|
||||||
|
return request.get<PurchaseListResponse>('/purchase-orders', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取采购单详情
|
||||||
|
* @param id 采购单ID
|
||||||
|
*/
|
||||||
|
export function getPurchaseDetail(id: string) {
|
||||||
|
return request.get<PurchaseOrder>(`/purchase-orders/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建采购单
|
||||||
|
* @param data 采购单数据
|
||||||
|
*/
|
||||||
|
export function createPurchaseOrder(data: CreatePurchaseOrderDto) {
|
||||||
|
return request.post<PurchaseOrder>('/purchase-orders', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新采购单
|
||||||
|
* @param id 采购单ID
|
||||||
|
* @param data 更新数据
|
||||||
|
*/
|
||||||
|
export function updatePurchaseOrder(id: string, data: UpdatePurchaseOrderDto) {
|
||||||
|
return request.put<PurchaseOrder>(`/purchase-orders/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交采购单审核
|
||||||
|
* @param id 采购单ID
|
||||||
|
*/
|
||||||
|
export function submitPurchaseReview(id: string) {
|
||||||
|
return request.post<void>(`/purchase-orders/${id}/submit-review`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核通过采购单
|
||||||
|
* @param id 采购单ID
|
||||||
|
*/
|
||||||
|
export function approvePurchaseOrder(id: string) {
|
||||||
|
return request.post<void>(`/purchase-orders/${id}/approve`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 驳回采购单
|
||||||
|
* @param id 采购单ID
|
||||||
|
* @param comment 驳回原因
|
||||||
|
*/
|
||||||
|
export function rejectPurchaseOrder(id: string, comment: string) {
|
||||||
|
return request.post<void>(`/purchase-orders/${id}/reject`, { comment })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送采购单到云仓
|
||||||
|
* @param id 采购单ID
|
||||||
|
*/
|
||||||
|
export function pushPurchaseToCloud(id: string) {
|
||||||
|
return request.post<void>(`/purchase-orders/${id}/push-cloud`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消推送采购单
|
||||||
|
* @param id 采购单ID
|
||||||
|
*/
|
||||||
|
export function cancelPushPurchase(id: string) {
|
||||||
|
return request.post<void>(`/purchase-orders/${id}/cancel-push`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 别名兼容
|
||||||
|
export const approvePurchase = approvePurchaseOrder
|
||||||
|
export const rejectPurchase = rejectPurchaseOrder
|
||||||
|
export const submitReview = submitPurchaseReview
|
||||||
|
export const createPurchase = createPurchaseOrder
|
||||||
|
export const updatePurchase = updatePurchaseOrder
|
||||||
|
export const getPurchase = getPurchaseDetail
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除采购单
|
||||||
|
*/
|
||||||
|
export function deletePurchase(id: string) {
|
||||||
|
return request.delete<void>(`/purchase-orders/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建草稿采购单
|
||||||
|
*/
|
||||||
|
export function createDraftPurchase(data: any) {
|
||||||
|
return Promise.resolve({ code: 200, message: '功能开发中', data: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 响应类型 ====================
|
||||||
|
|
||||||
|
export interface PurchaseListResponse {
|
||||||
|
list: PurchaseOrder[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
116
src/api/receiving.ts
Normal file
116
src/api/receiving.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/** 收货单状态 */
|
||||||
|
export type ReceivingOrderStatus = 'pending' | 'partial' | 'completed' | 'cancelled'
|
||||||
|
|
||||||
|
/** 收货单商品项 */
|
||||||
|
export interface ReceivingOrderItem {
|
||||||
|
id?: string
|
||||||
|
skuCode: string
|
||||||
|
skuName?: string
|
||||||
|
expectedQuantity: number
|
||||||
|
receivedQuantity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收货单 */
|
||||||
|
export interface ReceivingOrder {
|
||||||
|
id?: string
|
||||||
|
orderNo?: string
|
||||||
|
purchaseOrderId?: string
|
||||||
|
purchaseOrderNo?: string
|
||||||
|
warehouseId: string
|
||||||
|
warehouseName?: string
|
||||||
|
supplierId?: string
|
||||||
|
supplierName?: string
|
||||||
|
status: ReceivingOrderStatus
|
||||||
|
totalAmount?: number
|
||||||
|
remark?: string
|
||||||
|
items: ReceivingOrderItem[]
|
||||||
|
createTime?: string
|
||||||
|
updateTime?: string
|
||||||
|
receiver?: string
|
||||||
|
receiveTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收货单列表查询参数 */
|
||||||
|
export interface ReceivingListParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
status?: ReceivingOrderStatus
|
||||||
|
purchaseOrderId?: string
|
||||||
|
warehouseId?: string
|
||||||
|
keyword?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建收货单请求 */
|
||||||
|
export interface CreateReceivingOrderDto {
|
||||||
|
purchaseOrderId: string
|
||||||
|
warehouseId: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新收货单请求 */
|
||||||
|
export interface UpdateReceivingOrderDto {
|
||||||
|
remark?: string
|
||||||
|
items?: ReceivingOrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== API 函数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取收货单列表
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export function getReceivingList(params?: ReceivingListParams) {
|
||||||
|
return request.get<ReceivingListResponse>('/receiving-orders', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取收货单详情
|
||||||
|
* @param id 收货单ID
|
||||||
|
*/
|
||||||
|
export function getReceivingDetail(id: string) {
|
||||||
|
return request.get<ReceivingOrder>(`/receiving-orders/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建收货单
|
||||||
|
* @param data 收货单数据
|
||||||
|
*/
|
||||||
|
export function createReceivingOrder(data: CreateReceivingOrderDto) {
|
||||||
|
return request.post<ReceivingOrder>('/receiving-orders', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新收货单
|
||||||
|
* @param id 收货单ID
|
||||||
|
* @param data 更新数据
|
||||||
|
*/
|
||||||
|
export function updateReceivingOrder(id: string, data: UpdateReceivingOrderDto) {
|
||||||
|
return request.put<ReceivingOrder>(`/receiving-orders/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认收货
|
||||||
|
*/
|
||||||
|
export function receive(data: {
|
||||||
|
receivingId: string
|
||||||
|
items: { skuCode: string; receivedQuantity: number }[]
|
||||||
|
receiver?: string
|
||||||
|
remark?: string
|
||||||
|
}) {
|
||||||
|
return request.post('/receiving-orders/receive', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 响应类型 ====================
|
||||||
|
|
||||||
|
export interface ReceivingListResponse {
|
||||||
|
list: ReceivingOrder[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
64
src/api/role.ts
Normal file
64
src/api/role.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色列表
|
||||||
|
*/
|
||||||
|
export function getRoleList() {
|
||||||
|
return request.get('/roles')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色详情
|
||||||
|
*/
|
||||||
|
export function getRoleDetail(id: string) {
|
||||||
|
return request.get(`/roles/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建角色
|
||||||
|
*/
|
||||||
|
export function createRole(data: any) {
|
||||||
|
return request.post('/roles', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新角色
|
||||||
|
*/
|
||||||
|
export function updateRole(id: string, data: any) {
|
||||||
|
return request.put(`/roles/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除角色
|
||||||
|
*/
|
||||||
|
export function deleteRole(id: string) {
|
||||||
|
return request.delete(`/roles/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色权限
|
||||||
|
*/
|
||||||
|
export function getRolePermissions(id: string) {
|
||||||
|
return request.get(`/roles/${id}/permissions`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分配角色权限
|
||||||
|
*/
|
||||||
|
export function assignRolePermissions(id: string, permissions: string[]) {
|
||||||
|
return request.put(`/roles/${id}/permissions`, { permissions })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取权限列表
|
||||||
|
*/
|
||||||
|
export function getPermissionList() {
|
||||||
|
return request.get('/permissions')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取权限分组
|
||||||
|
*/
|
||||||
|
export function getPermissionGroups() {
|
||||||
|
return request.get('/permissions/groups')
|
||||||
|
}
|
||||||
0
src/api/rule.ts
Normal file
0
src/api/rule.ts
Normal file
93
src/api/shop.ts
Normal file
93
src/api/shop.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// src/api/shop.ts
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 店铺数据
|
||||||
|
*/
|
||||||
|
export interface Shop {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
platform_id?: number
|
||||||
|
platform_name?: string
|
||||||
|
shop_id?: string
|
||||||
|
app_key?: string
|
||||||
|
app_secret?: string
|
||||||
|
access_token?: string
|
||||||
|
refresh_token?: string
|
||||||
|
token_expires_at?: string
|
||||||
|
status?: number
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台列表(店铺绑定用)
|
||||||
|
*/
|
||||||
|
export function getShopList(params?: any) {
|
||||||
|
return request.get('/shops/platforms', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取店铺详情
|
||||||
|
*/
|
||||||
|
export function getShopDetail(id: number) {
|
||||||
|
return request.get(`/shops/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取授权URL
|
||||||
|
*/
|
||||||
|
export function getAuthUrl(platformId: number) {
|
||||||
|
return request.post('/shops/auth-url', { platform_id: platformId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新店铺Token
|
||||||
|
*/
|
||||||
|
export function refreshShopToken(id: number) {
|
||||||
|
return request.post(`/shops/${id}/refresh`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除店铺
|
||||||
|
*/
|
||||||
|
export function deleteShop(id: number) {
|
||||||
|
return request.delete(`/shops/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有店铺
|
||||||
|
*/
|
||||||
|
export function getAllShops() {
|
||||||
|
return request.get('/shops/all')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台授权URL
|
||||||
|
*/
|
||||||
|
export function getPlatformAuthUrl(platformId: number) {
|
||||||
|
return getAuthUrl(platformId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取平台配置
|
||||||
|
*/
|
||||||
|
export function getPlatformConfig(platformId: number) {
|
||||||
|
return Promise.resolve({
|
||||||
|
code: 200,
|
||||||
|
message: '功能开发中',
|
||||||
|
data: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理授权回调
|
||||||
|
*/
|
||||||
|
export function handleAuthCallback(code: string) {
|
||||||
|
return Promise.resolve({
|
||||||
|
code: 200,
|
||||||
|
message: '功能开发中',
|
||||||
|
data: null
|
||||||
|
})
|
||||||
|
}
|
||||||
182
src/api/stock.ts
Normal file
182
src/api/stock.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
// src/api/stock.ts
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// ============ 类型定义 ============
|
||||||
|
|
||||||
|
/** 库存记录 - 对应后端返回字段(snake_case) */
|
||||||
|
export interface Stock {
|
||||||
|
id: number
|
||||||
|
sku_code: string
|
||||||
|
sku_name: string
|
||||||
|
warehouse_id: number
|
||||||
|
warehouse_name: string
|
||||||
|
quantity: number
|
||||||
|
locked_quantity: number
|
||||||
|
available_quantity: number
|
||||||
|
defective_quantity: number
|
||||||
|
warning_threshold: number
|
||||||
|
is_low: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 库存列表查询参数 */
|
||||||
|
export interface StockListParams {
|
||||||
|
warehouse_id?: number
|
||||||
|
sku_code?: string
|
||||||
|
keyword?: string
|
||||||
|
low_stock?: boolean
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 库存列表响应 */
|
||||||
|
export interface StockListResponse {
|
||||||
|
list: Stock[]
|
||||||
|
total: number
|
||||||
|
current_page: number
|
||||||
|
last_page: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 库存操作日志 */
|
||||||
|
export interface StockLog {
|
||||||
|
id: number
|
||||||
|
stock_id: number
|
||||||
|
type: 'inbound' | 'outbound' | 'adjust' | 'lock' | 'unlock' | 'defective_inbound' | 'defective_outbound'
|
||||||
|
quantity: number
|
||||||
|
before_quantity: number
|
||||||
|
after_quantity: number
|
||||||
|
reason?: string
|
||||||
|
operator?: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 库存详情响应 */
|
||||||
|
export interface StockDetailResponse {
|
||||||
|
stock: Stock
|
||||||
|
logs: StockLog[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 库存日志查询参数 */
|
||||||
|
export interface StockLogsParams {
|
||||||
|
stock_id?: number
|
||||||
|
sku_code?: string
|
||||||
|
warehouse_id?: number
|
||||||
|
type?: string
|
||||||
|
start_date?: string
|
||||||
|
end_date?: string
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新阈值请求 */
|
||||||
|
export interface UpdateThresholdData {
|
||||||
|
sku_code: string
|
||||||
|
warehouse_id: number
|
||||||
|
threshold: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动入库请求 */
|
||||||
|
export interface InboundData {
|
||||||
|
sku_code: string
|
||||||
|
warehouse_id: number
|
||||||
|
quantity: number
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动出库请求 */
|
||||||
|
export interface OutboundData {
|
||||||
|
sku_code: string
|
||||||
|
warehouse_id: number
|
||||||
|
quantity: number
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 不良品入库请求 */
|
||||||
|
export interface DefectiveInboundData {
|
||||||
|
sku_code: string
|
||||||
|
warehouse_id: number
|
||||||
|
quantity: number
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 库存调整请求 */
|
||||||
|
export interface AdjustData {
|
||||||
|
sku_code: string
|
||||||
|
warehouse_id: number
|
||||||
|
adjust_quantity: number
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ API 函数 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取库存列表
|
||||||
|
* @param params 查询参数(仓库ID、SKU、关键词、低库存筛选、分页)
|
||||||
|
*/
|
||||||
|
export function getStockList(params?: StockListParams) {
|
||||||
|
return request.get<StockListResponse>('/stocks', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取库存详情
|
||||||
|
* @param params { sku_code, warehouse_id }
|
||||||
|
*/
|
||||||
|
export function getStockDetail(params: { sku_code: string; warehouse_id: number }) {
|
||||||
|
return request.get<StockDetailResponse>('/stocks/detail', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取库存日志
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export function getStockLogs(params?: StockLogsParams) {
|
||||||
|
return request.get<{ list: StockLog[]; total: number }>('/stocks/logs', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新预警阈值
|
||||||
|
* @param data { sku_code, warehouse_id, threshold }
|
||||||
|
*/
|
||||||
|
export function updateThreshold(data: UpdateThresholdData) {
|
||||||
|
return request.put<void>('/stocks/update-threshold', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动入库
|
||||||
|
* @param data { sku_code, warehouse_id, quantity, reason }
|
||||||
|
*/
|
||||||
|
export function inbound(data: InboundData) {
|
||||||
|
return request.post<Stock>('/stocks/inbound', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动出库
|
||||||
|
* @param data { sku_code, warehouse_id, quantity, reason }
|
||||||
|
*/
|
||||||
|
export function outbound(data: OutboundData) {
|
||||||
|
return request.post<Stock>('/stocks/outbound', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不良品入库
|
||||||
|
* @param data { sku_code, warehouse_id, quantity, reason }
|
||||||
|
*/
|
||||||
|
export function defectiveInbound(data: DefectiveInboundData) {
|
||||||
|
return request.post<Stock>('/stocks/defective-inbound', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 库存调整
|
||||||
|
* @param data { sku_code, warehouse_id, adjust_quantity, reason }
|
||||||
|
*/
|
||||||
|
export function adjustStock(data: AdjustData) {
|
||||||
|
return request.post<Stock>('/stocks/adjust', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新库存预警阈值
|
||||||
|
*/
|
||||||
|
export function updateWarningThreshold(data: { sku_code: string; warehouse_id: number; threshold: number }) {
|
||||||
|
return request.put('/stocks/update-threshold', data)
|
||||||
|
}
|
||||||
67
src/api/supplier.ts
Normal file
67
src/api/supplier.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// src/api/supplier.ts
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商数据类型
|
||||||
|
*/
|
||||||
|
export interface Supplier {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
code?: string
|
||||||
|
contact?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
address?: string
|
||||||
|
status?: number
|
||||||
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商列表
|
||||||
|
* @param params 查询参数(page, limit, name, status)
|
||||||
|
*/
|
||||||
|
export function getSupplierList(params?: any) {
|
||||||
|
return request.get('/suppliers', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商详情
|
||||||
|
* @param id 供应商ID
|
||||||
|
*/
|
||||||
|
export function getSupplierDetail(id: number) {
|
||||||
|
return request.get(`/suppliers/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建供应商
|
||||||
|
* @param data 供应商数据
|
||||||
|
*/
|
||||||
|
export function createSupplier(data: Supplier) {
|
||||||
|
return request.post('/suppliers', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新供应商
|
||||||
|
* @param id 供应商ID
|
||||||
|
* @param data 更新数据
|
||||||
|
*/
|
||||||
|
export function updateSupplier(id: number, data: Partial<Supplier>) {
|
||||||
|
return request.put(`/suppliers/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除供应商
|
||||||
|
* @param id 供应商ID
|
||||||
|
*/
|
||||||
|
export function deleteSupplier(id: number) {
|
||||||
|
return request.delete(`/suppliers/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有供应商用于下拉选择
|
||||||
|
*/
|
||||||
|
export function getAllSuppliers() {
|
||||||
|
return request.get('/suppliers/all')
|
||||||
|
}
|
||||||
70
src/api/template.ts
Normal file
70
src/api/template.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// 模板管理 API
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板接口定义
|
||||||
|
*/
|
||||||
|
export interface Template {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
platform: string
|
||||||
|
size: string
|
||||||
|
style: string
|
||||||
|
defaultExpress?: string
|
||||||
|
senderName: string
|
||||||
|
senderPhone: string
|
||||||
|
senderAddress: string
|
||||||
|
showProduct: boolean
|
||||||
|
remark?: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板列表查询参数
|
||||||
|
*/
|
||||||
|
export interface TemplateListParams {
|
||||||
|
currentPage?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模板列表
|
||||||
|
* @param params 分页参数
|
||||||
|
*/
|
||||||
|
export function getTemplateList(params?: TemplateListParams) {
|
||||||
|
return request.get('/templates', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模板详情
|
||||||
|
* @param id 模板ID
|
||||||
|
*/
|
||||||
|
export function getTemplateDetail(id: string) {
|
||||||
|
return request.get(`/templates/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建模板
|
||||||
|
* @param data 模板数据
|
||||||
|
*/
|
||||||
|
export function createTemplate(data: Partial<Template>) {
|
||||||
|
return request.post('/templates', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新模板
|
||||||
|
* @param id 模板ID
|
||||||
|
* @param data 更新数据
|
||||||
|
*/
|
||||||
|
export function updateTemplate(id: string, data: Partial<Template>) {
|
||||||
|
return request.put(`/templates/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除模板
|
||||||
|
* @param id 模板ID
|
||||||
|
*/
|
||||||
|
export function deleteTemplate(id: string) {
|
||||||
|
return request.delete(`/templates/${id}`)
|
||||||
|
}
|
||||||
64
src/api/thirdParty.ts
Normal file
64
src/api/thirdParty.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取第三方配置
|
||||||
|
*/
|
||||||
|
export function getThirdPartyConfigs() {
|
||||||
|
return request.get('/third-party-config', { params: { group: 'third_party' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存第三方配置
|
||||||
|
*/
|
||||||
|
export function saveThirdPartyConfigs(configs: any[]) {
|
||||||
|
return request.put('/third-party-config', { group: 'third_party', configs })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个配置
|
||||||
|
*/
|
||||||
|
export function getConfig(key: string) {
|
||||||
|
return request.get(`/third-party-config/${key}`, { params: { group: 'third_party' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取邮件配置
|
||||||
|
*/
|
||||||
|
export function getMailConfig() {
|
||||||
|
return request.get('/third-party-config', { params: { group: 'mail' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存邮件配置
|
||||||
|
*/
|
||||||
|
export function saveMailConfig(configs: any[]) {
|
||||||
|
return request.put('/third-party-config', { group: 'mail', configs })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取短信配置
|
||||||
|
*/
|
||||||
|
export function getSmsConfig() {
|
||||||
|
return request.get('/third-party-config', { params: { group: 'sms' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存短信配置
|
||||||
|
*/
|
||||||
|
export function saveSmsConfig(configs: any[]) {
|
||||||
|
return request.put('/third-party-config', { group: 'sms', configs })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信配置
|
||||||
|
*/
|
||||||
|
export function getWechatConfig() {
|
||||||
|
return request.get('/third-party-config', { params: { group: 'wechat' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存微信配置
|
||||||
|
*/
|
||||||
|
export function saveWechatConfig(configs: any[]) {
|
||||||
|
return request.put('/third-party-config', { group: 'wechat', configs })
|
||||||
|
}
|
||||||
198
src/api/user.ts
Normal file
198
src/api/user.ts
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
// 用户管理 API
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户列表
|
||||||
|
*/
|
||||||
|
export function getUserList(params?: any) {
|
||||||
|
return request.get('/users', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户详情
|
||||||
|
*/
|
||||||
|
export function getUserDetail(id: string) {
|
||||||
|
return request.get(`/users/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建用户(添加制)
|
||||||
|
*/
|
||||||
|
export function createUser(data: any) {
|
||||||
|
return request.post('/users', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户
|
||||||
|
*/
|
||||||
|
export function updateUser(id: string, data: any) {
|
||||||
|
return request.put(`/users/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户
|
||||||
|
*/
|
||||||
|
export function deleteUser(id: string) {
|
||||||
|
return request.delete(`/users/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用用户
|
||||||
|
*/
|
||||||
|
export function disableUser(id: string) {
|
||||||
|
return request.post(`/users/${id}/disable`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用用户
|
||||||
|
*/
|
||||||
|
export function enableUser(id: string) {
|
||||||
|
return request.post(`/users/${id}/enable`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邀请用户(邀请制)
|
||||||
|
*/
|
||||||
|
export function inviteUser(data: any) {
|
||||||
|
return request.post('/users/invite', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户权限
|
||||||
|
*/
|
||||||
|
export function setUserPermissions(id: string, data: { permissions: string[], warehouseIds: number[] }) {
|
||||||
|
return request.put(`/users/${id}/permissions`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户权限
|
||||||
|
*/
|
||||||
|
export function getUserPermissions(id: string) {
|
||||||
|
return request.get(`/users/${id}/permissions`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户登录日志
|
||||||
|
*/
|
||||||
|
export function getUserLoginLogs(id: string, params?: any) {
|
||||||
|
if (id === 'me') {
|
||||||
|
return request.get('/user/login-logs', { params })
|
||||||
|
}
|
||||||
|
return request.get(`/users/${id}/login-logs`, { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户操作日志
|
||||||
|
*/
|
||||||
|
export function getUserOperationLogs(id: string, params?: any) {
|
||||||
|
return request.get(`/users/${id}/operation-logs`, { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息
|
||||||
|
*/
|
||||||
|
export function getCurrentUser() {
|
||||||
|
return request.get('/user/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新当前用户信息
|
||||||
|
*/
|
||||||
|
export function updateCurrentUser(data: any) {
|
||||||
|
return request.put('/user/profile', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
*/
|
||||||
|
export function changePassword(data: { oldPassword: string, newPassword: string, confirmPassword: string }) {
|
||||||
|
return request.put('/user/password', {
|
||||||
|
current_password: data.oldPassword,
|
||||||
|
new_password: data.newPassword,
|
||||||
|
new_password_confirmation: data.confirmPassword
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置密码(超管用)
|
||||||
|
*/
|
||||||
|
export function resetPassword(id: string) {
|
||||||
|
return request.post(`/users/${id}/reset-password`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送配对码邮件
|
||||||
|
*/
|
||||||
|
export function sendPairCode(data: { username: string, expireHours: number }) {
|
||||||
|
return request.post('/auth/send-pair-code', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证配对码
|
||||||
|
*/
|
||||||
|
export function verifyPairCode(data: { code: string }) {
|
||||||
|
return request.post('/auth/verify-pair-code', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备授信列表
|
||||||
|
*/
|
||||||
|
export function getTrustedDevices() {
|
||||||
|
return request.get('/user/devices')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消设备授信
|
||||||
|
*/
|
||||||
|
export function revokeDevice(deviceId: string) {
|
||||||
|
return request.delete(`/user/devices/${deviceId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批准设备申请
|
||||||
|
*/
|
||||||
|
export function approveDevice(deviceId: string) {
|
||||||
|
return request.post(`/user/devices/${deviceId}/approve`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝设备申请
|
||||||
|
*/
|
||||||
|
export function rejectDevice(deviceId: string) {
|
||||||
|
return request.post(`/users/devices/${deviceId}/reject`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定微信
|
||||||
|
*/
|
||||||
|
export function bindWechat(data: { code: string }) {
|
||||||
|
return request.post('/users/bind-wechat', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解绑微信
|
||||||
|
*/
|
||||||
|
export function unbindWechat() {
|
||||||
|
return request.delete('/users/bind-wechat')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信登录二维码
|
||||||
|
*/
|
||||||
|
export function getWechatQRCode() {
|
||||||
|
return request.get('/users/wechat/qrcode')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查微信登录状态
|
||||||
|
*/
|
||||||
|
export function checkWechatLogin(qrcodeId: string) {
|
||||||
|
return request.get(`/users/wechat/check/${qrcodeId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮箱修改验证码
|
||||||
|
*/
|
||||||
|
export function sendEmailChangeCode(data: { email: string }) {
|
||||||
|
return request.post('/user/send-email-change-code', data)
|
||||||
|
}
|
||||||
0
src/api/utils/mock.ts
Normal file
0
src/api/utils/mock.ts
Normal file
0
src/api/utils/permission.ts
Normal file
0
src/api/utils/permission.ts
Normal file
61
src/api/utils/request.ts
Normal file
61
src/api/utils/request.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// src/utils/request.ts
|
||||||
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
// import { useUserStore } from '@/stores/user'; // 后续引入
|
||||||
|
|
||||||
|
// 创建 axios 实例
|
||||||
|
const service: AxiosInstance = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // 环境变量配置
|
||||||
|
timeout: 15000, // 15秒超时
|
||||||
|
headers: { 'Content-Type': 'application/json;charset=UTF-8' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// TODO: 从 Pinia 获取 token 并注入 Header
|
||||||
|
// const userStore = useUserStore();
|
||||||
|
// if (userStore.token) {
|
||||||
|
// config.headers['Authorization'] = `Bearer ${userStore.token}`;
|
||||||
|
// }
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Request Error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
const res = response.data;
|
||||||
|
|
||||||
|
// 假设后端返回格式:{ code: 200, data: {}, msg: 'success' }
|
||||||
|
// 请根据实际后端接口调整判断逻辑
|
||||||
|
if (res.code !== 200 && res.code !== 0) {
|
||||||
|
ElMessage.error(res.msg || '系统错误');
|
||||||
|
|
||||||
|
// 特殊状态码处理:401 未登录,403 无权限
|
||||||
|
if (res.code === 401) {
|
||||||
|
// TODO: 跳转登录页
|
||||||
|
// userStore.logout();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.msg || 'Error'));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
(error: AxiosError) => {
|
||||||
|
console.error('Response Error:', error);
|
||||||
|
let msg = '网络异常,请稍后重试';
|
||||||
|
if (error.response?.status === 401) msg = '未授权,请重新登录';
|
||||||
|
if (error.response?.status === 403) msg = '拒绝访问';
|
||||||
|
if (error.response?.status === 404) msg = '资源不存在';
|
||||||
|
if (error.response?.status === 500) msg = '服务器内部错误';
|
||||||
|
|
||||||
|
ElMessage.error(msg);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default service;
|
||||||
130
src/api/warehouse.ts
Normal file
130
src/api/warehouse.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
// src/api/warehouse.ts
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// ============ 类型定义 ============
|
||||||
|
|
||||||
|
/** 仓库类型 */
|
||||||
|
export type WarehouseType = 'erp' | 'cloud' | 'physical' | 'virtual'
|
||||||
|
|
||||||
|
/** 仓库状态 */
|
||||||
|
export type WarehouseStatus = 'active' | 'inactive'
|
||||||
|
|
||||||
|
/** 仓库 - 对应后端返回字段(snake_case) */
|
||||||
|
export interface Warehouse {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
type: WarehouseType
|
||||||
|
cloud_system?: string | null
|
||||||
|
owner_code?: string | null
|
||||||
|
cloud_code?: string | null
|
||||||
|
app_key?: string | null
|
||||||
|
app_secret?: string | null
|
||||||
|
remark?: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仓库列表查询参数 */
|
||||||
|
export interface WarehouseListParams {
|
||||||
|
keyword?: string
|
||||||
|
type?: WarehouseType
|
||||||
|
status?: WarehouseStatus
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仓库列表响应 */
|
||||||
|
export interface WarehouseListResponse {
|
||||||
|
list: Warehouse[]
|
||||||
|
total: number
|
||||||
|
current_page: number
|
||||||
|
last_page: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建仓库请求 */
|
||||||
|
export interface CreateWarehouseData {
|
||||||
|
name: string
|
||||||
|
type: WarehouseType
|
||||||
|
cloud_system?: string
|
||||||
|
owner_code?: string
|
||||||
|
cloud_code?: string
|
||||||
|
app_key?: string
|
||||||
|
app_secret?: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新仓库请求 */
|
||||||
|
export interface UpdateWarehouseData {
|
||||||
|
name?: string
|
||||||
|
type?: WarehouseType
|
||||||
|
cloud_system?: string
|
||||||
|
owner_code?: string
|
||||||
|
cloud_code?: string
|
||||||
|
app_key?: string
|
||||||
|
app_secret?: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ API 函数 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取仓库列表
|
||||||
|
* @param params 查询参数(关键词、类型、分页)
|
||||||
|
*/
|
||||||
|
export function getWarehouseList(params?: WarehouseListParams) {
|
||||||
|
return request.get<WarehouseListResponse>('/warehouses', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取仓库详情
|
||||||
|
* @param id 仓库ID
|
||||||
|
*/
|
||||||
|
export function getWarehouseDetail(id: string | number) {
|
||||||
|
return request.get<Warehouse>(`/warehouses/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建仓库
|
||||||
|
* @param data 仓库数据
|
||||||
|
*/
|
||||||
|
export function createWarehouse(data: CreateWarehouseData) {
|
||||||
|
return request.post<Warehouse>('/warehouses', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新仓库
|
||||||
|
* @param id 仓库ID
|
||||||
|
* @param data 更新数据
|
||||||
|
*/
|
||||||
|
export function updateWarehouse(id: string | number, data: UpdateWarehouseData) {
|
||||||
|
return request.put<Warehouse>(`/warehouses/${id}`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除仓库
|
||||||
|
* @param id 仓库ID
|
||||||
|
*/
|
||||||
|
export function deleteWarehouse(id: string | number) {
|
||||||
|
return request.delete<void>(`/warehouses/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取仓库模板绑定列表
|
||||||
|
*/
|
||||||
|
export function getWarehouseBindings(warehouseId: string) {
|
||||||
|
return request.get(`/warehouse/bindings/${warehouseId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加仓库模板绑定
|
||||||
|
*/
|
||||||
|
export function addBinding(data: any) {
|
||||||
|
return request.post('/warehouse/bindings', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新仓库模板绑定
|
||||||
|
*/
|
||||||
|
export function updateBinding(id: string, data: any) {
|
||||||
|
return request.put(`/warehouse/bindings/${id}`, data)
|
||||||
|
}
|
||||||
0
src/api/wdt.ts
Normal file
0
src/api/wdt.ts
Normal file
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
41
src/components/HelloWorld.vue
Normal file
41
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
msg: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="greetings">
|
||||||
|
<h1 class="green">{{ msg }}</h1>
|
||||||
|
<h3>
|
||||||
|
You’ve successfully created a project with
|
||||||
|
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
position: relative;
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.greetings h1,
|
||||||
|
.greetings h3 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
53
src/components/PrintDialog.vue
Normal file
53
src/components/PrintDialog.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="批量打印" width="600px" :close-on-click-modal="false" @close="handleClose">
|
||||||
|
<div>
|
||||||
|
<p>这是一个独立的打印对话框组件</p>
|
||||||
|
<p>选中订单数:{{ orderIds.length }}</p>
|
||||||
|
<p>环境模式:{{ envMode }}</p>
|
||||||
|
<el-switch
|
||||||
|
v-model="envMode"
|
||||||
|
active-value="production"
|
||||||
|
inactive-value="simulate"
|
||||||
|
active-text="生产模式"
|
||||||
|
inactive-text="模拟模式"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
orderIds: number[]
|
||||||
|
orders: any[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const visible = ref(props.modelValue)
|
||||||
|
const envMode = ref<'simulate' | 'production'>('simulate')
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
visible.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(visible, (val) => {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
ElMessage.success('打印功能待完善')
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
95
src/components/TheWelcome.vue
Normal file
95
src/components/TheWelcome.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import WelcomeItem from './WelcomeItem.vue'
|
||||||
|
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||||
|
import ToolingIcon from './icons/IconTooling.vue'
|
||||||
|
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||||
|
import CommunityIcon from './icons/IconCommunity.vue'
|
||||||
|
import SupportIcon from './icons/IconSupport.vue'
|
||||||
|
|
||||||
|
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<DocumentationIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Documentation</template>
|
||||||
|
|
||||||
|
Vue’s
|
||||||
|
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||||
|
provides you with all information you need to get started.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<ToolingIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Tooling</template>
|
||||||
|
|
||||||
|
This project is served and bundled with
|
||||||
|
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||||
|
recommended IDE setup is
|
||||||
|
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||||
|
+
|
||||||
|
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener"
|
||||||
|
>Vue - Official</a
|
||||||
|
>. If you need to test your components and web pages, check out
|
||||||
|
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||||
|
and
|
||||||
|
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||||
|
/
|
||||||
|
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
More instructions are available in
|
||||||
|
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||||
|
>.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<EcosystemIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Ecosystem</template>
|
||||||
|
|
||||||
|
Get official tools and libraries for your project:
|
||||||
|
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||||
|
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||||
|
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||||
|
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||||
|
you need more resources, we suggest paying
|
||||||
|
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||||
|
a visit.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<CommunityIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Community</template>
|
||||||
|
|
||||||
|
Got stuck? Ask your question on
|
||||||
|
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||||
|
(our official Discord server), or
|
||||||
|
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||||
|
>StackOverflow</a
|
||||||
|
>. You should also follow the official
|
||||||
|
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||||
|
Bluesky account or the
|
||||||
|
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||||
|
X account for latest news in the Vue world.
|
||||||
|
</WelcomeItem>
|
||||||
|
|
||||||
|
<WelcomeItem>
|
||||||
|
<template #icon>
|
||||||
|
<SupportIcon />
|
||||||
|
</template>
|
||||||
|
<template #heading>Support Vue</template>
|
||||||
|
|
||||||
|
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||||
|
us by
|
||||||
|
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||||
|
</WelcomeItem>
|
||||||
|
</template>
|
||||||
87
src/components/WelcomeItem.vue
Normal file
87
src/components/WelcomeItem.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="item">
|
||||||
|
<i>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
</i>
|
||||||
|
<div class="details">
|
||||||
|
<h3>
|
||||||
|
<slot name="heading"></slot>
|
||||||
|
</h3>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item {
|
||||||
|
margin-top: 2rem;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
place-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: var(--color-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.item {
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
top: calc(50% - 25px);
|
||||||
|
left: -26px;
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:before {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:after {
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(50% + 25px);
|
||||||
|
height: calc(50% - 25px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:first-of-type:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:last-of-type:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
src/components/__tests__/HelloWorld.spec.ts
Normal file
11
src/components/__tests__/HelloWorld.spec.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import HelloWorld from '../HelloWorld.vue'
|
||||||
|
|
||||||
|
describe('HelloWorld', () => {
|
||||||
|
it('renders properly', () => {
|
||||||
|
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
|
||||||
|
expect(wrapper.text()).toContain('Hello Vitest')
|
||||||
|
})
|
||||||
|
})
|
||||||
0
src/components/base/ElPagination.vue
Normal file
0
src/components/base/ElPagination.vue
Normal file
0
src/components/base/ElTable.vue
Normal file
0
src/components/base/ElTable.vue
Normal file
0
src/components/base/SearchBar.vue
Normal file
0
src/components/base/SearchBar.vue
Normal file
7
src/components/icons/IconCommunity.vue
Normal file
7
src/components/icons/IconCommunity.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
src/components/icons/IconDocumentation.vue
Normal file
7
src/components/icons/IconDocumentation.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
src/components/icons/IconEcosystem.vue
Normal file
7
src/components/icons/IconEcosystem.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
src/components/icons/IconSupport.vue
Normal file
7
src/components/icons/IconSupport.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
19
src/components/icons/IconTooling.vue
Normal file
19
src/components/icons/IconTooling.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
class="iconify iconify--mdi"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
0
src/components/order/OrderActionBtn.vue
Normal file
0
src/components/order/OrderActionBtn.vue
Normal file
40
src/components/order/OrderStatusTag.vue
Normal file
40
src/components/order/OrderStatusTag.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<el-tag :type="tagType">
|
||||||
|
{{ statusText }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
status: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const statusMap = {
|
||||||
|
1: '待审核',
|
||||||
|
2: '已审核',
|
||||||
|
3: '待发货',
|
||||||
|
4: '已发货',
|
||||||
|
5: '已完成',
|
||||||
|
6: '已关闭'
|
||||||
|
}
|
||||||
|
return statusMap[props.status] || '未知状态'
|
||||||
|
})
|
||||||
|
|
||||||
|
const tagType = computed(() => {
|
||||||
|
const typeMap = {
|
||||||
|
1: 'warning',
|
||||||
|
2: 'success',
|
||||||
|
3: 'info',
|
||||||
|
4: 'primary',
|
||||||
|
5: 'success',
|
||||||
|
6: 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[props.status] || 'info'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
0
src/components/product/GoodsMatchPanel.vue
Normal file
0
src/components/product/GoodsMatchPanel.vue
Normal file
317
src/composables/usePrint.ts
Normal file
317
src/composables/usePrint.ts
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import { ref, reactive, onUnmounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
createPrintBatch,
|
||||||
|
getExpressList,
|
||||||
|
getTemplateList,
|
||||||
|
generateWaybill,
|
||||||
|
testPrinterConnection,
|
||||||
|
queryPrinterStatus,
|
||||||
|
connectPrinterStatus,
|
||||||
|
disconnectPrinterStatus,
|
||||||
|
onPrinterStatus,
|
||||||
|
type ExpressItem,
|
||||||
|
type TemplateItem,
|
||||||
|
type OrderItem,
|
||||||
|
type PrinterStatus
|
||||||
|
} from '@/api/print'
|
||||||
|
import { addOperationLog } from '@/api/log'
|
||||||
|
|
||||||
|
export function usePrint() {
|
||||||
|
// 打印机配置
|
||||||
|
const printerConfig = ref({ port: 16666, path: '' })
|
||||||
|
const testingPrinter = ref(false)
|
||||||
|
const printerTestResult = ref<boolean | null>(null)
|
||||||
|
const printerStatusMessage = ref('')
|
||||||
|
const printerStatusColor = ref('')
|
||||||
|
const printerReady = ref(false)
|
||||||
|
|
||||||
|
// 快递和模板数据
|
||||||
|
const expressList = ref<ExpressItem[]>([])
|
||||||
|
const templateList = ref<TemplateItem[]>([])
|
||||||
|
const loadingExpress = ref(false)
|
||||||
|
|
||||||
|
// 打印状态
|
||||||
|
const printing = ref(false)
|
||||||
|
const simulateMode = ref(false)
|
||||||
|
const printProgress = ref(0)
|
||||||
|
const printProgressStatus = ref<'success' | 'exception' | 'warning'>('success')
|
||||||
|
const totalPrintCount = ref(0)
|
||||||
|
const currentPrintIndex = ref(0)
|
||||||
|
const currentPrintOrder = ref<OrderItem | null>(null)
|
||||||
|
const printCompleted = ref(false)
|
||||||
|
const printResultTitle = ref('')
|
||||||
|
const printResultType = ref<'success' | 'warning' | 'error'>('success')
|
||||||
|
const printResultDesc = ref('')
|
||||||
|
const failedOrders = ref<Array<{ id: number; shortId: string; platformOrderSn: string; errorMsg: string }>>([])
|
||||||
|
const canStopPrint = ref(false)
|
||||||
|
|
||||||
|
// 打印机状态监控
|
||||||
|
let unsubscribePrinterStatus: (() => void) | null = null
|
||||||
|
|
||||||
|
// 加载快递和模板
|
||||||
|
const loadExpressAndTemplates = async () => {
|
||||||
|
loadingExpress.value = true
|
||||||
|
try {
|
||||||
|
const [expressRes, templateRes] = await Promise.all([
|
||||||
|
getExpressList(),
|
||||||
|
getTemplateList()
|
||||||
|
])
|
||||||
|
expressList.value = expressRes.data
|
||||||
|
templateList.value = templateRes.data
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取快递信息失败', error)
|
||||||
|
ElMessage.error('获取快递信息失败')
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
loadingExpress.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试打印机连接
|
||||||
|
const testConnection = async (port: number, path: string) => {
|
||||||
|
if (!port) {
|
||||||
|
ElMessage.warning('请输入端口号')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
testingPrinter.value = true
|
||||||
|
printerTestResult.value = null
|
||||||
|
printerStatusMessage.value = '正在连接...'
|
||||||
|
printerStatusColor.value = '#909399'
|
||||||
|
try {
|
||||||
|
const ok = await testPrinterConnection(port, path)
|
||||||
|
printerTestResult.value = ok
|
||||||
|
if (ok) {
|
||||||
|
printerStatusMessage.value = '连接成功,查询打印机状态...'
|
||||||
|
const status = await queryPrinterStatus(port, path)
|
||||||
|
if (status.ready) {
|
||||||
|
printerReady.value = true
|
||||||
|
printerStatusMessage.value = '打印机就绪'
|
||||||
|
printerStatusColor.value = '#67c23a'
|
||||||
|
ElMessage.success('打印机就绪')
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
printerReady.value = false
|
||||||
|
printerStatusMessage.value = `打印机异常:${status.error}`
|
||||||
|
printerStatusColor.value = '#e6a23c'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printerReady.value = false
|
||||||
|
printerStatusMessage.value = '连接失败,请检查端口'
|
||||||
|
printerStatusColor.value = '#f56c6c'
|
||||||
|
ElMessage.error('连接失败')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
printerReady.value = false
|
||||||
|
printerStatusMessage.value = '测试过程出错'
|
||||||
|
printerStatusColor.value = '#f56c6c'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
testingPrinter.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始批量打印
|
||||||
|
const startBatchPrint = async (
|
||||||
|
envMode: 'simulate' | 'production',
|
||||||
|
selectedOrderIds: number[],
|
||||||
|
orderList: OrderItem[],
|
||||||
|
printForm: { expressId: string; templateId: string; remark: string },
|
||||||
|
simulateModeValue: boolean
|
||||||
|
) => {
|
||||||
|
// 生产模式必须打印机就绪
|
||||||
|
if (envMode === 'production' && !printerReady.value) {
|
||||||
|
ElMessage.warning('请先测试打印机连接并确保就绪')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!printForm.expressId || !printForm.templateId) {
|
||||||
|
ElMessage.warning('请选择快递公司和模板')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
printing.value = true
|
||||||
|
printCompleted.value = false
|
||||||
|
printProgress.value = 0
|
||||||
|
printProgressStatus.value = 'success'
|
||||||
|
failedOrders.value = []
|
||||||
|
|
||||||
|
// 生产模式建立状态监控
|
||||||
|
if (envMode === 'production' && printerReady.value) {
|
||||||
|
connectPrinterStatus(printerConfig.value.port, printerConfig.value.path)
|
||||||
|
unsubscribePrinterStatus = onPrinterStatus((status: PrinterStatus) => {
|
||||||
|
if (status.type === 'error' && currentPrintOrder.value) {
|
||||||
|
ElMessage.error(`打印机异常:${status.message || ''}`)
|
||||||
|
failedOrders.value.push({
|
||||||
|
id: currentPrintOrder.value.id,
|
||||||
|
shortId: currentPrintOrder.value.shortId,
|
||||||
|
platformOrderSn: currentPrintOrder.value.platformOrderSn,
|
||||||
|
errorMsg: status.message || '打印机异常'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = { id: 1001, name: 'admin' } // 应从用户 store 获取
|
||||||
|
const batchRes = await createPrintBatch({
|
||||||
|
orderIds: selectedOrderIds,
|
||||||
|
remark: printForm.remark,
|
||||||
|
printUserId: user.id,
|
||||||
|
printUserName: user.name
|
||||||
|
})
|
||||||
|
if (batchRes.code !== 200) throw new Error('创建批次失败')
|
||||||
|
const batchNo = batchRes.data.batchNo
|
||||||
|
ElMessage.success(`批次【${batchNo}】已生成`)
|
||||||
|
|
||||||
|
await addOperationLog({
|
||||||
|
operateType: 'BATCH_PRINT',
|
||||||
|
operateDesc: `批量打印订单:${selectedOrderIds.join(',')},批次号:${batchNo}`,
|
||||||
|
result: 1,
|
||||||
|
userId: user.id,
|
||||||
|
userName: user.name
|
||||||
|
})
|
||||||
|
|
||||||
|
const ordersToPrint = orderList.filter(o => selectedOrderIds.includes(o.id))
|
||||||
|
totalPrintCount.value = ordersToPrint.length
|
||||||
|
let successCount = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < ordersToPrint.length; i++) {
|
||||||
|
const order = ordersToPrint[i]
|
||||||
|
currentPrintIndex.value = i
|
||||||
|
currentPrintOrder.value = order
|
||||||
|
printProgress.value = Math.round((i / totalPrintCount.value) * 100)
|
||||||
|
|
||||||
|
const seq = i + 1
|
||||||
|
const printTime = new Date().toLocaleString()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const waybillRes = await generateWaybill({
|
||||||
|
orderId: order.id,
|
||||||
|
expressId: printForm.expressId,
|
||||||
|
templateId: printForm.templateId,
|
||||||
|
seq,
|
||||||
|
batchNo,
|
||||||
|
printTime
|
||||||
|
})
|
||||||
|
if (waybillRes.code !== 200) throw new Error(waybillRes.message || '生成面单失败')
|
||||||
|
|
||||||
|
if (!simulateModeValue) {
|
||||||
|
const printWindow = window.open('', '_blank')
|
||||||
|
printWindow!.document.write(waybillRes.data.html)
|
||||||
|
printWindow!.document.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 假设 orderStore 有 shipOrder 方法,这里需要传入
|
||||||
|
// 由于无法直接访问 orderStore,我们通过参数传入更新函数
|
||||||
|
// 这里简化,由外部处理
|
||||||
|
// await orderStore.shipOrder(order.id)
|
||||||
|
successCount++
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`订单 ${order.id} 打印失败`, err)
|
||||||
|
failedOrders.value.push({
|
||||||
|
id: order.id,
|
||||||
|
shortId: order.shortId,
|
||||||
|
platformOrderSn: order.platformOrderSn,
|
||||||
|
errorMsg: err.message || '未知错误'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
}
|
||||||
|
|
||||||
|
printProgress.value = 100
|
||||||
|
currentPrintOrder.value = null
|
||||||
|
|
||||||
|
if (failedOrders.value.length === 0) {
|
||||||
|
printResultTitle.value = '打印完成'
|
||||||
|
printResultType.value = 'success'
|
||||||
|
printResultDesc.value = `成功打印 ${successCount} 个订单`
|
||||||
|
printProgressStatus.value = 'success'
|
||||||
|
} else if (successCount === 0) {
|
||||||
|
printResultTitle.value = '打印失败'
|
||||||
|
printResultType.value = 'error'
|
||||||
|
printResultDesc.value = `全部失败,共 ${failedOrders.value.length} 个`
|
||||||
|
printProgressStatus.value = 'exception'
|
||||||
|
} else {
|
||||||
|
printResultTitle.value = '部分成功'
|
||||||
|
printResultType.value = 'warning'
|
||||||
|
printResultDesc.value = `成功 ${successCount},失败 ${failedOrders.value.length}`
|
||||||
|
printProgressStatus.value = 'warning'
|
||||||
|
}
|
||||||
|
printCompleted.value = true
|
||||||
|
printing.value = false
|
||||||
|
return true
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('批量打印异常', err)
|
||||||
|
ElMessage.error(`打印失败:${err.message}`)
|
||||||
|
printProgressStatus.value = 'exception'
|
||||||
|
printing.value = false
|
||||||
|
printCompleted.value = true
|
||||||
|
printResultTitle.value = '打印异常'
|
||||||
|
printResultType.value = 'error'
|
||||||
|
printResultDesc.value = err.message || '未知错误'
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
if (unsubscribePrinterStatus) {
|
||||||
|
unsubscribePrinterStatus()
|
||||||
|
unsubscribePrinterStatus = null
|
||||||
|
}
|
||||||
|
disconnectPrinterStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态(打开对话框时调用)
|
||||||
|
const resetPrintState = (envMode: 'simulate' | 'production') => {
|
||||||
|
printerTestResult.value = null
|
||||||
|
printerStatusMessage.value = ''
|
||||||
|
printerReady.value = envMode === 'simulate'
|
||||||
|
printing.value = false
|
||||||
|
printCompleted.value = false
|
||||||
|
printProgress.value = 0
|
||||||
|
failedOrders.value = []
|
||||||
|
currentPrintIndex.value = 0
|
||||||
|
totalPrintCount.value = 0
|
||||||
|
currentPrintOrder.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unsubscribePrinterStatus) unsubscribePrinterStatus()
|
||||||
|
disconnectPrinterStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
printerConfig,
|
||||||
|
testingPrinter,
|
||||||
|
printerTestResult,
|
||||||
|
printerStatusMessage,
|
||||||
|
printerStatusColor,
|
||||||
|
printerReady,
|
||||||
|
expressList,
|
||||||
|
templateList,
|
||||||
|
loadingExpress,
|
||||||
|
printing,
|
||||||
|
printProgress,
|
||||||
|
printProgressStatus,
|
||||||
|
totalPrintCount,
|
||||||
|
currentPrintIndex,
|
||||||
|
currentPrintOrder,
|
||||||
|
printCompleted,
|
||||||
|
printResultTitle,
|
||||||
|
printResultType,
|
||||||
|
printResultDesc,
|
||||||
|
failedOrders,
|
||||||
|
canStopPrint,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
loadExpressAndTemplates,
|
||||||
|
testConnection,
|
||||||
|
startBatchPrint,
|
||||||
|
resetPrintState
|
||||||
|
}
|
||||||
|
}
|
||||||
496
src/composables/usePrintPlugin.ts
Normal file
496
src/composables/usePrintPlugin.ts
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
/**
|
||||||
|
* 打印插件调度中心 - 参考聚水潭JstErpKit架构
|
||||||
|
*
|
||||||
|
* 架构说明:
|
||||||
|
* - 本模块模拟 JstErpKit 的角色,作为打印任务的调度中心
|
||||||
|
* - 各第三方打印插件(如菜鸟、拼多多等)作为客户端主动连接
|
||||||
|
* - 前端通过 WebSocket 与本调度中心通信,调度打印任务
|
||||||
|
*
|
||||||
|
* 端口分配:
|
||||||
|
* - 本调度中心监听: 19876 端口
|
||||||
|
* - 各插件连接上来后,通过设备ID标识
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, reactive, computed, onUnmounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// ========== 类型定义 ==========
|
||||||
|
|
||||||
|
export interface PluginInfo {
|
||||||
|
pluginCode: string // 插件代码: 'cainiao', 'pdd', 'douyin', 'kuaishou', 'kuaimai', 'feiyun'
|
||||||
|
pluginName: string // 插件名称
|
||||||
|
version: string // 版本号
|
||||||
|
deviceId: string // 设备唯一ID
|
||||||
|
deviceName: string // 设备名称
|
||||||
|
status: 'online' | 'offline' | 'connecting' // 在线状态
|
||||||
|
lastHeartbeat: number // 最后心跳时间
|
||||||
|
capabilities: string[] // 支持的能力
|
||||||
|
port: number // 监听的本地端口
|
||||||
|
connection?: WebSocket // WebSocket连接
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrintJob {
|
||||||
|
id: string
|
||||||
|
orderId: number
|
||||||
|
platform: string
|
||||||
|
templateId: string
|
||||||
|
expressType: string
|
||||||
|
recipient: {
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
address: string
|
||||||
|
}
|
||||||
|
printData: Record<string, any>
|
||||||
|
status: 'pending' | 'printing' | 'completed' | 'failed'
|
||||||
|
createdAt: number
|
||||||
|
completedAt?: number
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginMessage {
|
||||||
|
action: string // 消息类型
|
||||||
|
deviceId?: string // 设备ID
|
||||||
|
pluginCode?: string // 插件代码
|
||||||
|
data?: any // 消息数据
|
||||||
|
timestamp?: number // 时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 调度中心类 ==========
|
||||||
|
|
||||||
|
class PrintScheduler {
|
||||||
|
// 调度中心配置
|
||||||
|
private readonly SCHEDULER_PORT = 19876
|
||||||
|
private readonly HEARTBEAT_INTERVAL = 30000 // 30秒心跳
|
||||||
|
private readonly HEARTBEAT_TIMEOUT = 60000 // 60秒超时判定离线
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
private serverSocket: WebSocketServer | null = null
|
||||||
|
private schedulerWs: WebSocket | null = null
|
||||||
|
private plugins: Map<string, PluginInfo> = new Map()
|
||||||
|
private printQueue: PrintJob[] = []
|
||||||
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
// 事件回调
|
||||||
|
public onPluginConnected?: (plugin: PluginInfo) => void
|
||||||
|
public onPluginDisconnected?: (plugin: PluginInfo) => void
|
||||||
|
public onPluginMessage?: (plugin: PluginInfo, message: PluginMessage) => void
|
||||||
|
public onPrintJobResult?: (job: PrintJob, success: boolean, result?: any) => void
|
||||||
|
|
||||||
|
// 公开状态
|
||||||
|
public isRunning = ref(false)
|
||||||
|
public connectedPlugins = computed(() => Array.from(this.plugins.values()))
|
||||||
|
public onlinePlugins = computed(() => this.connectedPlugins.value.filter(p => p.status === 'online'))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动调度中心(作为WebSocket服务器)
|
||||||
|
*/
|
||||||
|
async start(): Promise<boolean> {
|
||||||
|
if (this.isRunning.value) {
|
||||||
|
console.log('[调度中心] 已经启动')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[调度中心] 启动中,监听端口 ${this.SCHEDULER_PORT}...`)
|
||||||
|
|
||||||
|
// 注意:由于浏览器限制,我们使用模拟模式
|
||||||
|
// 实际生产环境应该使用 Electron 或 Node.js 后台服务
|
||||||
|
this.startMockMode()
|
||||||
|
|
||||||
|
// 启动心跳检测
|
||||||
|
this.startHeartbeatCheck()
|
||||||
|
|
||||||
|
this.isRunning.value = true
|
||||||
|
console.log('[调度中心] 启动成功')
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[调度中心] 启动失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止调度中心
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer)
|
||||||
|
this.heartbeatTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断开所有插件连接
|
||||||
|
this.plugins.forEach(plugin => {
|
||||||
|
if (plugin.connection) {
|
||||||
|
plugin.connection.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.plugins.clear()
|
||||||
|
|
||||||
|
if (this.schedulerWs) {
|
||||||
|
this.schedulerWs.close()
|
||||||
|
this.schedulerWs = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRunning.value = false
|
||||||
|
console.log('[调度中心] 已停止')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟模式 - 模拟第三方插件连接
|
||||||
|
* 用于测试和开发环境
|
||||||
|
*/
|
||||||
|
private startMockMode(): void {
|
||||||
|
console.log('[调度中心] 模拟模式启动')
|
||||||
|
|
||||||
|
// 模拟一些已连接的插件
|
||||||
|
const mockPlugins: PluginInfo[] = [
|
||||||
|
{
|
||||||
|
pluginCode: 'cainiao',
|
||||||
|
pluginName: '菜鸟打印组件',
|
||||||
|
version: '2.5.1',
|
||||||
|
deviceId: 'mock-cainiao-' + Date.now(),
|
||||||
|
deviceName: '菜鸟云打印机',
|
||||||
|
status: 'online',
|
||||||
|
lastHeartbeat: Date.now(),
|
||||||
|
capabilities: ['print', 'preview', 'queryStatus'],
|
||||||
|
port: 13528
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pluginCode: 'pdd',
|
||||||
|
pluginName: '拼多多打印组件',
|
||||||
|
version: '1.8.6',
|
||||||
|
deviceId: 'mock-pdd-' + Date.now(),
|
||||||
|
deviceName: '拼多多专打印机',
|
||||||
|
status: 'online',
|
||||||
|
lastHeartbeat: Date.now(),
|
||||||
|
capabilities: ['print', 'preview'],
|
||||||
|
port: 5000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
mockPlugins.forEach(plugin => {
|
||||||
|
this.plugins.set(plugin.deviceId, plugin)
|
||||||
|
this.onPluginConnected?.(plugin)
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success(`调度中心启动,已发现 ${mockPlugins.length} 个在线打印插件(模拟)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟第三方插件连接(模拟JstErpKit发现插件的过程)
|
||||||
|
*/
|
||||||
|
simulatePluginConnect(pluginInfo: Partial<PluginInfo>): string {
|
||||||
|
const deviceId = pluginInfo.deviceId || `plugin-${Date.now()}`
|
||||||
|
const plugin: PluginInfo = {
|
||||||
|
pluginCode: pluginInfo.pluginCode || 'unknown',
|
||||||
|
pluginName: pluginInfo.pluginName || '未知插件',
|
||||||
|
version: pluginInfo.version || '1.0.0',
|
||||||
|
deviceId,
|
||||||
|
deviceName: pluginInfo.deviceName || '打印设备',
|
||||||
|
status: 'online',
|
||||||
|
lastHeartbeat: Date.now(),
|
||||||
|
capabilities: pluginInfo.capabilities || ['print'],
|
||||||
|
port: pluginInfo.port || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plugins.set(deviceId, plugin)
|
||||||
|
console.log(`[调度中心] 插件连接: ${plugin.pluginName} (${deviceId})`)
|
||||||
|
|
||||||
|
return deviceId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟插件断开
|
||||||
|
*/
|
||||||
|
simulatePluginDisconnect(deviceId: string): void {
|
||||||
|
const plugin = this.plugins.get(deviceId)
|
||||||
|
if (plugin) {
|
||||||
|
plugin.status = 'offline'
|
||||||
|
this.plugins.delete(deviceId)
|
||||||
|
this.onPluginDisconnected?.(plugin)
|
||||||
|
console.log(`[调度中心] 插件断开: ${plugin.pluginName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送心跳
|
||||||
|
*/
|
||||||
|
private startHeartbeatCheck(): void {
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
this.plugins.forEach((plugin, deviceId) => {
|
||||||
|
// 检查心跳超时
|
||||||
|
if (now - plugin.lastHeartbeat > this.HEARTBEAT_TIMEOUT) {
|
||||||
|
if (plugin.status === 'online') {
|
||||||
|
plugin.status = 'offline'
|
||||||
|
console.log(`[调度中心] 插件超时离线: ${plugin.pluginName}`)
|
||||||
|
this.onPluginDisconnected?.(plugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, this.HEARTBEAT_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理来自插件的消息
|
||||||
|
*/
|
||||||
|
handlePluginMessage(deviceId: string, message: PluginMessage): void {
|
||||||
|
const plugin = this.plugins.get(deviceId)
|
||||||
|
if (!plugin) {
|
||||||
|
console.warn(`[调度中心] 未知设备消息: ${deviceId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.action) {
|
||||||
|
case 'register':
|
||||||
|
// 插件注册
|
||||||
|
this.handlePluginRegister(plugin, message)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'heartbeat':
|
||||||
|
// 心跳
|
||||||
|
plugin.lastHeartbeat = Date.now()
|
||||||
|
if (plugin.status !== 'online') {
|
||||||
|
plugin.status = 'online'
|
||||||
|
this.onPluginConnected?.(plugin)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'printComplete':
|
||||||
|
// 打印完成
|
||||||
|
this.handlePrintComplete(plugin, message)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'printError':
|
||||||
|
// 打印失败
|
||||||
|
this.handlePrintError(plugin, message)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'statusUpdate':
|
||||||
|
// 状态更新
|
||||||
|
console.log(`[调度中心] ${plugin.pluginName} 状态更新:`, message.data)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`[调度中心] 未知消息类型: ${message.action}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onPluginMessage?.(plugin, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理插件注册
|
||||||
|
*/
|
||||||
|
private handlePluginRegister(plugin: PluginInfo, message: PluginMessage): void {
|
||||||
|
const data = message.data || {}
|
||||||
|
plugin.version = data.version || plugin.version
|
||||||
|
plugin.capabilities = data.capabilities || plugin.capabilities
|
||||||
|
plugin.status = 'online'
|
||||||
|
plugin.lastHeartbeat = Date.now()
|
||||||
|
|
||||||
|
console.log(`[调度中心] 插件注册: ${plugin.pluginName} v${plugin.version}`)
|
||||||
|
|
||||||
|
// 发送注册响应
|
||||||
|
this.sendToPlugin(plugin.deviceId, {
|
||||||
|
action: 'registerAck',
|
||||||
|
data: {
|
||||||
|
schedulerVersion: '1.0.0',
|
||||||
|
registered: true,
|
||||||
|
deviceId: plugin.deviceId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理打印完成
|
||||||
|
*/
|
||||||
|
private handlePrintComplete(plugin: PluginInfo, message: PluginMessage): void {
|
||||||
|
const { jobId, expressNo, success } = message.data || {}
|
||||||
|
const job = this.printQueue.find(j => j.id === jobId)
|
||||||
|
|
||||||
|
if (job) {
|
||||||
|
job.status = success ? 'completed' : 'failed'
|
||||||
|
job.completedAt = Date.now()
|
||||||
|
if (!success) {
|
||||||
|
job.error = '打印失败'
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[调度中心] 打印${success ? '成功' : '失败'}: ${jobId} - ${expressNo || ''}`)
|
||||||
|
this.onPrintJobResult?.(job, success, { expressNo })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理打印错误
|
||||||
|
*/
|
||||||
|
private handlePrintError(plugin: PluginInfo, message: PluginMessage): void {
|
||||||
|
const { jobId, error, errorCode } = message.data || {}
|
||||||
|
const job = this.printQueue.find(j => j.id === jobId)
|
||||||
|
|
||||||
|
if (job) {
|
||||||
|
job.status = 'failed'
|
||||||
|
job.completedAt = Date.now()
|
||||||
|
job.error = error || '未知错误'
|
||||||
|
|
||||||
|
console.error(`[调度中心] 打印错误: ${jobId} - ${error} (${errorCode})`)
|
||||||
|
this.onPrintJobResult?.(job, false, { error, errorCode })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息给插件
|
||||||
|
*/
|
||||||
|
sendToPlugin(deviceId: string, message: PluginMessage): boolean {
|
||||||
|
const plugin = this.plugins.get(deviceId)
|
||||||
|
if (!plugin || !plugin.connection) {
|
||||||
|
console.warn(`[调度中心] 无法发送消息给 ${deviceId}: 插件未连接`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
plugin.connection.send(JSON.stringify(message))
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[调度中心] 发送消息失败:`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交打印任务
|
||||||
|
*/
|
||||||
|
submitPrintJob(job: Omit<PrintJob, 'id' | 'status' | 'createdAt'>): string {
|
||||||
|
const fullJob: PrintJob = {
|
||||||
|
...job,
|
||||||
|
id: `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.printQueue.push(fullJob)
|
||||||
|
console.log(`[调度中心] 收到打印任务: ${fullJob.id}`)
|
||||||
|
|
||||||
|
// 分发给对应平台的插件
|
||||||
|
this.dispatchJob(fullJob)
|
||||||
|
|
||||||
|
return fullJob.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分发打印任务给插件
|
||||||
|
*/
|
||||||
|
private dispatchJob(job: PrintJob): void {
|
||||||
|
// 查找对应平台的在线插件
|
||||||
|
const plugin = Array.from(this.plugins.values()).find(
|
||||||
|
p => p.pluginCode === job.platform && p.status === 'online'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!plugin) {
|
||||||
|
console.warn(`[调度中心] 没有找到 ${job.platform} 的在线插件`)
|
||||||
|
job.status = 'failed'
|
||||||
|
job.error = '没有在线的打印插件'
|
||||||
|
job.completedAt = Date.now()
|
||||||
|
this.onPrintJobResult?.(job, false, { error: '没有在线的打印插件' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务状态
|
||||||
|
job.status = 'printing'
|
||||||
|
|
||||||
|
// 发送打印指令给插件
|
||||||
|
const sent = this.sendToPlugin(plugin.deviceId, {
|
||||||
|
action: 'print',
|
||||||
|
data: {
|
||||||
|
jobId: job.id,
|
||||||
|
orderId: job.orderId,
|
||||||
|
platform: job.platform,
|
||||||
|
templateId: job.templateId,
|
||||||
|
expressType: job.expressType,
|
||||||
|
recipient: job.recipient,
|
||||||
|
printData: job.printData
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!sent) {
|
||||||
|
job.status = 'failed'
|
||||||
|
job.error = '发送打印指令失败'
|
||||||
|
job.completedAt = Date.now()
|
||||||
|
this.onPrintJobResult?.(job, false, { error: '发送打印指令失败' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取打印队列
|
||||||
|
*/
|
||||||
|
getPrintQueue(): PrintJob[] {
|
||||||
|
return [...this.printQueue]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取在线插件列表
|
||||||
|
*/
|
||||||
|
getOnlinePlugins(): PluginInfo[] {
|
||||||
|
return this.onlinePlugins.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连接指定端口的打印服务
|
||||||
|
*/
|
||||||
|
async testConnection(host: string, port: number): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 模拟连接测试
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(`[调度中心] 测试连接 ${host}:${port}`)
|
||||||
|
resolve(true) // 模拟返回成功
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const printScheduler = new PrintScheduler()
|
||||||
|
|
||||||
|
// ========== Vue Composables ==========
|
||||||
|
|
||||||
|
export function usePrintScheduler() {
|
||||||
|
const isRunning = printScheduler.isRunning
|
||||||
|
const plugins = printScheduler.connectedPlugins
|
||||||
|
const onlinePlugins = printScheduler.onlinePlugins
|
||||||
|
|
||||||
|
const start = () => printScheduler.start()
|
||||||
|
const stop = () => printScheduler.stop()
|
||||||
|
|
||||||
|
const submitJob = (job: Omit<PrintJob, 'id' | 'status' | 'createdAt'>) => {
|
||||||
|
return printScheduler.submitPrintJob(job)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getQueue = () => printScheduler.getPrintQueue()
|
||||||
|
|
||||||
|
const testConnection = (host: string, port: number) => {
|
||||||
|
return printScheduler.testConnection(host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟插件连接(用于测试)
|
||||||
|
const simulateConnect = (pluginInfo: Partial<PluginInfo>) => {
|
||||||
|
return printScheduler.simulatePluginConnect(pluginInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
const simulateDisconnect = (deviceId: string) => {
|
||||||
|
printScheduler.simulatePluginDisconnect(deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRunning,
|
||||||
|
plugins,
|
||||||
|
onlinePlugins,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
submitJob,
|
||||||
|
getQueue,
|
||||||
|
testConnection,
|
||||||
|
simulateConnect,
|
||||||
|
simulateDisconnect
|
||||||
|
}
|
||||||
|
}
|
||||||
658
src/layouts/MainLayout.vue
Normal file
658
src/layouts/MainLayout.vue
Normal file
@ -0,0 +1,658 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main-layout" @mousemove="handleMouseActivity">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<aside class="sidebar" :class="{ collapsed: isCollapsed }">
|
||||||
|
<!-- Logo区域 -->
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="logo-area">
|
||||||
|
<div class="logo-icon">
|
||||||
|
<svg viewBox="0 0 100 100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="mainLogoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d4af37"/>
|
||||||
|
<stop offset="100%" style="stop-color:#f5e6a3"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="50" cy="50" r="40" fill="none" stroke="url(#mainLogoGrad)" stroke-width="3"/>
|
||||||
|
<path d="M30 50 L45 65 L70 35" fill="none" stroke="url(#mainLogoGrad)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="logo-text" v-show="!isCollapsed">ERP管理系统</span>
|
||||||
|
</div>
|
||||||
|
<button class="collapse-btn" @click="toggleCollapse">
|
||||||
|
<span class="collapse-icon">{{ isCollapsed ? '→' : '←' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 菜单区域 -->
|
||||||
|
<nav class="sidebar-menu">
|
||||||
|
<!-- 订单管理 -->
|
||||||
|
<div class="menu-section">
|
||||||
|
<div class="menu-section-title" v-show="!isCollapsed">📋 订单</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/order') }" @click="navigateTo('/order/list')">
|
||||||
|
<span class="menu-icon">📋</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">订单列表</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 商品管理 -->
|
||||||
|
<div class="menu-section">
|
||||||
|
<div class="menu-section-title" v-show="!isCollapsed">📦 商品</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/goods') }" @click="navigateTo('/goods/list')">
|
||||||
|
<span class="menu-icon">📦</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">商品列表</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/goods/platform') }" @click="navigateTo('/goods/platform')">
|
||||||
|
<span class="menu-icon">🌐</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">平台商品</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/goods/push-log') }" @click="navigateTo('/goods/push-log')">
|
||||||
|
<span class="menu-icon">📤</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">推送日志</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 仓库管理 -->
|
||||||
|
<div class="menu-section">
|
||||||
|
<div class="menu-section-title" v-show="!isCollapsed">🏪 仓库</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/warehouse') && !isActive('/warehouse/stock') && !isActive('/warehouse/purchase') && !isActive('/warehouse/receiving') }" @click="navigateTo('/warehouse')">
|
||||||
|
<span class="menu-icon">🏪</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">仓库列表</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/warehouse/stock') }" @click="navigateTo('/warehouse/stock')">
|
||||||
|
<span class="menu-icon">📊</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">库存管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/warehouse/purchase') }" @click="navigateTo('/warehouse/purchase')">
|
||||||
|
<span class="menu-icon">🛒</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">采购管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/warehouse/receiving') }" @click="navigateTo('/warehouse/receiving')">
|
||||||
|
<span class="menu-icon">📥</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">收货管理</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 打印管理 -->
|
||||||
|
<div class="menu-section">
|
||||||
|
<div class="menu-section-title" v-show="!isCollapsed">🖨️ 打印</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/print/batch') }" @click="navigateTo('/print/batch')">
|
||||||
|
<span class="menu-icon">📑</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">批量打印</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/print/log') }" @click="navigateTo('/print/log')">
|
||||||
|
<span class="menu-icon">📋</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">打印日志</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/print/plugin') }" @click="navigateTo('/print/plugin')">
|
||||||
|
<span class="menu-icon">🔌</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">插件管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/print/queue') }" @click="navigateTo('/print/queue')">
|
||||||
|
<span class="menu-icon">📜</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">打印队列</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/print/template') }" @click="navigateTo('/print/template-library/list')">
|
||||||
|
<span class="menu-icon">📄</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">模板列表</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 平台管理 -->
|
||||||
|
<div class="menu-section">
|
||||||
|
<div class="menu-section-title" v-show="!isCollapsed">🏬 平台</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/platform/shops') }" @click="navigateTo('/platform/shops')">
|
||||||
|
<span class="menu-icon">🏬</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">店铺管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/platform/waybill') }" @click="navigateTo('/platform/waybill-accounts')">
|
||||||
|
<span class="menu-icon">📮</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">运单账号</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统设置 -->
|
||||||
|
<div class="menu-section">
|
||||||
|
<div class="menu-section-title" v-show="!isCollapsed">⚙️ 系统</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/settings/user') }" @click="navigateTo('/settings/user')">
|
||||||
|
<span class="menu-icon">👥</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">用户管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/settings/role') }" @click="navigateTo('/settings/role')">
|
||||||
|
<span class="menu-icon">🔐</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">角色权限</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/settings/brand') }" @click="navigateTo('/settings/brand')">
|
||||||
|
<span class="menu-icon">🏷️</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">品牌管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/settings/supplier') }" @click="navigateTo('/settings/supplier')">
|
||||||
|
<span class="menu-icon">🤝</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">供应商管理</span>
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: isActive('/settings/operation-log') }" @click="navigateTo('/settings/operation-log')">
|
||||||
|
<span class="menu-icon">📝</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">操作日志</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 底部用户信息 -->
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="user-card" @click="openPersonalCenter">
|
||||||
|
<div class="user-avatar">👤</div>
|
||||||
|
<div class="user-info" v-show="!isCollapsed">
|
||||||
|
<span class="user-name">{{ currentUser.name || '管理员' }}</span>
|
||||||
|
<span class="user-role">{{ currentUser.role === 'admin' ? '系统管理员' : '普通用户' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="logout-row">
|
||||||
|
<button class="logout-btn" @click="openPersonalCenter" :title="'个人中心'">
|
||||||
|
<span class="logout-icon">👤</span>
|
||||||
|
</button>
|
||||||
|
<button class="logout-btn" @click="handleLogout" :title="'退出登录'">
|
||||||
|
<span class="logout-icon">🚪</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<span class="breadcrumb-item">首页</span>
|
||||||
|
<span class="breadcrumb-sep">/</span>
|
||||||
|
<span class="breadcrumb-item active">{{ currentPageName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="action-item" title="通知">
|
||||||
|
<span class="action-icon">🔔</span>
|
||||||
|
</div>
|
||||||
|
<div class="action-item" title="设置">
|
||||||
|
<span class="action-icon">⚙️</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 页面内容 -->
|
||||||
|
<main class="page-content">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const isCollapsed = ref(false)
|
||||||
|
|
||||||
|
// 自动折叠定时器
|
||||||
|
let collapseTimer = null
|
||||||
|
const AUTO_COLLAPSE_DELAY = 5000 // 5秒后自动折叠
|
||||||
|
|
||||||
|
const startCollapseTimer = () => {
|
||||||
|
clearTimeout(collapseTimer)
|
||||||
|
if (!isCollapsed.value) {
|
||||||
|
collapseTimer = setTimeout(() => {
|
||||||
|
isCollapsed.value = true
|
||||||
|
}, AUTO_COLLAPSE_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopCollapseTimer = () => {
|
||||||
|
clearTimeout(collapseTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUser = ref(JSON.parse(localStorage.getItem('current_user') || '{"name":"管理员","role":"admin"}'))
|
||||||
|
|
||||||
|
const menuRoutes = [
|
||||||
|
{ name: '欢迎页', path: '/welcome', icon: '🏠' },
|
||||||
|
{ name: '订单列表', path: '/order/list', icon: '📋' },
|
||||||
|
{ name: '售后管理', path: '/after-sale', icon: '🔧' },
|
||||||
|
{ name: '商品列表', path: '/goods/list', icon: '📦' },
|
||||||
|
{ name: '平台商品', path: '/goods/platform', icon: '🌐' },
|
||||||
|
{ name: '推送日志', path: '/goods/push-log', icon: '📤' },
|
||||||
|
{ name: '仓库列表', path: '/warehouse', icon: '🏪' },
|
||||||
|
{ name: '库存管理', path: '/warehouse/stock', icon: '📊' },
|
||||||
|
{ name: '采购管理', path: '/warehouse/purchase', icon: '🛒' },
|
||||||
|
{ name: '收货管理', path: '/warehouse/receiving', icon: '📥' },
|
||||||
|
{ name: '批量打印', path: '/print/batch', icon: '📑' },
|
||||||
|
{ name: '打印日志', path: '/print/log', icon: '📋' },
|
||||||
|
{ name: '插件管理', path: '/print/plugin', icon: '🔌' },
|
||||||
|
{ name: '打印队列', path: '/print/queue', icon: '📜' },
|
||||||
|
{ name: '模板列表', path: '/print/template-library/list', icon: '📄' },
|
||||||
|
{ name: '模板设计', path: '/print/template-library/design', icon: '🎨' },
|
||||||
|
{ name: '店铺管理', path: '/platform/shops', icon: '🏬' },
|
||||||
|
{ name: '运单账号', path: '/platform/waybill-accounts', icon: '📮' },
|
||||||
|
{ name: '用户管理', path: '/settings/user', icon: '👥' },
|
||||||
|
{ name: '角色权限', path: '/settings/role', icon: '🔐' },
|
||||||
|
{ name: '品牌管理', path: '/settings/brand', icon: '🏷️' },
|
||||||
|
{ name: '供应商管理', path: '/settings/supplier', icon: '🤝' },
|
||||||
|
{ name: '操作日志', path: '/settings/operation-log', icon: '📝' },
|
||||||
|
{ name: '个人中心', path: '/personal-center', icon: '👤' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentPageName = computed(() => {
|
||||||
|
const path = route.path
|
||||||
|
const item = menuRoutes.find(r => r.path === path)
|
||||||
|
return item ? item.name : '欢迎页'
|
||||||
|
})
|
||||||
|
|
||||||
|
const isActive = (path) => {
|
||||||
|
if (path === '/warehouse') {
|
||||||
|
return route.path === '/warehouse' || route.path === '/warehouse/list'
|
||||||
|
}
|
||||||
|
return route.path.startsWith(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCollapse = () => {
|
||||||
|
isCollapsed.value = !isCollapsed.value
|
||||||
|
if (!isCollapsed.value) {
|
||||||
|
// 展开时:2秒后自动折叠
|
||||||
|
startCollapseTimer()
|
||||||
|
} else {
|
||||||
|
// 折叠时:停止定时器
|
||||||
|
stopCollapseTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标移动时重置计时器(用户活跃时保持展开)
|
||||||
|
const handleMouseActivity = () => {
|
||||||
|
if (!isCollapsed.value) {
|
||||||
|
startCollapseTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateTo = (path) => {
|
||||||
|
// 如果侧边栏是折叠的,先展开
|
||||||
|
if (isCollapsed.value) {
|
||||||
|
isCollapsed.value = false
|
||||||
|
startCollapseTimer()
|
||||||
|
}
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
// 启动自动折叠计时器
|
||||||
|
startCollapseTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopCollapseTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打开个人中心
|
||||||
|
const openPersonalCenter = () => {
|
||||||
|
if (isCollapsed.value) {
|
||||||
|
isCollapsed.value = false
|
||||||
|
startCollapseTimer()
|
||||||
|
}
|
||||||
|
router.push('/personal-center')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
localStorage.removeItem('erp_token')
|
||||||
|
localStorage.removeItem('current_user')
|
||||||
|
router.push('/login')
|
||||||
|
ElMessage.success('退出登录成功!')
|
||||||
|
} catch {
|
||||||
|
ElMessage.info('已取消退出登录')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main-layout {
|
||||||
|
display: flex;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
width: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏头部 */
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn:hover {
|
||||||
|
background: rgba(212, 175, 55, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
color: #d4af37;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单 */
|
||||||
|
.sidebar-menu {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-section-title {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
background: linear-gradient(135deg, rgba(212, 175, 55, 0.2), rgba(212, 175, 55, 0.1));
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 24px;
|
||||||
|
background: #d4af37;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏底部 */
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: linear-gradient(135deg, #d4af37, #f5e6a3);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
background: rgba(220, 53, 69, 0.2);
|
||||||
|
border-color: rgba(220, 53, 69, 0.3);
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区 */
|
||||||
|
.main-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 240px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: margin-left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed ~ .main-wrapper {
|
||||||
|
margin-left: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航 */
|
||||||
|
.top-header {
|
||||||
|
height: 64px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 240px;
|
||||||
|
right: 0;
|
||||||
|
z-index: 50;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed ~ .main-wrapper .top-header,
|
||||||
|
.sidebar.collapsed + .main-wrapper .top-header {
|
||||||
|
left: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: #1a1a2e;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-sep {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面内容 */
|
||||||
|
.page-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 64px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
transform: translateX(0);
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed + .main-wrapper {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
495
src/layouts/Sidebar.vue
Normal file
495
src/layouts/Sidebar.vue
Normal file
@ -0,0 +1,495 @@
|
|||||||
|
<template>
|
||||||
|
<div class="layout-wrapper">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<aside class="sidebar" :class="{ collapsed: isCollapsed }">
|
||||||
|
<!-- Logo区域 -->
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="logo-area">
|
||||||
|
<div class="logo-icon">
|
||||||
|
<svg viewBox="0 0 100 100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="sidebarLogoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d4af37"/>
|
||||||
|
<stop offset="100%" style="stop-color:#f5e6a3"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="50" cy="50" r="40" fill="none" stroke="url(#sidebarLogoGrad)" stroke-width="3"/>
|
||||||
|
<path d="M30 50 L45 65 L70 35" fill="none" stroke="url(#sidebarLogoGrad)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="logo-text" v-show="!isCollapsed">ERP管理系统</span>
|
||||||
|
</div>
|
||||||
|
<button class="collapse-btn" @click="toggleCollapse">
|
||||||
|
<span class="collapse-icon">{{ isCollapsed ? '→' : '←' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 菜单区域 -->
|
||||||
|
<nav class="sidebar-menu">
|
||||||
|
<div class="menu-group" v-for="group in menuGroups" :key="group.title">
|
||||||
|
<div class="menu-group-title" v-show="!isCollapsed">{{ group.title }}</div>
|
||||||
|
<div
|
||||||
|
class="menu-item"
|
||||||
|
:class="{ active: isActive(item.path) }"
|
||||||
|
v-for="item in group.items"
|
||||||
|
:key="item.path"
|
||||||
|
@click="navigateTo(item.path)"
|
||||||
|
>
|
||||||
|
<span class="menu-icon">{{ item.icon }}</span>
|
||||||
|
<span class="menu-text" v-show="!isCollapsed">{{ item.name }}</span>
|
||||||
|
<span class="active-indicator" v-if="isActive(item.path)"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 底部用户信息 -->
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="user-card">
|
||||||
|
<div class="user-avatar">👤</div>
|
||||||
|
<div class="user-info" v-show="!isCollapsed">
|
||||||
|
<span class="user-name">{{ userName }}</span>
|
||||||
|
<span class="user-role">{{ userRole }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="logout-btn" @click="handleLogout" v-show="!isCollapsed">
|
||||||
|
<span class="logout-icon">🚪</span>
|
||||||
|
<span>退出登录</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<header class="top-header">
|
||||||
|
<div class="breadcrumb">
|
||||||
|
<span class="breadcrumb-item">首页</span>
|
||||||
|
<span class="breadcrumb-sep">/</span>
|
||||||
|
<span class="breadcrumb-item active">{{ currentPageName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="action-item">
|
||||||
|
<span class="action-icon">🔔</span>
|
||||||
|
</div>
|
||||||
|
<div class="action-item">
|
||||||
|
<span class="action-icon">⚙️</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 页面内容 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const isCollapsed = ref(false)
|
||||||
|
|
||||||
|
const userName = ref('管理员')
|
||||||
|
const userRole = ref('系统管理员')
|
||||||
|
|
||||||
|
const menuGroups = [
|
||||||
|
{
|
||||||
|
title: '工作台',
|
||||||
|
items: [
|
||||||
|
{ name: '欢迎页', path: '/welcome', icon: '🏠' },
|
||||||
|
{ name: '订单管理', path: '/order/list', icon: '📋' },
|
||||||
|
{ name: '商品管理', path: '/goods/list', icon: '📦' },
|
||||||
|
{ name: '打印发货', path: '/print/log', icon: '🖨️' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '仓库',
|
||||||
|
items: [
|
||||||
|
{ name: '仓库列表', path: '/warehouse', icon: '🏪' },
|
||||||
|
{ name: '库存管理', path: '/warehouse/stock', icon: '📊' },
|
||||||
|
{ name: '采购管理', path: '/warehouse/purchase', icon: '🛒' },
|
||||||
|
{ name: '收货管理', path: '/warehouse/receiving', icon: '📥' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '平台',
|
||||||
|
items: [
|
||||||
|
{ name: '店铺管理', path: '/platform/shops', icon: '🏬' },
|
||||||
|
{ name: '运单账号', path: '/platform/waybill', icon: '📮' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '系统',
|
||||||
|
items: [
|
||||||
|
{ name: '品牌管理', path: '/settings/brand', icon: '🏷️' },
|
||||||
|
{ name: '供应商管理', path: '/settings/supplier', icon: '🤝' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentPageName = computed(() => {
|
||||||
|
const path = route.path
|
||||||
|
for (const group of menuGroups) {
|
||||||
|
const item = group.items.find(i => i.path === path)
|
||||||
|
if (item) return item.name
|
||||||
|
}
|
||||||
|
return '欢迎页'
|
||||||
|
})
|
||||||
|
|
||||||
|
const isActive = (path) => {
|
||||||
|
return route.path === path || route.path.startsWith(path + '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCollapse = () => {
|
||||||
|
isCollapsed.value = !isCollapsed.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateTo = (path) => {
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('erp_token')
|
||||||
|
localStorage.removeItem('current_user')
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-wrapper {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏 */
|
||||||
|
.sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
width: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏头部 */
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn:hover {
|
||||||
|
background: rgba(212, 175, 55, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
color: #d4af37;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单 */
|
||||||
|
.sidebar-menu {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-group-title {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
background: linear-gradient(135deg, rgba(212, 175, 55, 0.2), rgba(212, 175, 55, 0.1));
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 24px;
|
||||||
|
background: #d4af37;
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏底部 */
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: linear-gradient(135deg, #d4af37, #f5e6a3);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn:hover {
|
||||||
|
background: rgba(220, 53, 69, 0.2);
|
||||||
|
border-color: rgba(220, 53, 69, 0.3);
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区 */
|
||||||
|
.main-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 240px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: margin-left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed + .main-wrapper {
|
||||||
|
margin-left: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部导航 */
|
||||||
|
.top-header {
|
||||||
|
height: 64px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: #1a1a2e;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-sep {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-item:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed {
|
||||||
|
transform: translateX(0);
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed + .main-wrapper {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
src/main.ts
Normal file
13
src/main.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router/manual'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(ElementPlus, { locale: zhCn })
|
||||||
|
app.mount('#app')
|
||||||
1208
src/mock/adapter.ts
Normal file
1208
src/mock/adapter.ts
Normal file
File diff suppressed because it is too large
Load Diff
203
src/mock/data.ts
Normal file
203
src/mock/data.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
// src/mock/data.ts
|
||||||
|
// 所有模拟数据使用可修改的对象
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
warehouses: [
|
||||||
|
{ id: 'w1', name: '北京仓', type: 'erp', ownerCode: 'OWN001', cloudCode: '', cloudSystem: '', createdAt: '2025-01-01 10:00' },
|
||||||
|
{ id: 'w2', name: '上海仓', type: 'erp', ownerCode: 'OWN002', cloudCode: '', cloudSystem: '', createdAt: '2025-01-02 11:00' },
|
||||||
|
{ id: 'w3', name: '广州云仓', type: 'cloud', ownerCode: 'OWN003', cloudCode: 'CLOUD001', cloudSystem: 'jst', createdAt: '2025-01-03 12:00' },
|
||||||
|
],
|
||||||
|
|
||||||
|
suppliers: [
|
||||||
|
{ id: 's1', name: '供应商A', code: 'SUP001' },
|
||||||
|
{ id: 's2', name: '供应商B', code: 'SUP002' },
|
||||||
|
],
|
||||||
|
|
||||||
|
brands: [
|
||||||
|
{ id: 'b1', name: '品牌X' },
|
||||||
|
{ id: 'b2', name: '品牌Y' },
|
||||||
|
],
|
||||||
|
|
||||||
|
goods: [
|
||||||
|
{ id: 'g1', code: 'G001', name: '商品一', costPrice: 10, retailPrice: 15, barcode: '123456', unit: '件', weight: 100, category: 'food', stockWarning: 5, type: 'normal' },
|
||||||
|
{ id: 'g2', code: 'G002', name: '商品二', costPrice: 20, retailPrice: 30, barcode: '789012', unit: '件', weight: 200, category: 'clothing', stockWarning: 10, type: 'normal' },
|
||||||
|
],
|
||||||
|
|
||||||
|
expressCompanies: [
|
||||||
|
{ id: 'e1', name: '顺丰速运' },
|
||||||
|
{ id: 'e2', name: '中通快递' },
|
||||||
|
{ id: 'e3', name: '圆通速递' },
|
||||||
|
],
|
||||||
|
|
||||||
|
templates: [
|
||||||
|
{
|
||||||
|
id: 't1',
|
||||||
|
name: '标准快递模板',
|
||||||
|
platform: 'common',
|
||||||
|
size: '100x150',
|
||||||
|
style: '标准样式',
|
||||||
|
defaultExpress: '顺丰速运',
|
||||||
|
senderName: '张三',
|
||||||
|
senderPhone: '13800138000',
|
||||||
|
senderAddress: '北京市朝阳区某某路1号',
|
||||||
|
showProduct: true,
|
||||||
|
remark: '默认模板',
|
||||||
|
updatedAt: '2025-01-01 10:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't2',
|
||||||
|
name: '京东专用模板',
|
||||||
|
platform: 'jd',
|
||||||
|
size: '100x150',
|
||||||
|
style: '京东样式',
|
||||||
|
defaultExpress: '京东物流',
|
||||||
|
senderName: '李四',
|
||||||
|
senderPhone: '13800138001',
|
||||||
|
senderAddress: '上海市浦东新区某某路2号',
|
||||||
|
showProduct: true,
|
||||||
|
remark: '',
|
||||||
|
updatedAt: '2025-01-02 11:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't3',
|
||||||
|
name: '拼多多模板',
|
||||||
|
platform: 'pdd',
|
||||||
|
size: '76x130',
|
||||||
|
style: '拼多多样式',
|
||||||
|
defaultExpress: '中通快递',
|
||||||
|
senderName: '王五',
|
||||||
|
senderPhone: '13800138002',
|
||||||
|
senderAddress: '广州市天河区某某路3号',
|
||||||
|
showProduct: false,
|
||||||
|
remark: '不显示商品明细',
|
||||||
|
updatedAt: '2025-01-03 12:00'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
warehouseBindings: [
|
||||||
|
{
|
||||||
|
id: 'b1',
|
||||||
|
warehouseId: 'w3',
|
||||||
|
warehouseName: '广州云仓',
|
||||||
|
platform: 'common',
|
||||||
|
templateId: 't1',
|
||||||
|
templateName: '标准快递模板',
|
||||||
|
updatedAt: '2025-01-03 12:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b2',
|
||||||
|
warehouseId: 'w3',
|
||||||
|
warehouseName: '广州云仓',
|
||||||
|
platform: 'jd',
|
||||||
|
templateId: 't2',
|
||||||
|
templateName: '京东专用模板',
|
||||||
|
updatedAt: '2025-01-03 12:05'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
purchases: [],
|
||||||
|
receivings: [],
|
||||||
|
orders: [],
|
||||||
|
stocks: [],
|
||||||
|
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 'user_1',
|
||||||
|
username: 'admin@erp.com',
|
||||||
|
password: 'password123',
|
||||||
|
name: '系统管理员',
|
||||||
|
phone: '13800138000',
|
||||||
|
roleCode: 'super_admin',
|
||||||
|
roleName: '超管',
|
||||||
|
warehouseIds: [1, 2, 3, 4],
|
||||||
|
warehouseNames: '总仓库,华东仓,华南仓,华北仓',
|
||||||
|
permissions: ['*'],
|
||||||
|
status: 'active',
|
||||||
|
isSuperAdmin: true,
|
||||||
|
lastLoginTime: '2026-03-27 14:30:22',
|
||||||
|
createTime: '2025-01-01 10:00:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'user_2',
|
||||||
|
username: 'manager@erp.com',
|
||||||
|
name: '张经理',
|
||||||
|
phone: '13800138001',
|
||||||
|
roleCode: 'admin',
|
||||||
|
roleName: '管理员',
|
||||||
|
warehouseIds: [1, 2],
|
||||||
|
warehouseNames: '总仓库,华东仓',
|
||||||
|
permissions: ['order.*', 'goods.*', 'warehouse.*', 'print.*', 'platform.*', 'settings.brand', 'settings.supplier'],
|
||||||
|
status: 'active',
|
||||||
|
isSuperAdmin: false,
|
||||||
|
lastLoginTime: '2026-03-27 10:15:08',
|
||||||
|
createTime: '2025-01-15 10:00:00'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
operationLogs: [
|
||||||
|
{ id: 'log_1', userId: 'user_1', userName: '系统管理员', type: 'create_user', targetId: 'user_2', content: '创建用户: manager@erp.com', ip: '192.168.1.100', deviceId: 'DVC-XXXX-YYYY-ZZZZ', createTime: '2025-01-15 10:00:00' },
|
||||||
|
],
|
||||||
|
|
||||||
|
loginLogs: [
|
||||||
|
{ id: 'login_1', userId: 'user_1', loginTime: '2026-03-27 14:30:22', deviceType: 'Windows PC', deviceId: 'DVC-XXXX-YYYY-ZZZZ', ip: '192.168.1.100', location: '上海市浦东新区', status: 'success' },
|
||||||
|
],
|
||||||
|
|
||||||
|
trustedDevices: [
|
||||||
|
{ deviceId: 'DVC-XXXX-YYYY-ZZZZ', deviceName: '工作电脑', deviceType: 'Windows PC', userId: 'user_1', lastLoginTime: '2026-03-27 14:30', status: 'trusted' },
|
||||||
|
],
|
||||||
|
|
||||||
|
pendingDevices: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initData = () => {
|
||||||
|
// 生成库存
|
||||||
|
data.stocks = []
|
||||||
|
data.warehouses.forEach(warehouse => {
|
||||||
|
data.goods.forEach(good => {
|
||||||
|
data.stocks.push({
|
||||||
|
id: `stock-${warehouse.id}-${good.id}`,
|
||||||
|
skuCode: good.code,
|
||||||
|
skuName: good.name,
|
||||||
|
barcode: good.barcode,
|
||||||
|
warehouseId: warehouse.id,
|
||||||
|
warehouseName: warehouse.name,
|
||||||
|
quantity: Math.floor(Math.random() * 100),
|
||||||
|
lockedQuantity: 0,
|
||||||
|
availableQuantity: Math.floor(Math.random() * 100),
|
||||||
|
warningThreshold: good.stockWarning || 10,
|
||||||
|
status: 'normal'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 采购单示例
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const warehouse = data.warehouses[Math.floor(Math.random() * data.warehouses.length)]
|
||||||
|
const supplier = data.suppliers[Math.floor(Math.random() * data.suppliers.length)]
|
||||||
|
const items = data.goods.slice(0, 2).map(g => ({
|
||||||
|
skuCode: g.code,
|
||||||
|
skuName: g.name,
|
||||||
|
quantity: Math.floor(Math.random() * 10) + 1,
|
||||||
|
price: g.costPrice,
|
||||||
|
}))
|
||||||
|
data.purchases.push({
|
||||||
|
id: `po-${i}`,
|
||||||
|
poNo: `PO-2025-${String(i+1).padStart(4, '0')}`,
|
||||||
|
warehouseId: warehouse.id,
|
||||||
|
warehouseName: warehouse.name,
|
||||||
|
supplierId: supplier.id,
|
||||||
|
supplierName: supplier.name,
|
||||||
|
expectedArrival: '2025-04-01',
|
||||||
|
totalAmount: items.reduce((sum, it) => sum + it.quantity * it.price, 0),
|
||||||
|
status: i % 3 === 0 ? 'draft' : i % 3 === 1 ? 'underReview' : 'approved',
|
||||||
|
pushed: false,
|
||||||
|
createTime: new Date().toISOString(),
|
||||||
|
items,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initData()
|
||||||
|
|
||||||
|
export default data
|
||||||
312
src/router/manual.ts
Normal file
312
src/router/manual.ts
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Layout from '@/layouts/MainLayout.vue'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
// 登录页面 - 独立路由,不包含主布局
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('@/views/Auth/Login.vue'),
|
||||||
|
meta: { title: '登录', requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: Layout,
|
||||||
|
redirect: '/welcome',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'welcome',
|
||||||
|
name: 'Welcome',
|
||||||
|
component: () => import('@/views/Dashboard/Welcome.vue'),
|
||||||
|
meta: { title: '欢迎' }
|
||||||
|
},
|
||||||
|
// ========== 订单模块 ==========
|
||||||
|
{
|
||||||
|
path: 'order/list',
|
||||||
|
name: 'OrderList',
|
||||||
|
component: () => import('@/views/Order/OrderList.vue'),
|
||||||
|
meta: { title: '订单列表' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 售后模块 ==========
|
||||||
|
{
|
||||||
|
path: 'after-sale',
|
||||||
|
name: 'AfterSale',
|
||||||
|
component: () => import('@/views/AfterSale/AfterSaleList.vue'),
|
||||||
|
meta: { title: '售后管理' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 商品模块 ==========
|
||||||
|
{
|
||||||
|
path: 'goods/list',
|
||||||
|
name: 'GoodsList',
|
||||||
|
component: () => import('@/views/Goods/List.vue'),
|
||||||
|
meta: { title: '商品列表' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'goods/edit',
|
||||||
|
name: 'GoodsEdit',
|
||||||
|
component: () => import('@/views/Goods/Edit.vue'),
|
||||||
|
props: (route) => ({ type: route.query.type }),
|
||||||
|
meta: { title: '新增商品' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'goods/edit/:id',
|
||||||
|
name: 'GoodsEditDetail',
|
||||||
|
component: () => import('@/views/Goods/Edit.vue'),
|
||||||
|
props: (route) => ({ id: route.params.id, type: route.query.type }),
|
||||||
|
meta: { title: '编辑商品' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'goods/platform',
|
||||||
|
name: 'PlatformGoods',
|
||||||
|
component: () => import('@/views/Goods/PlatformGoods.vue'),
|
||||||
|
meta: { title: '平台商品' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'goods/push-log',
|
||||||
|
name: 'GoodsPushLog',
|
||||||
|
component: () => import('@/views/Goods/PushLog.vue'),
|
||||||
|
meta: { title: '商品推送日志' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 仓库管理 ==========
|
||||||
|
{
|
||||||
|
path: 'warehouse',
|
||||||
|
name: 'Warehouse',
|
||||||
|
redirect: '/warehouse/list',
|
||||||
|
meta: { title: '仓库管理' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'WarehouseList',
|
||||||
|
component: () => import('@/views/Warehouse/index.vue'),
|
||||||
|
meta: { title: '仓库列表' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'stock',
|
||||||
|
name: 'StockManagement',
|
||||||
|
component: () => import('@/views/Warehouse/Stock.vue'),
|
||||||
|
meta: { title: '库存管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'stock-detail/:id',
|
||||||
|
name: 'StockDetail',
|
||||||
|
component: () => import('@/views/Warehouse/StockDetail.vue'),
|
||||||
|
props: true,
|
||||||
|
meta: { title: '库存详情' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'purchase',
|
||||||
|
name: 'PurchaseOrder',
|
||||||
|
component: () => import('@/views/Warehouse/Purchase.vue'),
|
||||||
|
meta: { title: '采购单管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'purchase-detail/:id',
|
||||||
|
name: 'PurchaseDetail',
|
||||||
|
component: () => import('@/views/Warehouse/PurchaseDetail.vue'),
|
||||||
|
props: true,
|
||||||
|
meta: { title: '采购单详情' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'receiving',
|
||||||
|
name: 'ReceivingOrder',
|
||||||
|
component: () => import('@/views/Warehouse/Receiving.vue'),
|
||||||
|
meta: { title: '收货单管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'receiving-detail/:id',
|
||||||
|
name: 'ReceivingDetail',
|
||||||
|
component: () => import('@/views/Warehouse/ReceivingDetail.vue'),
|
||||||
|
props: true,
|
||||||
|
meta: { title: '收货单详情' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
name: 'WarehouseEdit',
|
||||||
|
component: () => import('@/views/Warehouse/Edit.vue'),
|
||||||
|
meta: { title: '新增仓库' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:id',
|
||||||
|
name: 'WarehouseEditDetail',
|
||||||
|
component: () => import('@/views/Warehouse/Edit.vue'),
|
||||||
|
props: true,
|
||||||
|
meta: { title: '编辑仓库' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bind-template/:id',
|
||||||
|
name: 'WarehouseBindTemplate',
|
||||||
|
component: () => import('@/views/Warehouse/BindTemplate.vue'),
|
||||||
|
props: true,
|
||||||
|
meta: { title: '仓库模板绑定' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 打印管理 ==========
|
||||||
|
{
|
||||||
|
path: 'print/batch',
|
||||||
|
name: 'PrintBatch',
|
||||||
|
component: () => import('@/views/Print/batch.vue'),
|
||||||
|
meta: { title: '批量打印' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'print/log',
|
||||||
|
name: 'PrintLog',
|
||||||
|
component: () => import('@/views/Print/log.vue'),
|
||||||
|
meta: { title: '打印日志' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'print/template-library/list',
|
||||||
|
name: 'TemplateList',
|
||||||
|
component: () => import('@/views/Print/TemplateList/index.vue'),
|
||||||
|
meta: { title: '模板列表' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'print/template-library/design',
|
||||||
|
name: 'TemplateDesign',
|
||||||
|
component: () => import('@/views/Print/TemplateDesign/index.vue'),
|
||||||
|
meta: { title: '模板设计' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'print/batch-detail/:id',
|
||||||
|
name: 'PrintBatchDetail',
|
||||||
|
component: () => import('@/views/Print/batch-detail/[id].vue'),
|
||||||
|
props: true,
|
||||||
|
meta: { title: '批次详情' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'print/plugin',
|
||||||
|
name: 'PluginManagement',
|
||||||
|
component: () => import('@/views/Print/PluginManagement.vue'),
|
||||||
|
meta: { title: '插件管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'print/queue',
|
||||||
|
name: 'PrintQueue',
|
||||||
|
component: () => import('@/views/Print/PrintQueue.vue'),
|
||||||
|
meta: { title: '打印队列' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'print/plugin-debug',
|
||||||
|
name: 'PluginDebug',
|
||||||
|
component: () => import('@/views/Print/PluginDebug.vue'),
|
||||||
|
meta: { title: '插件调试' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 平台管理 ==========
|
||||||
|
{
|
||||||
|
path: 'platform/waybill-accounts',
|
||||||
|
name: 'WaybillAccounts',
|
||||||
|
component: () => import('@/views/Platform/WaybillAccounts.vue'),
|
||||||
|
meta: { title: '面单账号' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'platform/shops',
|
||||||
|
name: 'Shops',
|
||||||
|
component: () => import('@/views/Platform/Shops/index.vue'),
|
||||||
|
meta: { title: '店铺管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'platform/shops/callback',
|
||||||
|
name: 'ShopCallback',
|
||||||
|
component: () => import('@/views/Platform/Shops/Callback.vue'),
|
||||||
|
meta: { title: '授权回调' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 系统设置 ==========
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
name: 'Setting',
|
||||||
|
redirect: '/settings/supplier',
|
||||||
|
meta: { title: '系统设置' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'supplier',
|
||||||
|
name: 'Supplier',
|
||||||
|
component: () => import('@/views/Setting/Supplier.vue'),
|
||||||
|
meta: { title: '供应商管理', permission: 'settings.supplier' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'brand',
|
||||||
|
name: 'Brand',
|
||||||
|
component: () => import('@/views/Setting/Brand.vue'),
|
||||||
|
meta: { title: '品牌管理', permission: 'settings.brand' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'user',
|
||||||
|
name: 'UserManagement',
|
||||||
|
component: () => import('@/views/System/UserManagement.vue'),
|
||||||
|
meta: { title: '用户管理', permission: 'settings.user' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'role',
|
||||||
|
name: 'RoleManagement',
|
||||||
|
component: () => import('@/views/System/RoleManagement.vue'),
|
||||||
|
meta: { title: '角色权限', permission: 'settings.role' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'operation-log',
|
||||||
|
name: 'OperationLog',
|
||||||
|
component: () => import('@/views/System/OperationLog.vue'),
|
||||||
|
meta: { title: '操作日志', permission: 'settings.log' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'third-party-config',
|
||||||
|
name: 'ThirdPartyConfig',
|
||||||
|
component: () => import('@/views/Setting/ThirdPartyConfig.vue'),
|
||||||
|
meta: { title: '第三方配置', permission: 'settings.config' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== 个人中心 ==========
|
||||||
|
{
|
||||||
|
path: 'personal-center',
|
||||||
|
name: 'PersonalCenter',
|
||||||
|
component: () => import('@/views/System/PersonalCenter.vue'),
|
||||||
|
meta: { title: '个人中心' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫:登录验证和页面标题设置
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
|
// 设置页面标题
|
||||||
|
if (to.meta.title) {
|
||||||
|
document.title = `${to.meta.title} - ERP管理系统`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否已登录
|
||||||
|
const isLoggedIn = !!localStorage.getItem('erp_token')
|
||||||
|
|
||||||
|
// 登录页面特殊处理
|
||||||
|
if (to.path === '/login') {
|
||||||
|
// 如果已登录,重定向到首页
|
||||||
|
if (isLoggedIn) {
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查路由是否需要认证
|
||||||
|
// 登录页面明确设置 requiresAuth: false,其他页面默认需要认证
|
||||||
|
const requiresAuth = to.matched.some(record => record.meta.requiresAuth !== false)
|
||||||
|
|
||||||
|
if (requiresAuth && !isLoggedIn) {
|
||||||
|
// 保存当前路径,登录后可以跳转回来
|
||||||
|
return `/login?redirect=${encodeURIComponent(to.fullPath)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
273
src/stores/order.ts
Normal file
273
src/stores/order.ts
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
// src/stores/order.ts
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export interface OrderItem {
|
||||||
|
id: number
|
||||||
|
shortId: string
|
||||||
|
platformOrderSn: string
|
||||||
|
platform: string
|
||||||
|
platformLabel?: string
|
||||||
|
shopId: number
|
||||||
|
shopName: string
|
||||||
|
orderTime: string
|
||||||
|
buyerNick: string
|
||||||
|
receiverName: string
|
||||||
|
receiverPhone: string
|
||||||
|
receiverAddress: string
|
||||||
|
goodsAmount: string
|
||||||
|
discountAmount: string
|
||||||
|
freight: string
|
||||||
|
totalAmount: string
|
||||||
|
orderStatus: string
|
||||||
|
platformStatus: string
|
||||||
|
auditStatus: string
|
||||||
|
deliveryStatus: string
|
||||||
|
expressCompany?: string
|
||||||
|
expressNo?: string
|
||||||
|
warehouseId?: number
|
||||||
|
warehouseName?: string
|
||||||
|
remark?: string
|
||||||
|
items: any[]
|
||||||
|
erpStatus: number // -1:已驳回,0:待审核,1:待发货,2:已发货
|
||||||
|
matchStatus: number // 0:未匹配,1:已匹配
|
||||||
|
createDate?: string
|
||||||
|
printError?: string
|
||||||
|
// 前端显示用字段
|
||||||
|
isBindErp?: boolean
|
||||||
|
erpGoodsName?: string
|
||||||
|
platformGoodsName?: string
|
||||||
|
erpSku?: string
|
||||||
|
platformSku?: string
|
||||||
|
bindStatusText?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台映射
|
||||||
|
const platformMap: Record<string, string> = {
|
||||||
|
'taobao': '淘宝',
|
||||||
|
'tmall': '天猫',
|
||||||
|
'jd': '京东',
|
||||||
|
'pdd': '拼多多',
|
||||||
|
'douyin': '抖音',
|
||||||
|
'kuaishou': '快手',
|
||||||
|
'youzan': '有赞',
|
||||||
|
'weidian': '微店'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态映射
|
||||||
|
const orderStatusMap: Record<string, number> = {
|
||||||
|
'pending': 0,
|
||||||
|
'auditing': 1,
|
||||||
|
'shipped': 2,
|
||||||
|
'completed': 3,
|
||||||
|
'cancelled': -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将API数据转换为store格式
|
||||||
|
function transformOrder(apiOrder: any): OrderItem {
|
||||||
|
return {
|
||||||
|
id: apiOrder.id,
|
||||||
|
shortId: apiOrder.short_id || apiOrder.shortId || '',
|
||||||
|
platformOrderSn: apiOrder.platform_order_sn || apiOrder.platformOrderSn || '',
|
||||||
|
platform: apiOrder.platform || '',
|
||||||
|
platformLabel: platformMap[apiOrder.platform] || apiOrder.platform || '',
|
||||||
|
shopId: apiOrder.shop_id || apiOrder.shopId || 0,
|
||||||
|
shopName: apiOrder.shop_name || apiOrder.shopName || '',
|
||||||
|
orderTime: apiOrder.order_time || apiOrder.orderTime || '',
|
||||||
|
buyerNick: apiOrder.buyer_nick || apiOrder.buyerNick || '',
|
||||||
|
receiverName: apiOrder.receiver_name || apiOrder.receiverName || '',
|
||||||
|
receiverPhone: apiOrder.receiver_phone || apiOrder.receiverPhone || '',
|
||||||
|
receiverAddress: apiOrder.receiver_address || apiOrder.receiverAddress || '',
|
||||||
|
goodsAmount: apiOrder.goods_amount || apiOrder.goodsAmount || '0',
|
||||||
|
discountAmount: apiOrder.discount_amount || apiOrder.discountAmount || '0',
|
||||||
|
freight: apiOrder.freight || '0',
|
||||||
|
totalAmount: apiOrder.total_amount || apiOrder.totalAmount || '0',
|
||||||
|
orderStatus: apiOrder.order_status || apiOrder.orderStatus || '',
|
||||||
|
platformStatus: apiOrder.platform_status || apiOrder.platformStatus || '',
|
||||||
|
auditStatus: apiOrder.audit_status || apiOrder.auditStatus || '',
|
||||||
|
deliveryStatus: apiOrder.delivery_status || apiOrder.deliveryStatus || '',
|
||||||
|
expressCompany: apiOrder.express_company || apiOrder.expressCompany || '',
|
||||||
|
expressNo: apiOrder.express_no || apiOrder.expressNo || '',
|
||||||
|
warehouseId: apiOrder.warehouse_id || apiOrder.warehouseId,
|
||||||
|
warehouseName: apiOrder.warehouse_name || apiOrder.warehouseName || '',
|
||||||
|
remark: apiOrder.remark || '',
|
||||||
|
erpStatus: orderStatusMap[apiOrder.order_status] ?? apiOrder.erpStatus ?? 0,
|
||||||
|
matchStatus: apiOrder.match_status ?? apiOrder.matchStatus ?? 0,
|
||||||
|
createDate: apiOrder.created_at || apiOrder.createDate || '',
|
||||||
|
printError: apiOrder.print_error || apiOrder.printError || '',
|
||||||
|
|
||||||
|
// 前端显示用字段(从 items 计算)
|
||||||
|
isBindErp: (apiOrder.items || []).some((item: any) => item.erp_sku_id),
|
||||||
|
erpGoodsName: (apiOrder.items || []).find((item: any) => item.erp_sku_id)?.erp_goods_name || '',
|
||||||
|
platformGoodsName: (apiOrder.items || [])[0]?.goods_name || '',
|
||||||
|
erpSku: (apiOrder.items || []).find((item: any) => item.erp_sku_id)?.erp_goods_sku || '',
|
||||||
|
platformSku: (apiOrder.items || [])[0]?.platform_sku || '',
|
||||||
|
bindStatusText: (apiOrder.items || []).every((item: any) => item.erp_sku_id) ? '已绑定' : '仅平台',
|
||||||
|
|
||||||
|
items: (apiOrder.items || []).map((item: any) => ({
|
||||||
|
...item,
|
||||||
|
isBindErp: !!item.erp_sku_id,
|
||||||
|
erpGoodsName: item.erp_goods_name || '',
|
||||||
|
platformGoodsName: item.goods_name || '',
|
||||||
|
erpSku: item.erp_goods_sku || item.erp_sku || '',
|
||||||
|
platformSku: item.platform_sku || '',
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOrderStore = defineStore('order', () => {
|
||||||
|
const orders = ref<OrderItem[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 从真实API获取订单
|
||||||
|
const fetchOrders = async (params?: { page?: number; limit?: number; keyword?: string }) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await request.get('/api/orders', { params })
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const list = Array.isArray(res.data) ? res.data : (res.data.list || [])
|
||||||
|
orders.value = list.map(transformOrder)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订单列表失败', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据ID获取订单详情
|
||||||
|
const fetchOrderDetail = async (id: number): Promise<OrderItem | null> => {
|
||||||
|
try {
|
||||||
|
const res = await request.get(`/api/orders/${id}`)
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
return transformOrder(res.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订单详情失败', error)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单
|
||||||
|
const updateOrder = async (id: number, data: Partial<OrderItem>) => {
|
||||||
|
try {
|
||||||
|
const res = await request.put(`/api/orders/${id}`, data)
|
||||||
|
if (res.code === 200) {
|
||||||
|
// 更新本地数据
|
||||||
|
const index = orders.value.findIndex(o => o.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
orders.value[index] = { ...orders.value[index], ...data }
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新订单失败', error)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单发货
|
||||||
|
const shipOrder = async (id: number, expressCompany: string, expressNo: string) => {
|
||||||
|
try {
|
||||||
|
const res = await request.post(`/api/orders/${id}/ship`, { expressCompany, expressNo })
|
||||||
|
if (res.code === 200) {
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.erpStatus = 2
|
||||||
|
order.expressCompany = expressCompany
|
||||||
|
order.expressNo = expressNo
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('订单发货失败', error)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审核订单
|
||||||
|
const auditOrder = async (id: number, action: 'approve' | 'reject', comment?: string) => {
|
||||||
|
try {
|
||||||
|
const res = await request.post(`/api/orders/${id}/audit`, { action, comment })
|
||||||
|
if (res.code === 200) {
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.erpStatus = action === 'approve' ? 1 : 0
|
||||||
|
order.auditStatus = action === 'approve' ? 'approved' : 'rejected'
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('审核订单失败', error)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除/关闭订单
|
||||||
|
const deleteOrder = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const res = await request.delete(`/api/orders/${id}`)
|
||||||
|
if (res.code === 200) {
|
||||||
|
orders.value = orders.value.filter(o => o.id !== id)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除订单失败', error)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
const updateOrderStatus = async (id: number, status: number) => {
|
||||||
|
try {
|
||||||
|
const res = await request.put(`/api/orders/${id}`, { erpStatus: status })
|
||||||
|
if (res.code === 200) {
|
||||||
|
const order = orders.value.find(o => o.id === id)
|
||||||
|
if (order) {
|
||||||
|
order.erpStatus = status
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新订单状态失败', error)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驳回订单
|
||||||
|
const rejectOrder = async (id: number, reason: string) => {
|
||||||
|
return await auditOrder(id, 'reject', reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加订单到列表
|
||||||
|
const addOrder = (order: OrderItem) => {
|
||||||
|
orders.value.unshift(order)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量添加订单
|
||||||
|
const addOrders = (newOrders: OrderItem[]) => {
|
||||||
|
orders.value.unshift(...newOrders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取单个订单
|
||||||
|
const getOrder = (id: number) => {
|
||||||
|
return orders.value.find(o => o.id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
orders,
|
||||||
|
loading,
|
||||||
|
fetchOrders,
|
||||||
|
fetchOrderDetail,
|
||||||
|
updateOrder,
|
||||||
|
shipOrder,
|
||||||
|
auditOrder,
|
||||||
|
deleteOrder,
|
||||||
|
updateOrderStatus,
|
||||||
|
rejectOrder,
|
||||||
|
addOrder,
|
||||||
|
addOrders,
|
||||||
|
getOrder
|
||||||
|
}
|
||||||
|
})
|
||||||
69
src/stores/user.ts
Normal file
69
src/stores/user.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
phone?: string
|
||||||
|
avatar?: string
|
||||||
|
status: string
|
||||||
|
is_test_user: boolean
|
||||||
|
created_at: string
|
||||||
|
last_login_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// State
|
||||||
|
const token = ref<string>(localStorage.getItem('erp_token') || '')
|
||||||
|
const userInfo = ref<UserInfo | null>(null)
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const isLoggedIn = computed(() => !!token.value)
|
||||||
|
const currentUser = computed(() => userInfo.value)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
function setToken(newToken: string) {
|
||||||
|
token.value = newToken
|
||||||
|
localStorage.setItem('erp_token', newToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUserInfo(info: UserInfo) {
|
||||||
|
userInfo.value = info
|
||||||
|
localStorage.setItem('current_user', JSON.stringify(info))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUser() {
|
||||||
|
token.value = ''
|
||||||
|
userInfo.value = null
|
||||||
|
localStorage.removeItem('erp_token')
|
||||||
|
localStorage.removeItem('current_user')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时从 localStorage 恢复
|
||||||
|
function init() {
|
||||||
|
const storedToken = localStorage.getItem('erp_token')
|
||||||
|
const storedUser = localStorage.getItem('current_user')
|
||||||
|
if (storedToken) {
|
||||||
|
token.value = storedToken
|
||||||
|
}
|
||||||
|
if (storedUser) {
|
||||||
|
try {
|
||||||
|
userInfo.value = JSON.parse(storedUser)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse stored user:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
userInfo,
|
||||||
|
isLoggedIn,
|
||||||
|
currentUser,
|
||||||
|
setToken,
|
||||||
|
setUserInfo,
|
||||||
|
clearUser,
|
||||||
|
init
|
||||||
|
}
|
||||||
|
})
|
||||||
410
src/style.css
Normal file
410
src/style.css
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
color-scheme: light;
|
||||||
|
color: #1a1a2e;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Color Palette */
|
||||||
|
:root {
|
||||||
|
--gold: #d4af37;
|
||||||
|
--gold-light: #f5e6a3;
|
||||||
|
--gold-gradient: linear-gradient(135deg, #d4af37, #f5e6a3);
|
||||||
|
--deep-blue: #1a1a2e;
|
||||||
|
--deep-blue-light: #16213e;
|
||||||
|
--bg-page: #f8f9fa;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #6c757d;
|
||||||
|
--text-muted: #adb5bd;
|
||||||
|
--border-color: #e9ecef;
|
||||||
|
--shadow-soft: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
--radius-card: 12px;
|
||||||
|
--radius-input: 8px;
|
||||||
|
--transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-page);
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Card Style */
|
||||||
|
.el-card {
|
||||||
|
border-radius: var(--radius-card) !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: var(--shadow-soft) !important;
|
||||||
|
background: var(--bg-card) !important;
|
||||||
|
transition: var(--transition) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card:hover {
|
||||||
|
box-shadow: var(--shadow-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card .el-card__header {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Button Styles */
|
||||||
|
.el-button {
|
||||||
|
border-radius: var(--radius-input) !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
transition: var(--transition) !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary {
|
||||||
|
background: var(--gold) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary:hover {
|
||||||
|
background: var(--gold-light) !important;
|
||||||
|
color: var(--deep-blue) !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--success {
|
||||||
|
background: linear-gradient(135deg, #52c41a, #73d13d) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--success:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--warning {
|
||||||
|
background: linear-gradient(135deg, #fa8c16, #ffa940) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--danger {
|
||||||
|
background: linear-gradient(135deg, #ff4d4f, #ff7875) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--default {
|
||||||
|
background: #fff !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
border: 1px solid var(--border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--default:hover {
|
||||||
|
border-color: var(--gold) !important;
|
||||||
|
color: var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Table Styles */
|
||||||
|
.el-table {
|
||||||
|
border-radius: var(--radius-card) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table th.el-table__cell {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 14px 12px !important;
|
||||||
|
border-bottom: 1px solid var(--border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table td.el-table__cell {
|
||||||
|
padding: 14px 12px !important;
|
||||||
|
border-bottom: 1px solid var(--border-color) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__row {
|
||||||
|
transition: var(--transition) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__row:hover > td {
|
||||||
|
background: #fafafa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table--border .el-table__cell {
|
||||||
|
border-right: 1px solid var(--border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Input Styles */
|
||||||
|
.el-input__wrapper {
|
||||||
|
border-radius: var(--radius-input) !important;
|
||||||
|
box-shadow: 0 0 0 1px var(--border-color) !important;
|
||||||
|
transition: var(--transition) !important;
|
||||||
|
padding: 4px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper:hover {
|
||||||
|
box-shadow: 0 0 0 1px var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper.is-focus {
|
||||||
|
box-shadow: 0 0 0 2px var(--gold) !important;
|
||||||
|
border-color: var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__inner::placeholder {
|
||||||
|
color: var(--text-muted) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Select Styles */
|
||||||
|
.el-select__wrapper {
|
||||||
|
border-radius: var(--radius-input) !important;
|
||||||
|
box-shadow: 0 0 0 1px var(--border-color) !important;
|
||||||
|
transition: var(--transition) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select__wrapper:hover {
|
||||||
|
box-shadow: 0 0 0 1px var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select__wrapper.is-focus {
|
||||||
|
box-shadow: 0 0 0 2px var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Date Picker */
|
||||||
|
.el-date-editor.el-input__wrapper {
|
||||||
|
border-radius: var(--radius-input) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Pagination */
|
||||||
|
.el-pagination {
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination button {
|
||||||
|
border-radius: var(--radius-input) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pager li {
|
||||||
|
border-radius: var(--radius-input) !important;
|
||||||
|
transition: var(--transition) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pager li:hover {
|
||||||
|
color: var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pager li.is-active {
|
||||||
|
background: var(--gold) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Tag Styles */
|
||||||
|
.el-tag {
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Dialog */
|
||||||
|
.el-dialog {
|
||||||
|
border-radius: var(--radius-card) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__header {
|
||||||
|
background: linear-gradient(135deg, var(--deep-blue), var(--deep-blue-light));
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px 24px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__title {
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__headerbtn .el-dialog__close {
|
||||||
|
color: rgba(255,255,255,0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__headerbtn:hover .el-dialog__close {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: 16px 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Drawer */
|
||||||
|
.el-drawer__header {
|
||||||
|
background: linear-gradient(135deg, var(--deep-blue), var(--deep-blue-light));
|
||||||
|
color: #fff;
|
||||||
|
padding: 20px 24px !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__header span {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__headerbtn {
|
||||||
|
top: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__headerbtn .el-drawer__close {
|
||||||
|
color: rgba(255,255,255,0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Form */
|
||||||
|
.el-form-item__label {
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__label::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Descriptions */
|
||||||
|
.el-descriptions__label {
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
background: #fafafa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions__content {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Tabs */
|
||||||
|
.el-tabs__item {
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
transition: var(--transition) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__item:hover {
|
||||||
|
color: var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__item.is-active {
|
||||||
|
color: var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__active-bar {
|
||||||
|
background: var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Dropdown */
|
||||||
|
.el-dropdown-menu__item:hover {
|
||||||
|
background: #fafafa !important;
|
||||||
|
color: var(--gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Premium Badge */
|
||||||
|
.el-badge__content {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.page-container {
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--bg-page);
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
padding: 20px;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-card:hover {
|
||||||
|
box-shadow: var(--shadow-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 2px solid var(--gold);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
38
src/types/template.ts
Normal file
38
src/types/template.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
export interface TemplateElement {
|
||||||
|
id: string
|
||||||
|
type: 'text' | 'barcode' | 'rect' | 'image' | 'line' // 可扩展
|
||||||
|
content?: any
|
||||||
|
x: number // 单位 mm
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
fontSize?: number
|
||||||
|
fontWeight?: 'normal' | 'bold'
|
||||||
|
color?: string
|
||||||
|
backgroundColor?: string
|
||||||
|
border?: string
|
||||||
|
borderRadius?: number
|
||||||
|
textAlign?: 'left' | 'center' | 'right'
|
||||||
|
protected?: boolean // 是否为保护区域内的元素(不可编辑)
|
||||||
|
locked?: boolean // 锁定位置/大小
|
||||||
|
dataField?: string // 绑定的数据字段名
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Template {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
width: number // 单位 mm
|
||||||
|
height: number
|
||||||
|
unit: 'mm'
|
||||||
|
elements: TemplateElement[]
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
version: number
|
||||||
|
protectedAreas?: Array<{
|
||||||
|
id: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}>
|
||||||
|
}
|
||||||
61
src/utils/request.ts
Normal file
61
src/utils/request.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// src/utils/request.ts
|
||||||
|
import axios from 'axios'
|
||||||
|
import MockAdapter from 'axios-mock-adapter'
|
||||||
|
import { setupMockInterceptors } from '@/mock/adapter'
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||||
|
timeout: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器:添加 Token
|
||||||
|
instance.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
const token = localStorage.getItem('erp_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器:统一处理响应
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
// 处理 204 No Content(无内容)
|
||||||
|
if (response.status === 204) {
|
||||||
|
return { code: 200, message: 'success' }
|
||||||
|
}
|
||||||
|
// 如果 response.data 存在,返回 data
|
||||||
|
if (response.data !== undefined && response.data !== null) {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
// 如果 response.data 为空但状态码是 200,也返回成功对象
|
||||||
|
if (response.status === 200) {
|
||||||
|
return { code: 200, message: 'success' }
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
// 处理 401 未授权(不在登录页时跳转)
|
||||||
|
if (error.response?.status === 401 && !window.location.pathname.includes('/login')) {
|
||||||
|
localStorage.removeItem('erp_token')
|
||||||
|
localStorage.removeItem('current_user')
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
console.error('请求错误:', error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 仅在开发环境且开启了 mock 时启用拦截
|
||||||
|
if (import.meta.env.DEV && import.meta.env.VITE_USE_MOCK === 'true') {
|
||||||
|
console.log('✅ Mock 已启用')
|
||||||
|
const mock = new MockAdapter(instance, { delayResponse: 300 })
|
||||||
|
setupMockInterceptors(mock)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default instance
|
||||||
620
src/views/AfterSale/AfterSaleList.vue
Normal file
620
src/views/AfterSale/AfterSaleList.vue
Normal file
@ -0,0 +1,620 @@
|
|||||||
|
<template>
|
||||||
|
<div class="after-sale-container">
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<el-row :gutter="16" class="stats-row">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value pending">{{ stats.pending }}</span>
|
||||||
|
<span class="stat-label">待处理</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value processing">{{ stats.processing }}</span>
|
||||||
|
<span class="stat-label">处理中</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-value completed">{{ stats.completedToday }}</span>
|
||||||
|
<span class="stat-label">今日完成</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 标签页 -->
|
||||||
|
<el-tabs v-model="activeTab" class="main-tabs" @tab-change="handleTabChange">
|
||||||
|
<!-- 售后列表 -->
|
||||||
|
<el-tab-pane label="售后列表" name="list">
|
||||||
|
<el-card class="main-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>售后管理</span>
|
||||||
|
<el-button type="primary" @click="showCreateDialog">
|
||||||
|
<el-icon><Plus /></el-icon> 新建售后单
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 筛选 -->
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-select v-model="filterForm.type" placeholder="售后类型" clearable style="width: 120px;">
|
||||||
|
<el-option label="仅退款" value="refund_only" />
|
||||||
|
<el-option label="退货" value="return" />
|
||||||
|
<el-option label="换货" value="exchange" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="filterForm.status" placeholder="状态" clearable style="width: 120px;">
|
||||||
|
<el-option label="待处理" value="pending" />
|
||||||
|
<el-option label="处理中" value="processing" />
|
||||||
|
<el-option label="已完成" value="completed" />
|
||||||
|
<el-option label="已拒绝" value="rejected" />
|
||||||
|
</el-select>
|
||||||
|
<el-input v-model="filterForm.keyword" placeholder="售后单号/订单号/原因" clearable style="width: 200px;" @keyup.enter="loadList">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-date-picker v-model="filterForm.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 240px;" @change="loadList" />
|
||||||
|
<el-button @click="loadList"><el-icon><Refresh /></el-icon> 刷新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<el-table :data="list" border v-loading="loading" size="small">
|
||||||
|
<el-table-column prop="short_id" label="售后单号" width="150" fixed />
|
||||||
|
<el-table-column prop="order_short_id" label="原订单号" width="150">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-link type="primary" @click="viewOrder(row.order_id)">{{ row.order_short_id }}</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="type" label="类型" width="90">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-tag size="small" :type="getTypeTagType(row.type)">{{ row.type_text }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="90">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-tag size="small" :type="getStatusTagType(row.status)">{{ row.status_text }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="reason" label="售后原因" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="refund_amount" label="退款金额" width="100">
|
||||||
|
<template #default="{row}">
|
||||||
|
¥{{ row.refund_amount }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="creator_name" label="申请人" width="100" />
|
||||||
|
<el-table-column prop="created_at" label="申请时间" width="160" />
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-button type="primary" link size="small" @click="viewDetail(row)">查看</el-button>
|
||||||
|
<el-button type="success" link size="small" @click="handleAction(row)" v-if="row.status !== 'completed' && row.status !== 'rejected'">处理</el-button>
|
||||||
|
<el-button type="danger" link size="small" @click="deleteRecord(row)" v-if="row.status === 'pending'">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.currentPage"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@size-change="loadList"
|
||||||
|
@current-change="loadList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 新建售后单 -->
|
||||||
|
<el-tab-pane label="新建售后" name="create">
|
||||||
|
<el-card class="main-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>新建售后单</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 选择订单 -->
|
||||||
|
<div class="section">
|
||||||
|
<h4>选择订单</h4>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-input v-model="orderSearchKeyword" placeholder="订单号/收货人/手机号" clearable style="width: 200px;" @keyup.enter="searchOrders">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-button @click="searchOrders">搜索订单</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已发货订单(退货/换货) -->
|
||||||
|
<h5>已发货订单(可退货/换货)</h5>
|
||||||
|
<el-table :data="shippedOrders" border v-loading="orderLoading" size="small" max-height="200" @row-click="selectShippedOrder">
|
||||||
|
<el-table-column prop="short_id" label="订单号" width="150" />
|
||||||
|
<el-table-column prop="shop_name" label="店铺" width="120" />
|
||||||
|
<el-table-column prop="receiver_name" label="收货人" width="100" />
|
||||||
|
<el-table-column prop="receiver_phone" label="手机号" width="120" />
|
||||||
|
<el-table-column prop="total_amount" label="订单金额" width="100">
|
||||||
|
<template #default="{row}">¥{{ row.total_amount }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="80">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-button type="primary" size="small" @click.stop="selectOrder(row, 'shipped')">选择</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 未发货订单(仅退款) -->
|
||||||
|
<h5 style="margin-top: 16px;">未发货订单(可仅退款)</h5>
|
||||||
|
<el-table :data="notShippedOrders" border v-loading="orderLoading" size="small" max-height="200" @row-click="selectNotShippedOrder">
|
||||||
|
<el-table-column prop="short_id" label="订单号" width="150" />
|
||||||
|
<el-table-column prop="shop_name" label="店铺" width="120" />
|
||||||
|
<el-table-column prop="receiver_name" label="收货人" width="100" />
|
||||||
|
<el-table-column prop="receiver_phone" label="手机号" width="120" />
|
||||||
|
<el-table-column prop="total_amount" label="订单金额" width="100">
|
||||||
|
<template #default="{row}">¥{{ row.total_amount }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="80">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-button type="warning" size="small" @click.stop="selectOrder(row, 'not_shipped')">选择</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已选订单 -->
|
||||||
|
<div class="section" v-if="selectedOrder">
|
||||||
|
<h4>已选订单</h4>
|
||||||
|
<el-card shadow="never" class="selected-order-card">
|
||||||
|
<div class="order-info">
|
||||||
|
<span>订单号:<strong>{{ selectedOrder.short_id }}</strong></span>
|
||||||
|
<span>店铺:{{ selectedOrder.shop_name }}</span>
|
||||||
|
<span>收货人:{{ selectedOrder.receiver_name }}</span>
|
||||||
|
<span>金额:<strong>¥{{ selectedOrder.total_amount }}</strong></span>
|
||||||
|
</div>
|
||||||
|
<h5>选择商品(必选)</h5>
|
||||||
|
<el-table :data="selectedOrder.items" border size="small">
|
||||||
|
<el-table-column width="50">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-radio v-model="selectedItemId" :label="row.id" @change="handleItemSelect(row)"> </el-radio>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="goods_name" label="商品名称" min-width="150" />
|
||||||
|
<el-table-column prop="erp_sku" label="ERP SKU" width="120" />
|
||||||
|
<el-table-column prop="quantity" label="数量" width="60" />
|
||||||
|
<el-table-column prop="price" label="单价" width="80">
|
||||||
|
<template #default="{row}">¥{{ row.price }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="total_amount" label="小计" width="80">
|
||||||
|
<template #default="{row}">¥{{ row.total_amount }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 售后信息 -->
|
||||||
|
<div class="section" v-if="selectedOrder && selectedItem">
|
||||||
|
<h4>售后信息</h4>
|
||||||
|
<el-form :model="afterSaleForm" :rules="formRules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="售后类型">
|
||||||
|
<el-tag :type="selectedOrder.delivery_status === 'pending' ? 'warning' : 'primary'">
|
||||||
|
{{ selectedOrder.delivery_status === 'pending' ? '仅退款' : (afterSaleForm.type === 'return' ? '退货' : '换货') }}
|
||||||
|
</el-tag>
|
||||||
|
<span class="form-tip" v-if="selectedOrder.delivery_status === 'pending'">(未发货,仅支持仅退款)</span>
|
||||||
|
<div v-else style="margin-top: 8px;">
|
||||||
|
<el-radio-group v-model="afterSaleForm.type">
|
||||||
|
<el-radio label="return">退货</el-radio>
|
||||||
|
<el-radio label="exchange">换货</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="售后原因" prop="reason">
|
||||||
|
<el-select v-model="afterSaleForm.reason" placeholder="请选择原因" style="width: 100%;">
|
||||||
|
<el-option label="商品损坏" value="商品损坏" />
|
||||||
|
<el-option label="商品错漏" value="商品错漏" />
|
||||||
|
<el-option label="收到假货" value="收到假货" />
|
||||||
|
<el-option label="不想要了" value="不想要了" />
|
||||||
|
<el-option label="效果不好" value="效果不好" />
|
||||||
|
<el-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="详细说明" prop="description">
|
||||||
|
<el-input v-model="afterSaleForm.description" type="textarea" :rows="3" placeholder="请详细描述问题..." />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="退款金额">
|
||||||
|
<el-input-number v-model="afterSaleForm.refund_amount" :min="0" :precision="2" :max="selectedItem.total_amount" style="width: 200px;" />
|
||||||
|
<span class="form-tip">(不能超过 ¥{{ selectedItem.total_amount }})</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitAfterSale" :loading="submitLoading">提交售后单</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<!-- 售后详情弹窗 -->
|
||||||
|
<el-dialog title="售后详情" v-model="detailDialogVisible" width="700px">
|
||||||
|
<div v-if="currentAfterSale" class="detail-content">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="售后单号">{{ currentAfterSale.short_id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="售后类型">
|
||||||
|
<el-tag size="small" :type="getTypeTagType(currentAfterSale.type)">{{ currentAfterSale.type_text }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="原订单号">
|
||||||
|
<el-link type="primary" @click="viewOrder(currentAfterSale.order_id)">{{ currentAfterSale.order_short_id }}</el-link>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag size="small" :type="getStatusTagType(currentAfterSale.status)">{{ currentAfterSale.status_text }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="售后原因">{{ currentAfterSale.reason }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="退款金额">¥{{ currentAfterSale.refund_amount }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="申请人">{{ currentAfterSale.creator_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="申请时间">{{ currentAfterSale.created_at }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="处理人" v-if="currentAfterSale.processor_name">{{ currentAfterSale.processor_name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="处理时间" v-if="currentAfterSale.processed_at">{{ currentAfterSale.processed_at }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="拒绝原因" v-if="currentAfterSale.reject_reason" :span="2">{{ currentAfterSale.reject_reason }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="退货快递" v-if="currentAfterSale.return_express_company" :span="2">
|
||||||
|
{{ currentAfterSale.return_express_company }} {{ currentAfterSale.return_express_no }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="详细说明" :span="2" v-if="currentAfterSale.description">{{ currentAfterSale.description }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<h4 style="margin-top: 20px;">售后商品</h4>
|
||||||
|
<el-table :data="currentAfterSale.items" border size="small">
|
||||||
|
<el-table-column prop="goods_name" label="商品名称" min-width="150" />
|
||||||
|
<el-table-column prop="erp_sku" label="ERP SKU" width="120" />
|
||||||
|
<el-table-column prop="quantity" label="数量" width="60" />
|
||||||
|
<el-table-column prop="price" label="单价" width="80">
|
||||||
|
<template #default="{row}">¥{{ row.price }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="total_amount" label="小计" width="80">
|
||||||
|
<template #default="{row}">¥{{ row.total_amount }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<h4 style="margin-top: 20px;">原订单信息</h4>
|
||||||
|
<el-table :data="[currentAfterSale.order]" border size="small" v-if="currentAfterSale.order">
|
||||||
|
<el-table-column prop="short_id" label="订单号" width="150" />
|
||||||
|
<el-table-column prop="platform" label="平台" width="80" />
|
||||||
|
<el-table-column prop="shop_name" label="店铺" width="120" />
|
||||||
|
<el-table-column prop="receiver_name" label="收货人" width="100" />
|
||||||
|
<el-table-column prop="total_amount" label="订单金额" width="100">
|
||||||
|
<template #default="{row}">¥{{ row.total_amount }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="express_company" label="快递公司" width="100" />
|
||||||
|
<el-table-column prop="express_no" label="快递号" width="140" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button type="primary" @click="handleAction(currentAfterSale)" v-if="currentAfterSale && currentAfterSale.status !== 'completed' && currentAfterSale.status !== 'rejected'">处理</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 处理售后弹窗 -->
|
||||||
|
<el-dialog title="处理售后" v-model="actionDialogVisible" width="500px">
|
||||||
|
<el-form :model="actionForm" ref="actionFormRef" label-width="100px">
|
||||||
|
<el-form-item label="处理操作">
|
||||||
|
<el-radio-group v-model="actionForm.status">
|
||||||
|
<el-radio label="processing">开始处理</el-radio>
|
||||||
|
<el-radio label="completed">完成</el-radio>
|
||||||
|
<el-radio label="rejected">拒绝</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="退货快递" v-if="actionForm.status !== 'rejected'">
|
||||||
|
<el-input v-model="actionForm.return_express_company" placeholder="快递公司" style="width: 45%;" />
|
||||||
|
<el-input v-model="actionForm.return_express_no" placeholder="快递号" style="width: 45%; margin-left: 10px;" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="拒绝原因" v-if="actionForm.status === 'rejected'" prop="reject_reason">
|
||||||
|
<el-input v-model="actionForm.reject_reason" type="textarea" :rows="3" placeholder="请输入拒绝原因..." />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="actionDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitAction" :loading="actionLoading">确认</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import * as afterSaleApi from '@/api/afterSale'
|
||||||
|
import * as orderApi from '@/api/order'
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = ref({
|
||||||
|
pending: 0,
|
||||||
|
processing: 0,
|
||||||
|
completedToday: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 列表数据
|
||||||
|
const list = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const pagination = ref({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
const filterForm = ref({
|
||||||
|
type: '',
|
||||||
|
status: '',
|
||||||
|
keyword: '',
|
||||||
|
dateRange: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 标签页
|
||||||
|
const activeTab = ref('list')
|
||||||
|
|
||||||
|
// 新建售后
|
||||||
|
const orderLoading = ref(false)
|
||||||
|
const orderSearchKeyword = ref('')
|
||||||
|
const shippedOrders = ref<any[]>([])
|
||||||
|
const notShippedOrders = ref<any[]>([])
|
||||||
|
const selectedOrder = ref<any>(null)
|
||||||
|
const selectedItemId = ref<number | null>(null)
|
||||||
|
const selectedItem = ref<any>(null)
|
||||||
|
const afterSaleForm = ref({
|
||||||
|
type: 'return',
|
||||||
|
reason: '',
|
||||||
|
description: '',
|
||||||
|
refund_amount: 0
|
||||||
|
})
|
||||||
|
const formRef = ref()
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
reason: [{ required: true, message: '请选择售后原因', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情弹窗
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentAfterSale = ref<any>(null)
|
||||||
|
|
||||||
|
// 处理弹窗
|
||||||
|
const actionDialogVisible = ref(false)
|
||||||
|
const actionForm = ref({
|
||||||
|
status: 'processing',
|
||||||
|
reject_reason: '',
|
||||||
|
return_express_company: '',
|
||||||
|
return_express_no: ''
|
||||||
|
})
|
||||||
|
const actionFormRef = ref()
|
||||||
|
const actionLoading = ref(false)
|
||||||
|
|
||||||
|
// 获取统计
|
||||||
|
const loadStats = async () => {
|
||||||
|
try {
|
||||||
|
const res = await afterSaleApi.getAfterSaleStats()
|
||||||
|
if (res.code === 200) {
|
||||||
|
stats.value = res.data
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取列表
|
||||||
|
const loadList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await afterSaleApi.getAfterSalesList({
|
||||||
|
page: pagination.value.currentPage,
|
||||||
|
limit: pagination.value.pageSize,
|
||||||
|
type: filterForm.value.type,
|
||||||
|
status: filterForm.value.status,
|
||||||
|
keyword: filterForm.value.keyword,
|
||||||
|
date_range: filterForm.value.dateRange?.join(',')
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
list.value = res.data.list
|
||||||
|
pagination.value.total = res.data.total
|
||||||
|
}
|
||||||
|
} catch {} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标签页切换
|
||||||
|
const handleTabChange = (tab: string) => {
|
||||||
|
if (tab === 'create') {
|
||||||
|
searchOrders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索订单
|
||||||
|
const searchOrders = async () => {
|
||||||
|
orderLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await afterSaleApi.getAllAvailableOrders({
|
||||||
|
keyword: orderSearchKeyword.value,
|
||||||
|
limit: 50
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
shippedOrders.value = res.data.shipped_orders?.list || []
|
||||||
|
notShippedOrders.value = res.data.not_shipped_orders?.list || []
|
||||||
|
}
|
||||||
|
} catch {} finally {
|
||||||
|
orderLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择已发货订单
|
||||||
|
const selectShippedOrder = (row: any) => {
|
||||||
|
selectOrder(row, 'shipped')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择未发货订单
|
||||||
|
const selectNotShippedOrder = (row: any) => {
|
||||||
|
selectOrder(row, 'not_shipped')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择订单
|
||||||
|
const selectOrder = (order: any, type: string) => {
|
||||||
|
selectedOrder.value = order
|
||||||
|
selectedItemId.value = null
|
||||||
|
selectedItem.value = null
|
||||||
|
afterSaleForm.value = {
|
||||||
|
type: type === 'not_shipped' ? 'refund_only' : 'return',
|
||||||
|
reason: '',
|
||||||
|
description: '',
|
||||||
|
refund_amount: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择商品
|
||||||
|
const handleItemSelect = (item: any) => {
|
||||||
|
selectedItem.value = item
|
||||||
|
afterSaleForm.value.refund_amount = item.total_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交售后单
|
||||||
|
const submitAfterSale = async () => {
|
||||||
|
if (!selectedOrder.value || !selectedItem.value) {
|
||||||
|
ElMessage.warning('请选择订单和商品')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!afterSaleForm.value.reason) {
|
||||||
|
ElMessage.warning('请选择售后原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await formRef.value?.validate()
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await afterSaleApi.createAfterSale({
|
||||||
|
order_id: selectedOrder.value.id,
|
||||||
|
type: afterSaleForm.value.type,
|
||||||
|
reason: afterSaleForm.value.reason,
|
||||||
|
description: afterSaleForm.value.description,
|
||||||
|
refund_amount: afterSaleForm.value.refund_amount,
|
||||||
|
items: [{ order_item_id: selectedItem.value.id, quantity: selectedItem.value.quantity }]
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('售后单创建成功')
|
||||||
|
activeTab.value = 'list'
|
||||||
|
loadList()
|
||||||
|
loadStats()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '创建失败')
|
||||||
|
}
|
||||||
|
} catch {} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const viewDetail = async (row: any) => {
|
||||||
|
try {
|
||||||
|
const res = await afterSaleApi.getAfterSaleDetail(row.id)
|
||||||
|
if (res.code === 200) {
|
||||||
|
currentAfterSale.value = res.data
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看订单
|
||||||
|
const viewOrder = (orderId: number) => {
|
||||||
|
ElMessage.info('查看订单详情功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理售后
|
||||||
|
const handleAction = (row: any) => {
|
||||||
|
currentAfterSale.value = row
|
||||||
|
actionForm.value = {
|
||||||
|
status: 'processing',
|
||||||
|
reject_reason: '',
|
||||||
|
return_express_company: '',
|
||||||
|
return_express_no: ''
|
||||||
|
}
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
actionDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交处理
|
||||||
|
const submitAction = async () => {
|
||||||
|
if (actionForm.value.status === 'rejected' && !actionForm.value.reject_reason) {
|
||||||
|
ElMessage.warning('请输入拒绝原因')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actionLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await afterSaleApi.updateAfterSaleStatus(currentAfterSale.value.id, actionForm.value)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('处理成功')
|
||||||
|
actionDialogVisible.value = false
|
||||||
|
loadList()
|
||||||
|
loadStats()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '处理失败')
|
||||||
|
}
|
||||||
|
} catch {} finally {
|
||||||
|
actionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const deleteRecord = async (row: any) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除该售后单吗?', '提示', { type: 'warning' })
|
||||||
|
const res = await afterSaleApi.deleteAfterSale(row.id)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadList()
|
||||||
|
loadStats()
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getTypeTagType = (type: string) => {
|
||||||
|
return type === 'refund_only' ? 'warning' : type === 'return' ? 'danger' : 'primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusTagType = (status: string) => {
|
||||||
|
return status === 'pending' ? 'info' : status === 'processing' ? 'warning' : status === 'completed' ? 'success' : 'danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示创建弹窗(兼容按钮触发)
|
||||||
|
const showCreateDialog = () => {
|
||||||
|
activeTab.value = 'create'
|
||||||
|
searchOrders()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadStats()
|
||||||
|
loadList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.after-sale-container { padding: 20px; background: #f8f9fa; min-height: 100vh; }
|
||||||
|
.stats-row { margin-bottom: 16px; }
|
||||||
|
.stat-card { text-align: center; }
|
||||||
|
.stat-item { display: flex; flex-direction: column; align-items: center; padding: 10px 0; }
|
||||||
|
.stat-value { font-size: 32px; font-weight: 600; }
|
||||||
|
.stat-value.pending { color: #909399; }
|
||||||
|
.stat-value.processing { color: #e6a23c; }
|
||||||
|
.stat-value.completed { color: #67c23a; }
|
||||||
|
.stat-label { font-size: 14px; color: #909399; margin-top: 4px; }
|
||||||
|
.main-tabs { background: #fff; padding: 16px; border-radius: 8px; }
|
||||||
|
.main-card { border-radius: 8px; }
|
||||||
|
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.filter-bar { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; align-items: center; }
|
||||||
|
.section { margin-top: 24px; }
|
||||||
|
.section h4 { margin-bottom: 12px; color: #303133; }
|
||||||
|
.section h5 { margin: 12px 0 8px; color: #606266; font-size: 14px; }
|
||||||
|
.selected-order-card { background: #f5f7fa; }
|
||||||
|
.order-info { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||||
|
.order-info span { color: #606266; }
|
||||||
|
.form-tip { margin-left: 12px; color: #909399; font-size: 12px; }
|
||||||
|
.detail-content { padding: 0 10px; }
|
||||||
|
.pagination-wrapper { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||||
|
</style>
|
||||||
1151
src/views/Auth/Login.vue
Normal file
1151
src/views/Auth/Login.vue
Normal file
File diff suppressed because it is too large
Load Diff
364
src/views/Dashboard/Welcome.vue
Normal file
364
src/views/Dashboard/Welcome.vue
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome-container">
|
||||||
|
<div class="welcome-card">
|
||||||
|
<div class="welcome-header">
|
||||||
|
<div class="welcome-icon">
|
||||||
|
<svg viewBox="0 0 48 48" width="48" height="48">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="iconGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d4af37"/>
|
||||||
|
<stop offset="100%" style="stop-color:#f5e6a3"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="24" cy="24" r="20" fill="none" stroke="url(#iconGrad)" stroke-width="2"/>
|
||||||
|
<path d="M14 24 L21 31 L34 17" fill="none" stroke="url(#iconGrad)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1>欢迎进入ERP管理系统</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-content">
|
||||||
|
<p class="welcome-text">
|
||||||
|
您已成功登录系统。当前登录账号:
|
||||||
|
<el-tag class="user-tag" type="warning" size="large" effect="dark">
|
||||||
|
{{ userInfo.username }}
|
||||||
|
</el-tag>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="user-info-card">
|
||||||
|
<div class="info-card-title">账户信息</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="用户名">
|
||||||
|
{{ userInfo.username }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="姓名">
|
||||||
|
{{ userInfo.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="角色">
|
||||||
|
<el-tag :type="userInfo.role === 'admin' ? 'danger' : 'success'" size="small">
|
||||||
|
{{ userInfo.role === 'admin' ? '管理员' : '普通用户' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="登录时间">
|
||||||
|
{{ loginTime }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="system-features">
|
||||||
|
<div class="section-title">系统功能模块</div>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon-wrap gold">
|
||||||
|
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/>
|
||||||
|
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||||
|
<path d="M16 10a4 4 0 0 1-8 0"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h4>订单管理</h4>
|
||||||
|
<p>创建、查看和处理客户订单</p>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon-wrap blue">
|
||||||
|
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h4>商品管理</h4>
|
||||||
|
<p>管理商品信息和库存</p>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon-wrap green">
|
||||||
|
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="1" y="3" width="15" height="13"/>
|
||||||
|
<polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/>
|
||||||
|
<circle cx="5.5" cy="18.5" r="2.5"/>
|
||||||
|
<circle cx="18.5" cy="18.5" r="2.5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h4>仓库管理</h4>
|
||||||
|
<p>管理仓库和库存位置</p>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quick-actions">
|
||||||
|
<div class="section-title">快速操作</div>
|
||||||
|
<el-space wrap>
|
||||||
|
<el-button type="primary" class="action-btn gold-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
|
新建订单
|
||||||
|
</el-button>
|
||||||
|
<el-button class="action-btn white-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||||
|
查询库存
|
||||||
|
</el-button>
|
||||||
|
<el-button class="action-btn white-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||||
|
系统设置
|
||||||
|
</el-button>
|
||||||
|
<el-button class="action-btn white-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||||
|
使用帮助
|
||||||
|
</el-button>
|
||||||
|
</el-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-footer">
|
||||||
|
<el-divider />
|
||||||
|
<p class="footer-text">
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||||
|
提示:这是一个演示系统。所有数据均为模拟数据。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const userInfo = ref({
|
||||||
|
username: '',
|
||||||
|
name: '',
|
||||||
|
role: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const loginTime = ref('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const userData = localStorage.getItem('current_user')
|
||||||
|
if (userData) {
|
||||||
|
try {
|
||||||
|
userInfo.value = JSON.parse(userData)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析用户信息失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
loginTime.value = now.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.welcome-container {
|
||||||
|
padding: 24px;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-card {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-icon {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tag {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
margin: 0 8px;
|
||||||
|
background: linear-gradient(135deg, #d4af37, #f5e6a3);
|
||||||
|
border: none;
|
||||||
|
color: #1a1a2e;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-card {
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #d4af37;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-features {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #d4af37;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px 20px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon-wrap {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon-wrap.gold {
|
||||||
|
background: linear-gradient(135deg, rgba(212, 175, 55, 0.15), rgba(212, 175, 55, 0.05));
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon-wrap.blue {
|
||||||
|
background: linear-gradient(135deg, rgba(26, 26, 46, 0.1), rgba(26, 26, 46, 0.05));
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon-wrap.green {
|
||||||
|
background: linear-gradient(135deg, rgba(82, 196, 26, 0.1), rgba(82, 196, 26, 0.05));
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6c757d;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
padding: 10px 20px !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gold-btn {
|
||||||
|
background: linear-gradient(135deg, #d4af37, #f5e6a3) !important;
|
||||||
|
color: #1a1a2e !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gold-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(212, 175, 55, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white-btn {
|
||||||
|
background: #fff !important;
|
||||||
|
color: #1a1a2e !important;
|
||||||
|
border: 1px solid #e9ecef !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white-btn:hover {
|
||||||
|
border-color: #d4af37 !important;
|
||||||
|
color: #d4af37 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-footer {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
text-align: center;
|
||||||
|
color: #adb5bd;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.welcome-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
251
src/views/Goods/Edit.vue
Normal file
251
src/views/Goods/Edit.vue
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
<template>
|
||||||
|
<div class="goods-edit-container">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<span>{{ isEdit ? '编辑商品' : '新增商品' }} - {{ goodsType === 'normal' ? '普通商品' : '组套商品' }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :model="form" label-width="120px" ref="formRef" :rules="rules">
|
||||||
|
<!-- 商品编码(必填) -->
|
||||||
|
<el-form-item label="商品编码" prop="code">
|
||||||
|
<el-input v-model="form.code" placeholder="请输入商品编码(唯一)" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 商品名称 -->
|
||||||
|
<el-form-item label="商品名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入商品名称" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 商品检测条形码 -->
|
||||||
|
<el-form-item label="商品检测条形码" prop="barcode">
|
||||||
|
<el-input v-model="form.barcode" placeholder="请输入商品检测条形码(必填)" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 商品分类 -->
|
||||||
|
<el-form-item label="商品分类" prop="category">
|
||||||
|
<el-select v-model="form.category" placeholder="请选择分类">
|
||||||
|
<el-option label="食品" value="food" />
|
||||||
|
<el-option label="服装" value="clothing" />
|
||||||
|
<el-option label="电子产品" value="electronics" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 单位和重量 -->
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="单位" prop="unit">
|
||||||
|
<el-input v-model="form.unit" placeholder="如:件、箱" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="重量(克)" prop="weight">
|
||||||
|
<el-input-number v-model="form.weight" :min="0" :precision="2" style="width:100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 零售价和成本价 -->
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="零售价(元)" prop="retailPrice">
|
||||||
|
<el-input-number v-model="form.retailPrice" :min="0" :precision="2" style="width:100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="成本价(元)" prop="costPrice">
|
||||||
|
<el-input-number v-model="form.costPrice" :min="0" :precision="2" style="width:100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 库存预警 -->
|
||||||
|
<el-form-item label="库存预警">
|
||||||
|
<el-input-number v-model="form.stockWarning" :min="0" style="width:200px" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 组套商品特有:子件列表 -->
|
||||||
|
<div v-if="goodsType === 'combo'">
|
||||||
|
<el-divider>组套子件</el-divider>
|
||||||
|
<div class="sub-items-toolbar">
|
||||||
|
<el-button type="primary" @click="addSubItem">添加子件</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="form.subItems" border style="width:100%">
|
||||||
|
<el-table-column prop="goodsName" label="子商品" min-width="150">
|
||||||
|
<template #default="{row, $index}">
|
||||||
|
<el-select v-model="row.goodsId" placeholder="请选择" filterable @change="(val) => handleSubItemChange(val, $index)">
|
||||||
|
<el-option v-for="item in allGoodsList" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="quantity" label="数量" width="120">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-input-number v-model="row.quantity" :min="1" style="width:100%" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="80">
|
||||||
|
<template #default="{row, $index}">
|
||||||
|
<el-button type="danger" size="small" @click="removeSubItem($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm">保存</el-button>
|
||||||
|
<el-button @click="goBack">取消</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getGoodsDetail, createGoods, updateGoods, getAllGoodsForSelect } from '@/api/goods'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const goodsId = route.params.id as string
|
||||||
|
const isEdit = computed(() => !!goodsId)
|
||||||
|
const goodsType = ref(route.query.type === 'combo' ? 'combo' : 'normal')
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const allGoodsList = ref<any[]>([])
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
barcode: '',
|
||||||
|
category: '',
|
||||||
|
unit: '',
|
||||||
|
weight: null,
|
||||||
|
retailPrice: 0,
|
||||||
|
costPrice: 0,
|
||||||
|
stockWarning: 0,
|
||||||
|
subItems: [] as { goodsId: string; goodsName?: string; quantity: number }[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
code: [{ required: true, message: '请输入商品编码', trigger: 'blur' }],
|
||||||
|
name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
|
||||||
|
barcode: [{ required: true, message: '请输入商品检测条形码', trigger: 'blur' }],
|
||||||
|
unit: [{ required: true, message: '请输入单位', trigger: 'blur' }],
|
||||||
|
costPrice: [{ required: true, message: '请输入成本价', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAllGoods = async () => {
|
||||||
|
const res = await getAllGoodsForSelect()
|
||||||
|
if (res.code === 200) {
|
||||||
|
allGoodsList.value = res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDetail = async () => {
|
||||||
|
const res = await getGoodsDetail(goodsId)
|
||||||
|
if (res.code === 200) {
|
||||||
|
const data = res.data
|
||||||
|
form.value = {
|
||||||
|
code: data.code,
|
||||||
|
name: data.name,
|
||||||
|
barcode: data.barcode,
|
||||||
|
category: data.category,
|
||||||
|
unit: data.unit,
|
||||||
|
weight: data.weight,
|
||||||
|
retailPrice: data.retail_price,
|
||||||
|
costPrice: data.cost_price,
|
||||||
|
stockWarning: data.stock_warning,
|
||||||
|
subItems: data.sub_items ? data.sub_items.map((item: any) => ({
|
||||||
|
goodsId: item.goods_id,
|
||||||
|
goodsName: item.goods_name,
|
||||||
|
quantity: item.quantity
|
||||||
|
})) : []
|
||||||
|
}
|
||||||
|
goodsType.value = data.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSubItem = () => {
|
||||||
|
form.value.subItems.push({ goodsId: '', quantity: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubItemChange = (goodsId: string, index: number) => {
|
||||||
|
const selected = allGoodsList.value.find(g => g.id === goodsId)
|
||||||
|
if (selected) {
|
||||||
|
form.value.subItems[index].goodsName = selected.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeSubItem = (index: number) => {
|
||||||
|
form.value.subItems.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
await formRef.value.validate()
|
||||||
|
|
||||||
|
const submitData: any = {
|
||||||
|
name: form.value.name,
|
||||||
|
code: form.value.code,
|
||||||
|
barcode: form.value.barcode || null,
|
||||||
|
category: form.value.category || null,
|
||||||
|
unit: form.value.unit,
|
||||||
|
weight: form.value.weight,
|
||||||
|
retail_price: form.value.retailPrice,
|
||||||
|
cost_price: form.value.costPrice,
|
||||||
|
stock_warning: form.value.stockWarning,
|
||||||
|
type: goodsType.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goodsType.value === 'combo') {
|
||||||
|
submitData.sub_items = form.value.subItems.map(item => ({
|
||||||
|
goods_id: item.goodsId,
|
||||||
|
quantity: item.quantity
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
await updateGoods(goodsId, submitData)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
await createGoods(submitData)
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
}
|
||||||
|
router.push('/goods/list')
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.status === 422) {
|
||||||
|
const errors = error.response.data.errors
|
||||||
|
console.error('验证错误:', errors)
|
||||||
|
// 将第一个错误信息展示给用户
|
||||||
|
const firstError = Object.values(errors)[0] as string[]
|
||||||
|
ElMessage.error(firstError?.[0] || '数据验证失败')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push('/goods/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadAllGoods()
|
||||||
|
if (isEdit.value) {
|
||||||
|
loadDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.goods-edit-container {
|
||||||
|
padding: 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
.sub-items-toolbar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
239
src/views/Goods/List.vue
Normal file
239
src/views/Goods/List.vue
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
<template>
|
||||||
|
<div class="goods-list-container">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>商品列表</span>
|
||||||
|
<div class="header-right">
|
||||||
|
<!-- 分类筛选下拉框 -->
|
||||||
|
<el-select v-model="categoryFilter" placeholder="全部分类" clearable style="width:150px; margin-right:10px;">
|
||||||
|
<el-option v-for="cat in categories" :key="cat.value" :label="cat.label" :value="cat.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" @click="goToAdd('normal')">新增普通商品</el-button>
|
||||||
|
<el-button type="success" @click="goToAdd('combo')">新增组套商品</el-button>
|
||||||
|
<el-button type="warning" @click="openBatchPush" :disabled="selectedGoodsIds.length === 0">批量推送</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="filteredGoodsList"
|
||||||
|
border
|
||||||
|
v-loading="loading"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
style="width:100%"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
<el-table-column prop="name" label="商品名称" min-width="180" />
|
||||||
|
<el-table-column prop="barcode" label="条码" width="150" />
|
||||||
|
<el-table-column prop="type" label="商品类型" width="100">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-tag :type="row.type === 'normal' ? 'primary' : 'success'">
|
||||||
|
{{ row.type === 'normal' ? '普通' : '组套' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="category" label="分类" width="120">
|
||||||
|
<template #default="{row}">
|
||||||
|
{{ getCategoryName(row.category) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="unit" label="单位" width="80" />
|
||||||
|
<el-table-column prop="weight" label="重量(g)" width="100" />
|
||||||
|
<el-table-column prop="retailPrice" label="零售价(元)" width="100">
|
||||||
|
<template #default="{row}">¥{{ row.retailPrice }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="costPrice" label="成本价(元)" width="100">
|
||||||
|
<template #default="{row}">¥{{ row.costPrice }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="stock" label="库存" width="100" />
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-button type="primary" size="small" @click="editGoods(row.id)">编辑</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDeleteGoods(row.id)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="loadList"
|
||||||
|
@current-change="loadList"
|
||||||
|
style="margin-top:20px; text-align:right;"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 推送对话框 -->
|
||||||
|
<GoodsPushDialog
|
||||||
|
v-model="pushDialogVisible"
|
||||||
|
:selected-goods="selectedGoodsList"
|
||||||
|
@confirm="handlePushConfirm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getGoodsList, deleteGoods, pushGoodsToCloud } from '@/api/goods'
|
||||||
|
import GoodsPushDialog from './components/GoodsPushDialog.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const goodsList = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
// 分类筛选
|
||||||
|
const categoryFilter = ref('')
|
||||||
|
const categories = [
|
||||||
|
{ label: '食品', value: 'food' },
|
||||||
|
{ label: '服装', value: 'clothing' },
|
||||||
|
{ label: '电子产品', value: 'electronics' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const getCategoryName = (value: string) => {
|
||||||
|
const cat = categories.find(c => c.value === value)
|
||||||
|
return cat ? cat.label : value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据分类筛选数据
|
||||||
|
const filteredGoodsList = computed(() => {
|
||||||
|
if (!categoryFilter.value) return goodsList.value
|
||||||
|
return goodsList.value.filter(item => item.category === categoryFilter.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 多选
|
||||||
|
const selectedGoodsIds = ref<string[]>([])
|
||||||
|
const selectedGoodsList = computed(() => goodsList.value.filter(g => selectedGoodsIds.value.includes(g.id)))
|
||||||
|
|
||||||
|
// 推送对话框
|
||||||
|
const pushDialogVisible = ref(false)
|
||||||
|
|
||||||
|
const loadList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getGoodsList({
|
||||||
|
currentPage: currentPage.value,
|
||||||
|
pageSize: pageSize.value
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
goodsList.value = res.data.list
|
||||||
|
total.value = res.data.total
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '获取商品列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载商品列表异常', error)
|
||||||
|
ElMessage.error('加载失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToAdd = (type: string) => {
|
||||||
|
console.log('goToAdd called with type:', type)
|
||||||
|
router.push(`/goods/edit?type=${type}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const editGoods = (id: string) => {
|
||||||
|
router.push(`/goods/edit/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteGoods = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除该商品吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await deleteGoods(id)
|
||||||
|
console.log('删除商品响应:', res)
|
||||||
|
|
||||||
|
if (res && res.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
await loadList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// 用户取消删除时 error 为 'cancel',不处理
|
||||||
|
if (error === 'cancel') return
|
||||||
|
|
||||||
|
// 如果是 Axios 错误,尝试获取后端返回的错误信息
|
||||||
|
if (error.response) {
|
||||||
|
const data = error.response.data
|
||||||
|
ElMessage.error(data.message || `删除失败 (${error.response.status})`)
|
||||||
|
} else if (error.message) {
|
||||||
|
ElMessage.error(error.message)
|
||||||
|
} else {
|
||||||
|
ElMessage.error('操作失败')
|
||||||
|
}
|
||||||
|
console.error('删除商品异常', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedGoodsIds.value = selection.map(item => item.id)
|
||||||
|
console.log('选中商品IDs:', selectedGoodsIds.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openBatchPush = () => {
|
||||||
|
console.log('List.vue: openBatchPush 被调用,选中商品数:', selectedGoodsIds.value.length)
|
||||||
|
if (selectedGoodsIds.value.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一个商品')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pushDialogVisible.value = true
|
||||||
|
console.log('List.vue: pushDialogVisible 设置为', pushDialogVisible.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePushConfirm = async ({ goods, warehouseId }: { goods: any[]; warehouseId: string }) => {
|
||||||
|
console.log('推送确认,商品:', goods, '仓库ID:', warehouseId)
|
||||||
|
try {
|
||||||
|
const res = await pushGoodsToCloud({ goodsIds: goods.map(g => g.id), warehouseId: Number(warehouseId) })
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('推送任务已提交')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '推送失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('推送异常', error)
|
||||||
|
ElMessage.error('推送失败')
|
||||||
|
}
|
||||||
|
pushDialogVisible.value = false
|
||||||
|
selectedGoodsIds.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.goods-list-container {
|
||||||
|
padding: 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
408
src/views/Goods/PlatformGoods.vue
Normal file
408
src/views/Goods/PlatformGoods.vue
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
<template>
|
||||||
|
<div class="platform-goods-container">
|
||||||
|
<!-- 顶部:下载按钮 -->
|
||||||
|
<div class="top-bar">
|
||||||
|
<el-button type="primary" @click="showDownloadDialog = true">
|
||||||
|
<el-icon><Download /></el-icon> 下载商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<!-- 左侧平台店铺树 -->
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="tree-card">
|
||||||
|
<template #header>
|
||||||
|
<span>平台店铺</span>
|
||||||
|
</template>
|
||||||
|
<el-tree
|
||||||
|
:data="treeData"
|
||||||
|
node-key="id"
|
||||||
|
default-expand-all
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<span class="tree-node">
|
||||||
|
<el-icon v-if="data.type === 'platform'"><Platform /></el-icon>
|
||||||
|
<el-icon v-else><Shop /></el-icon>
|
||||||
|
<span>{{ data.label }}</span>
|
||||||
|
<el-tag v-if="data.type === 'shop' && shopStats[data.shopId]" size="small" type="info">
|
||||||
|
{{ shopStats[data.shopId]?.total || 0 }} 商品
|
||||||
|
</el-tag>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 右侧商品列表 -->
|
||||||
|
<el-col :span="18">
|
||||||
|
<el-card class="goods-card">
|
||||||
|
<template #header>
|
||||||
|
<span>{{ currentShop ? currentShop.label : '请选择店铺' }} 的商品</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-if="currentShop"
|
||||||
|
:data="spuList"
|
||||||
|
border
|
||||||
|
v-loading="loading"
|
||||||
|
row-key="id"
|
||||||
|
:tree-props="{ children: 'skus', hasChildren: 'hasChildren' }"
|
||||||
|
default-expand-all
|
||||||
|
>
|
||||||
|
<el-table-column prop="name" label="商品名称" min-width="200" />
|
||||||
|
<el-table-column prop="code" label="商品编码" width="150" />
|
||||||
|
<el-table-column prop="price" label="价格(元)" width="100" />
|
||||||
|
<el-table-column label="平台状态" width="100">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-tag :type="row.status === 'onsale' ? 'success' : row.status === 'instock' ? 'info' : 'danger'">
|
||||||
|
{{ row.status === 'onsale' ? '在售' : row.status === 'instock' ? '库存中' : '已下架' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="绑定状态" width="100">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-tag :type="row.bound ? 'success' : 'info'">
|
||||||
|
{{ row.bound ? '已绑定' : '未绑定' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-button
|
||||||
|
v-if="row.type === 'sku'"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="openBindDialog(row)"
|
||||||
|
:disabled="row.bound"
|
||||||
|
>
|
||||||
|
{{ row.bound ? '已绑定' : '绑定ERP' }}
|
||||||
|
</el-button>
|
||||||
|
<span v-else-if="row.type === 'spu' && row.skus?.length">
|
||||||
|
({{ row.skus.length }}个SKU)
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-else description="请先在左侧选择店铺" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 下载商品对话框(保持不变) -->
|
||||||
|
<el-dialog v-model="showDownloadDialog" title="下载平台商品" width="500px">
|
||||||
|
<el-form :model="downloadForm" label-width="100px">
|
||||||
|
<el-form-item label="平台" required>
|
||||||
|
<el-select v-model="downloadForm.platform" placeholder="请选择平台" @change="handlePlatformChange">
|
||||||
|
<el-option label="淘宝" value="taobao" />
|
||||||
|
<el-option label="京东" value="jd" />
|
||||||
|
<el-option label="拼多多" value="pdd" />
|
||||||
|
<el-option label="抖音" value="douyin" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="店铺" required>
|
||||||
|
<el-select v-model="downloadForm.shopId" placeholder="请选择店铺" :disabled="!downloadForm.platform">
|
||||||
|
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.name" :value="shop.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="下载方式">
|
||||||
|
<el-radio-group v-model="downloadForm.downloadType">
|
||||||
|
<el-radio label="all">全部</el-radio>
|
||||||
|
<el-radio label="increment">增量</el-radio>
|
||||||
|
<el-radio label="specify">指定</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="downloadForm.downloadType === 'specify'" label="商品ID">
|
||||||
|
<el-input
|
||||||
|
v-model="downloadForm.specifyIds"
|
||||||
|
placeholder="输入商品ID,逗号分隔"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showDownloadDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleDownload" :loading="downloading">确定下载</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 绑定ERP商品对话框(保持不变) -->
|
||||||
|
<el-dialog v-model="bindDialogVisible" title="绑定ERP商品" width="500px">
|
||||||
|
<el-form :model="bindForm" label-width="100px">
|
||||||
|
<el-form-item label="平台SKU">
|
||||||
|
<span>{{ currentSku?.name }} ({{ currentSku?.code }})</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="ERP商品" required>
|
||||||
|
<el-select
|
||||||
|
v-model="bindForm.erpGoodsId"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择ERP商品"
|
||||||
|
style="width:100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in erpGoodsList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="bindDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveBind">保存绑定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Download, Platform, Shop } from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
getPlatformShopTree,
|
||||||
|
getShopsByPlatform,
|
||||||
|
downloadPlatformGoods,
|
||||||
|
getPlatformGoodsList,
|
||||||
|
bindErpSku
|
||||||
|
} from '@/api/platformGoods'
|
||||||
|
import { getAllGoodsForSelect } from '@/api/goods'
|
||||||
|
|
||||||
|
// 树数据
|
||||||
|
const treeData = ref<any[]>([])
|
||||||
|
const shopStats = ref<Record<string, { total: number }>>({})
|
||||||
|
|
||||||
|
// 当前选中的店铺
|
||||||
|
const currentShop = ref<any>(null)
|
||||||
|
|
||||||
|
// 商品列表
|
||||||
|
const spuList = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 下载对话框
|
||||||
|
const showDownloadDialog = ref(false)
|
||||||
|
const downloading = ref(false)
|
||||||
|
const downloadForm = ref({
|
||||||
|
platform: '',
|
||||||
|
shopId: '',
|
||||||
|
downloadType: 'all',
|
||||||
|
specifyIds: ''
|
||||||
|
})
|
||||||
|
const shopList = ref<any[]>([])
|
||||||
|
|
||||||
|
// 绑定对话框
|
||||||
|
const bindDialogVisible = ref(false)
|
||||||
|
const currentSku = ref<any>(null)
|
||||||
|
const bindForm = ref({ erpGoodsId: '' })
|
||||||
|
const erpGoodsList = ref<any[]>([])
|
||||||
|
|
||||||
|
// 初始化树
|
||||||
|
const loadTree = async () => {
|
||||||
|
const res = await getPlatformShopTree()
|
||||||
|
if (res.code === 200) {
|
||||||
|
treeData.value = res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理节点点击
|
||||||
|
const handleNodeClick = (data: any) => {
|
||||||
|
if (data.type === 'shop') {
|
||||||
|
currentShop.value = data
|
||||||
|
loadGoodsList(data.platform, data.shopId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成测试数据(用于空数据时展示)
|
||||||
|
const generateTestData = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'test_spu_1',
|
||||||
|
type: 'spu',
|
||||||
|
name: '测试连衣裙',
|
||||||
|
code: 'TEST-SPU-001',
|
||||||
|
price: 199,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: false,
|
||||||
|
hasChildren: true,
|
||||||
|
skus: [
|
||||||
|
{
|
||||||
|
id: 'test_sku_11',
|
||||||
|
spuId: 'test_spu_1',
|
||||||
|
type: 'sku',
|
||||||
|
name: '红色-S',
|
||||||
|
code: 'TEST-SKU-001-R-S',
|
||||||
|
price: 199,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'test_sku_12',
|
||||||
|
spuId: 'test_spu_1',
|
||||||
|
type: 'sku',
|
||||||
|
name: '红色-M',
|
||||||
|
code: 'TEST-SKU-001-R-M',
|
||||||
|
price: 199,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'test_spu_2',
|
||||||
|
type: 'spu',
|
||||||
|
name: '测试运动鞋',
|
||||||
|
code: 'TEST-SPU-002',
|
||||||
|
price: 299,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: false,
|
||||||
|
hasChildren: true,
|
||||||
|
skus: [
|
||||||
|
{
|
||||||
|
id: 'test_sku_21',
|
||||||
|
spuId: 'test_spu_2',
|
||||||
|
type: 'sku',
|
||||||
|
name: '白色-42',
|
||||||
|
code: 'TEST-SKU-002-W-42',
|
||||||
|
price: 299,
|
||||||
|
status: 'onsale',
|
||||||
|
bound: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载店铺的商品列表
|
||||||
|
const loadGoodsList = async (platform: string, shopId: string) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getPlatformGoodsList({ platform, shopId })
|
||||||
|
console.log('平台商品原始数据:', res.data)
|
||||||
|
if (res.code === 200) {
|
||||||
|
if (res.data && res.data.length > 0) {
|
||||||
|
// 为每个SPU添加 hasChildren: true
|
||||||
|
const processedData = res.data.map((item: any) => {
|
||||||
|
if (item.type === 'spu') {
|
||||||
|
return { ...item, hasChildren: true }
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
spuList.value = processedData
|
||||||
|
} else {
|
||||||
|
console.warn('平台商品数据为空,使用测试数据')
|
||||||
|
spuList.value = generateTestData()
|
||||||
|
}
|
||||||
|
shopStats.value[shopId] = { total: spuList.value.length }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台变化时加载店铺列表
|
||||||
|
const handlePlatformChange = async () => {
|
||||||
|
downloadForm.value.shopId = ''
|
||||||
|
if (!downloadForm.value.platform) {
|
||||||
|
shopList.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await getShopsByPlatform(downloadForm.value.platform)
|
||||||
|
if (res.code === 200) {
|
||||||
|
shopList.value = res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始下载
|
||||||
|
const handleDownload = async () => {
|
||||||
|
console.log('handleDownload 被调用,参数:', downloadForm.value)
|
||||||
|
if (!downloadForm.value.platform || !downloadForm.value.shopId) {
|
||||||
|
ElMessage.warning('请选择平台和店铺')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (downloadForm.value.downloadType === 'specify' && !downloadForm.value.specifyIds) {
|
||||||
|
ElMessage.warning('请输入商品ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
downloading.value = true
|
||||||
|
try {
|
||||||
|
const res = await downloadPlatformGoods({
|
||||||
|
platform: downloadForm.value.platform,
|
||||||
|
shopId: downloadForm.value.shopId,
|
||||||
|
downloadType: downloadForm.value.downloadType,
|
||||||
|
specifyIds: downloadForm.value.specifyIds
|
||||||
|
})
|
||||||
|
console.log('下载结果:', res)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('下载任务已提交')
|
||||||
|
showDownloadDialog.value = false
|
||||||
|
if (currentShop.value?.platform === downloadForm.value.platform && currentShop.value?.shopId === downloadForm.value.shopId) {
|
||||||
|
console.log('重新加载商品列表')
|
||||||
|
setTimeout(() => {
|
||||||
|
loadGoodsList(downloadForm.value.platform, downloadForm.value.shopId)
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '下载失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载异常:', error)
|
||||||
|
ElMessage.error('下载失败')
|
||||||
|
} finally {
|
||||||
|
downloading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开绑定对话框
|
||||||
|
const openBindDialog = async (sku: any) => {
|
||||||
|
currentSku.value = sku
|
||||||
|
bindForm.value = { erpGoodsId: '' }
|
||||||
|
const res = await getAllGoodsForSelect()
|
||||||
|
if (res.code === 200) {
|
||||||
|
erpGoodsList.value = res.data
|
||||||
|
}
|
||||||
|
bindDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存绑定
|
||||||
|
const saveBind = async () => {
|
||||||
|
if (!bindForm.value.erpGoodsId) {
|
||||||
|
ElMessage.warning('请选择ERP商品')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await bindErpSku({
|
||||||
|
skuId: currentSku.value.id,
|
||||||
|
erpGoodsId: bindForm.value.erpGoodsId
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('绑定成功')
|
||||||
|
bindDialogVisible.value = false
|
||||||
|
currentSku.value.bound = true
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '绑定失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadTree()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.platform-goods-container {
|
||||||
|
padding: 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
.top-bar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.tree-card, .goods-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.tree-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
72
src/views/Goods/PushLog.vue
Normal file
72
src/views/Goods/PushLog.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="push-log-container">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<span>商品推送日志</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="logList" border v-loading="loading" style="width:100%">
|
||||||
|
<el-table-column prop="goodsName" label="商品名称" min-width="150" />
|
||||||
|
<el-table-column prop="goodsCode" label="商品编码" width="120" />
|
||||||
|
<el-table-column prop="cloudSystem" label="云仓系统" width="100" />
|
||||||
|
<el-table-column prop="pushTime" label="推送时间" width="160" />
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{row}">
|
||||||
|
<el-tag :type="row.status === 'success' ? 'success' : 'danger'">
|
||||||
|
{{ row.status === 'success' ? '成功' : '失败' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="errorMsg" label="失败原因" min-width="200" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="loadLogs"
|
||||||
|
@current-change="loadLogs"
|
||||||
|
style="margin-top:20px; text-align:right;"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { getPushLogs } from '@/api/goods'
|
||||||
|
|
||||||
|
const logList = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
const loadLogs = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getPushLogs({
|
||||||
|
currentPage: currentPage.value,
|
||||||
|
pageSize: pageSize.value
|
||||||
|
})
|
||||||
|
logList.value = res.data.list
|
||||||
|
total.value = res.data.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadLogs()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.push-log-container {
|
||||||
|
padding: 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
97
src/views/Goods/components/GoodsPushDialog.vue
Normal file
97
src/views/Goods/components/GoodsPushDialog.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" title="商品推送" width="600px" @close="handleClose">
|
||||||
|
<!-- 显示待推送商品列表(不可再选) -->
|
||||||
|
<el-table :data="selectedGoods" border style="width:100%; margin-bottom:15px;">
|
||||||
|
<el-table-column prop="name" label="商品名称" min-width="150" />
|
||||||
|
<el-table-column prop="barcode" label="条码" width="120" />
|
||||||
|
<el-table-column prop="retailPrice" label="零售价" width="100">
|
||||||
|
<template #default="{row}">¥{{ row.retailPrice }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 仓库选择(仅云仓) -->
|
||||||
|
<el-form-item label="目标仓库" required>
|
||||||
|
<el-select v-model="selectedWarehouseId" placeholder="请选择仓库" style="width:100%">
|
||||||
|
<el-option
|
||||||
|
v-for="warehouse in cloudWarehouseList"
|
||||||
|
:key="warehouse.id"
|
||||||
|
:label="warehouse.name"
|
||||||
|
:value="warehouse.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleConfirm" :disabled="!selectedWarehouseId">
|
||||||
|
确认推送
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import { getWarehouseList } from '@/api/warehouse'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
selectedGoods: { type: Array, default: () => [] }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||||
|
|
||||||
|
// 使用 computed 实现双向绑定
|
||||||
|
const dialogVisible = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const cloudWarehouseList = ref([])
|
||||||
|
const selectedWarehouseId = ref('')
|
||||||
|
|
||||||
|
// 监听对话框打开事件
|
||||||
|
watch(() => props.modelValue, async (val) => {
|
||||||
|
console.log('GoodsPushDialog: props.modelValue 变为', val)
|
||||||
|
if (val) {
|
||||||
|
console.log('GoodsPushDialog: 开始加载云仓数据')
|
||||||
|
await loadCloudWarehouses()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadCloudWarehouses = async () => {
|
||||||
|
console.log('GoodsPushDialog: loadCloudWarehouses 开始执行')
|
||||||
|
try {
|
||||||
|
const res = await getWarehouseList({ pageSize: 100 })
|
||||||
|
console.log('GoodsPushDialog: getWarehouseList 返回完整响应', JSON.stringify(res, null, 2))
|
||||||
|
if (res.code === 200) {
|
||||||
|
// 兼容两种数据格式:如果 res.data 直接是数组,则使用 res.data;否则使用 res.data?.list
|
||||||
|
let list = Array.isArray(res.data) ? res.data : (res.data?.list || [])
|
||||||
|
console.log('GoodsPushDialog: 原始仓库列表', list)
|
||||||
|
// 只保留云仓类型
|
||||||
|
cloudWarehouseList.value = list.filter(w => w.type === 'cloud')
|
||||||
|
console.log('GoodsPushDialog: 过滤后的云仓列表', cloudWarehouseList.value)
|
||||||
|
if (cloudWarehouseList.value.length === 0) {
|
||||||
|
console.warn('GoodsPushDialog: 未找到云仓,请检查数据字段名或创建云仓')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('GoodsPushDialog: 返回码异常', res)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GoodsPushDialog: 获取仓库列表失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emit('confirm', {
|
||||||
|
goods: props.selectedGoods,
|
||||||
|
warehouseId: selectedWarehouseId.value
|
||||||
|
})
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
selectedWarehouseId.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
123
src/views/Goods/components/PlatformGoodsDownloadDialog.vue
Normal file
123
src/views/Goods/components/PlatformGoodsDownloadDialog.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" title="下载平台商品" width="500px" @close="handleClose">
|
||||||
|
<el-form :model="form" label-width="100px">
|
||||||
|
<el-form-item label="平台" required>
|
||||||
|
<el-select v-model="form.platform" placeholder="请选择平台" @change="handlePlatformChange">
|
||||||
|
<el-option label="淘宝" value="taobao" />
|
||||||
|
<el-option label="京东" value="jd" />
|
||||||
|
<el-option label="拼多多" value="pdd" />
|
||||||
|
<el-option label="抖音" value="douyin" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="店铺" required>
|
||||||
|
<el-select v-model="form.shopId" placeholder="请选择店铺" :disabled="!form.platform">
|
||||||
|
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.name" :value="shop.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="下载方式">
|
||||||
|
<el-radio-group v-model="form.downloadType">
|
||||||
|
<el-radio label="all">全部</el-radio>
|
||||||
|
<el-radio label="increment">增量</el-radio>
|
||||||
|
<el-radio label="specify">指定</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.downloadType === 'specify'" label="商品ID">
|
||||||
|
<el-input
|
||||||
|
v-model="form.specifyIds"
|
||||||
|
placeholder="输入商品ID,逗号分隔"
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleDownload" :loading="downloading">开始下载</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getShopsByPlatform, downloadPlatformGoods } from '@/api/platformGoods'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
shop: { type: Object, default: null } // 当前选中的店铺(可能预填充)
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue', 'success'])
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const downloading = ref(false)
|
||||||
|
const form = ref({
|
||||||
|
platform: '',
|
||||||
|
shopId: '',
|
||||||
|
downloadType: 'all',
|
||||||
|
specifyIds: ''
|
||||||
|
})
|
||||||
|
const shopList = ref([])
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
dialogVisible.value = val
|
||||||
|
if (val && props.shop) {
|
||||||
|
// 如果已经有选中的店铺,可以预填平台和店铺
|
||||||
|
form.value.platform = props.shop.platform
|
||||||
|
form.value.shopId = props.shop.id
|
||||||
|
} else {
|
||||||
|
// 重置表单
|
||||||
|
form.value = { platform: '', shopId: '', downloadType: 'all', specifyIds: '' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(dialogVisible, (val) => {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handlePlatformChange = async () => {
|
||||||
|
form.value.shopId = ''
|
||||||
|
if (!form.value.platform) {
|
||||||
|
shopList.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await getShopsByPlatform(form.value.platform)
|
||||||
|
if (res.code === 200) {
|
||||||
|
shopList.value = res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (!form.value.platform || !form.value.shopId) {
|
||||||
|
ElMessage.warning('请选择平台和店铺')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (form.value.downloadType === 'specify' && !form.value.specifyIds) {
|
||||||
|
ElMessage.warning('请输入商品ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
downloading.value = true
|
||||||
|
try {
|
||||||
|
const res = await downloadPlatformGoods({
|
||||||
|
platform: form.value.platform,
|
||||||
|
shopId: form.value.shopId,
|
||||||
|
downloadType: form.value.downloadType,
|
||||||
|
specifyIds: form.value.specifyIds
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('下载任务已提交')
|
||||||
|
emit('success')
|
||||||
|
dialogVisible.value = false
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '下载失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
downloading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
355
src/views/Log/operation/OperationLog.vue
Normal file
355
src/views/Log/operation/OperationLog.vue
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
<template>
|
||||||
|
<div class="operation-log-container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<el-page-header content="操作日志管理" @back="goBack"></el-page-header>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选查询区域 -->
|
||||||
|
<el-card class="search-card">
|
||||||
|
<el-form :model="searchForm" inline @submit.prevent="fetchLogList">
|
||||||
|
<el-form-item label="操作人">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.operator"
|
||||||
|
placeholder="请输入操作人账号/姓名"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="操作类型">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.operationType"
|
||||||
|
placeholder="请选择操作类型"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
>
|
||||||
|
<el-option label="新增" value="ADD"></el-option>
|
||||||
|
<el-option label="编辑" value="EDIT"></el-option>
|
||||||
|
<el-option label="删除" value="DELETE"></el-option>
|
||||||
|
<el-option label="查询" value="QUERY"></el-option>
|
||||||
|
<el-option label="配置修改" value="CONFIG"></el-option>
|
||||||
|
<el-option label="登录/退出" value="LOGIN"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="操作模块">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.module"
|
||||||
|
placeholder="请选择操作模块"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
>
|
||||||
|
<el-option label="WDT配置" value="WDT_CONFIG"></el-option>
|
||||||
|
<el-option label="订单管理" value="ORDER"></el-option>
|
||||||
|
<el-option label="商品管理" value="GOODS"></el-option>
|
||||||
|
<el-option label="系统管理" value="SYSTEM"></el-option>
|
||||||
|
<el-option label="操作日志" value="LOG"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="操作时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="searchForm.timeRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
clearable
|
||||||
|
style="width: 350px"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="fetchLogList">查询</el-button>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
<el-button type="success" @click="exportLog">导出日志</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card> <!-- 补全search-card的闭合标签 -->
|
||||||
|
|
||||||
|
<!-- 日志列表 -->
|
||||||
|
<el-card class="log-list-card">
|
||||||
|
<el-table
|
||||||
|
:data="logList"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="加载中..."
|
||||||
|
highlight-current-row
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="日志ID" width="80" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="operator" label="操作人" width="120" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="operationType" label="操作类型" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getTagType(scope.row.operationType)">
|
||||||
|
{{ getOperationTypeName(scope.row.operationType) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="module" label="操作模块" width="120" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ getModuleName(scope.row.module) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="operationContent" label="操作内容" min-width="300"></el-table-column>
|
||||||
|
<el-table-column prop="ipAddress" label="操作IP" width="150" align="center"></el-table-column>
|
||||||
|
<el-table-column prop="operationTime" label="操作时间" width="200" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatTime(scope.row.operationTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="text" @click="viewDetail(scope.row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
:current-page="pagination.currentPage"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:page-size="pagination.pageSize"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="pagination.total"
|
||||||
|
style="margin-top: 20px; text-align: right"
|
||||||
|
></el-pagination>
|
||||||
|
</el-card> <!-- 补全log-list-card的闭合标签 -->
|
||||||
|
|
||||||
|
<!-- 日志详情弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
title="操作日志详情"
|
||||||
|
width="60%"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-descriptions :column="1" border v-if="currentLog">
|
||||||
|
<el-descriptions-item label="日志ID">{{ currentLog.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作人">{{ currentLog.operator }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作人账号">{{ currentLog.operatorAccount || '未知' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作类型">{{ getOperationTypeName(currentLog.operationType) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作模块">{{ getModuleName(currentLog.module) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作内容">{{ currentLog.operationContent }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作IP">{{ currentLog.ipAddress }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作时间">{{ formatTime(currentLog.operationTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作结果">{{ currentLog.operationResult || '成功' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注/错误信息" v-if="currentLog.remark">
|
||||||
|
{{ currentLog.remark }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div> <!-- 补全根容器的闭合标签 -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import dayjs from 'dayjs' // 需安装:npm install dayjs
|
||||||
|
|
||||||
|
// ===================== 基础数据定义 =====================
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
operator: '', // 操作人
|
||||||
|
operationType: '', // 操作类型
|
||||||
|
module: '', // 操作模块
|
||||||
|
timeRange: [] // 操作时间范围
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 日志列表
|
||||||
|
const logList = ref([])
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
// 详情弹窗
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentLog = ref(null)
|
||||||
|
|
||||||
|
// ===================== 方法定义 =====================
|
||||||
|
// 页面初始化加载日志列表
|
||||||
|
onMounted(() => {
|
||||||
|
fetchLogList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取日志列表(目前是模拟数据,后续替换为真实接口)
|
||||||
|
const fetchLogList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 模拟后端接口请求,后续替换为真实接口调用
|
||||||
|
const mockData = {
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
list: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
operator: '管理员',
|
||||||
|
operatorAccount: 'admin',
|
||||||
|
operationType: 'CONFIG',
|
||||||
|
module: 'WDT_CONFIG',
|
||||||
|
operationContent: '修改WDT配置:服务器地址从http://192.168.1.100改为http://192.168.1.101,端口8080不变',
|
||||||
|
ipAddress: '127.0.0.1',
|
||||||
|
operationTime: new Date().getTime() - 3600 * 1000,
|
||||||
|
operationResult: '成功',
|
||||||
|
remark: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
operator: '测试账号',
|
||||||
|
operatorAccount: 'test',
|
||||||
|
operationType: 'LOGIN',
|
||||||
|
module: 'SYSTEM',
|
||||||
|
operationContent: '用户登录系统',
|
||||||
|
ipAddress: '192.168.1.2',
|
||||||
|
operationTime: new Date().getTime() - 7200 * 1000,
|
||||||
|
operationResult: '成功',
|
||||||
|
remark: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
operator: '管理员',
|
||||||
|
operatorAccount: 'admin',
|
||||||
|
operationType: 'DELETE',
|
||||||
|
module: 'ORDER',
|
||||||
|
operationContent: '删除订单ID:OD20260306001',
|
||||||
|
ipAddress: '127.0.0.1',
|
||||||
|
operationTime: new Date().getTime() - 86400 * 1000,
|
||||||
|
operationResult: '失败',
|
||||||
|
remark: '订单已发货,不允许删除'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
total: 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mockData.code === 200) {
|
||||||
|
logList.value = mockData.data.list
|
||||||
|
pagination.total = mockData.data.total
|
||||||
|
} else {
|
||||||
|
ElMessage.error(mockData.msg || '获取操作日志失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('网络异常,获取操作日志失败')
|
||||||
|
console.error('获取操作日志失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索条件
|
||||||
|
const resetSearch = () => {
|
||||||
|
searchForm.operator = ''
|
||||||
|
searchForm.operationType = ''
|
||||||
|
searchForm.module = ''
|
||||||
|
searchForm.timeRange = []
|
||||||
|
pagination.currentPage = 1
|
||||||
|
fetchLogList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出日志
|
||||||
|
const exportLog = async () => {
|
||||||
|
try {
|
||||||
|
// 模拟导出接口
|
||||||
|
ElMessage.success('日志导出成功,文件将自动下载')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('日志导出失败')
|
||||||
|
console.error('导出日志失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看日志详情
|
||||||
|
const viewDetail = (row) => {
|
||||||
|
currentLog.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页尺寸变更
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
pagination.pageSize = val
|
||||||
|
fetchLogList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页页码变更
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
pagination.currentPage = val
|
||||||
|
fetchLogList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
window.history.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 辅助方法 =====================
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timestamp) => {
|
||||||
|
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取操作类型名称
|
||||||
|
const getOperationTypeName = (type) => {
|
||||||
|
const typeMap = {
|
||||||
|
ADD: '新增',
|
||||||
|
EDIT: '编辑',
|
||||||
|
DELETE: '删除',
|
||||||
|
QUERY: '查询',
|
||||||
|
CONFIG: '配置修改',
|
||||||
|
LOGIN: '登录/退出'
|
||||||
|
}
|
||||||
|
return typeMap[type] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取模块名称
|
||||||
|
const getModuleName = (module) => {
|
||||||
|
const moduleMap = {
|
||||||
|
WDT_CONFIG: 'WDT配置',
|
||||||
|
ORDER: '订单管理',
|
||||||
|
GOODS: '商品管理',
|
||||||
|
SYSTEM: '系统管理',
|
||||||
|
LOG: '操作日志'
|
||||||
|
}
|
||||||
|
return moduleMap[module] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取操作类型标签样式
|
||||||
|
const getTagType = (type) => {
|
||||||
|
const typeMap = {
|
||||||
|
ADD: 'success',
|
||||||
|
EDIT: 'primary',
|
||||||
|
DELETE: 'danger',
|
||||||
|
QUERY: 'info',
|
||||||
|
CONFIG: 'warning',
|
||||||
|
LOGIN: 'info'
|
||||||
|
}
|
||||||
|
return typeMap[type] || 'default'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.operation-log-container {
|
||||||
|
padding: 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.log-list-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user