Initial commit: ERP Frontend baseline

This commit is contained in:
Yatu 2026-04-01 17:07:17 +08:00
commit 9f64a4d6f4
139 changed files with 30546 additions and 0 deletions

3
.env.development Normal file
View 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
View File

@ -0,0 +1 @@
VITE_API_BASE_URL=http://111.229.80.149/api

24
.gitignore vendored Normal file
View 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
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

102
LOGIN_TESTING.md Normal file
View 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
View 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
View File

22
eslint.config.js Normal file
View 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
View 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>

0
npm Normal file
View File

3571
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View 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
View 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
View 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
View 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
View 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>

View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

0
s.id Normal file
View File

3
src/App.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<router-view />
</template>

58
src/api/afterSale.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

93
src/api/shop.ts Normal file
View 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
View 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 IDSKU
*/
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
View 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
View 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
View 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
View 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
View File

View File

61
src/api/utils/request.ts Normal file
View 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
View 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
View File

1
src/assets/vue.svg Normal file
View 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

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve 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>

View 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>

View 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>
Vues
<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>

View 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>

View 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')
})
})

View File

View File

View File

View 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>

View 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>

View 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>

View 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>

View 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>

View File

View 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>

317
src/composables/usePrint.ts Normal file
View 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
}
}

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

203
src/mock/data.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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)">&nbsp;</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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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: '删除订单IDOD20260306001',
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