init
This commit is contained in:
14
admin/.babelrc
Normal file
14
admin/.babelrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
2
admin/.env.prod
Normal file
2
admin/.env.prod
Normal file
@@ -0,0 +1,2 @@
|
||||
# 生产环境(youchang 接口)
|
||||
BUILD_ENV=prod
|
||||
2
admin/.env.sit
Normal file
2
admin/.env.sit
Normal file
@@ -0,0 +1,2 @@
|
||||
# SIT 环境(tennis 接口)
|
||||
BUILD_ENV=sit
|
||||
24
admin/.gitignore
vendored
Normal file
24
admin/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# 依赖
|
||||
node_modules/
|
||||
|
||||
# 构建输出
|
||||
dist/
|
||||
|
||||
# 日志
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# 编辑器
|
||||
.vscode/
|
||||
.idea/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
251
admin/README.md
Normal file
251
admin/README.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Admin Framework Demo
|
||||
|
||||
本目录包含 Admin Framework 的使用示例,提供两种使用方式:
|
||||
|
||||
## 📁 文件说明
|
||||
|
||||
### 🌐 CDN 版本(快速体验)
|
||||
- **index.html** - 基础示例(CDN)
|
||||
- **advanced.html** - 高级示例(CDN)
|
||||
|
||||
适合快速体验,所有依赖从 CDN 加载,无需安装。
|
||||
|
||||
### 💻 本地开发版本(推荐开发使用)
|
||||
- **src/** - 源代码目录
|
||||
- **main.js** - 基础示例入口
|
||||
- **main-advanced.js** - 高级示例入口
|
||||
- **components/** - 自定义组件
|
||||
- **package.json** - 依赖配置
|
||||
- **webpack.config.js** - 构建配置
|
||||
|
||||
所有依赖本地安装,支持热更新,适合开发调试。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用方式
|
||||
|
||||
### 方式一:CDN 版本(快速体验)
|
||||
|
||||
#### index.html - 基础示例
|
||||
最简单的使用示例,展示如何:
|
||||
- 引入必要的依赖
|
||||
- 初始化框架
|
||||
- 创建基本应用
|
||||
|
||||
#### advanced.html - 高级示例
|
||||
完整的使用示例,展示如何:
|
||||
- 添加自定义页面组件
|
||||
- 注册自定义 Vuex 模块
|
||||
- 添加自定义路由
|
||||
- 配置路由守卫
|
||||
- 配置 Axios 拦截器
|
||||
- 使用组件映射
|
||||
|
||||
### 方式二:本地开发版本(推荐)
|
||||
|
||||
查看详细文档:[README-LOCAL.md](./README-LOCAL.md)
|
||||
|
||||
快速开始:
|
||||
```bash
|
||||
# 1. 构建框架(在项目根目录)
|
||||
cd ..
|
||||
npm run build
|
||||
|
||||
# 2. 安装 demo 依赖
|
||||
cd demo
|
||||
npm install
|
||||
|
||||
# 3. 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 使用步骤
|
||||
|
||||
### 1. 构建框架
|
||||
首先需要构建 admin-framework:
|
||||
|
||||
```bash
|
||||
# 生产构建(压缩,无 sourcemap)
|
||||
npm run build
|
||||
|
||||
# 开发构建(不压缩,有 sourcemap)
|
||||
npm run build:dev
|
||||
```
|
||||
|
||||
### 2. 启动示例
|
||||
|
||||
有以下几种方式启动示例:
|
||||
|
||||
#### 方式一:使用 Live Server(推荐)
|
||||
1. 安装 VS Code 的 Live Server 插件
|
||||
2. 右键 `index.html` 或 `advanced.html`
|
||||
3. 选择 "Open with Live Server"
|
||||
|
||||
#### 方式二:使用 HTTP 服务器
|
||||
```bash
|
||||
# 安装 http-server
|
||||
npm install -g http-server
|
||||
|
||||
# 在项目根目录运行
|
||||
http-server
|
||||
|
||||
# 访问
|
||||
# http://localhost:8080/demo/index.html
|
||||
# http://localhost:8080/demo/advanced.html
|
||||
```
|
||||
|
||||
#### 方式三:直接打开
|
||||
- 双击 HTML 文件在浏览器中打开
|
||||
- 注意:某些功能可能因跨域限制无法使用
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 基本配置
|
||||
```javascript
|
||||
const config = {
|
||||
title: '系统标题',
|
||||
apiUrl: 'http://your-api.com/api/', // API 基础地址
|
||||
uploadUrl: 'http://your-api.com/api/upload' // 上传接口地址
|
||||
}
|
||||
```
|
||||
|
||||
### 初始化框架
|
||||
```javascript
|
||||
framework.install(Vue, {
|
||||
config: config, // 配置对象
|
||||
ViewUI: iview, // iView 实例
|
||||
VueRouter: VueRouter, // Vue Router
|
||||
Vuex: Vuex, // Vuex
|
||||
createPersistedState: null, // Vuex 持久化插件(可选)
|
||||
componentMap: {} // 自定义组件映射
|
||||
})
|
||||
```
|
||||
|
||||
## 内置功能
|
||||
|
||||
### 1. 系统页面
|
||||
- **登录页面**: `/login`
|
||||
- **首页**: `/home`
|
||||
- **错误页面**: `/401`, `/404`, `/500`
|
||||
|
||||
### 2. 系统管理
|
||||
- **用户管理**: 系统用户的增删改查
|
||||
- **角色管理**: 角色权限管理
|
||||
- **菜单管理**: 动态菜单配置
|
||||
- **日志管理**: 系统操作日志
|
||||
|
||||
### 3. 高级功能
|
||||
- **动态表单**: 基于配置生成表单
|
||||
- **动态表格**: 可配置的数据表格
|
||||
- **文件上传**: 单文件/多文件上传
|
||||
- **富文本编辑器**: WangEditor
|
||||
- **代码编辑器**: Ace Editor
|
||||
|
||||
## API 使用
|
||||
|
||||
### HTTP 请求
|
||||
```javascript
|
||||
// GET 请求
|
||||
framework.http.get('/api/users').then(res => {
|
||||
console.log(res.data)
|
||||
})
|
||||
|
||||
// POST 请求
|
||||
framework.http.post('/api/users', {
|
||||
name: '张三',
|
||||
age: 25
|
||||
}).then(res => {
|
||||
console.log(res.data)
|
||||
})
|
||||
|
||||
// 在组件中使用
|
||||
this.$http.get('/api/users').then(res => {
|
||||
console.log(res.data)
|
||||
})
|
||||
```
|
||||
|
||||
### 工具函数
|
||||
```javascript
|
||||
// 使用框架提供的工具函数
|
||||
const tools = framework.tools
|
||||
|
||||
// 日期格式化
|
||||
tools.formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss')
|
||||
|
||||
// 深拷贝
|
||||
tools.deepClone(obj)
|
||||
|
||||
// 防抖
|
||||
tools.debounce(fn, 500)
|
||||
|
||||
// 节流
|
||||
tools.throttle(fn, 500)
|
||||
```
|
||||
|
||||
### UI 工具
|
||||
```javascript
|
||||
// 使用 UI 工具
|
||||
const uiTool = framework.uiTool
|
||||
|
||||
// 成功提示
|
||||
window.framework.uiTool.success('操作成功')
|
||||
|
||||
// 错误提示
|
||||
window.framework.uiTool.error('操作失败')
|
||||
|
||||
// 确认对话框
|
||||
window.framework.uiTool.confirm('确定删除吗?').then(() => {
|
||||
// 确认后的操作
|
||||
})
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 依赖库版本
|
||||
确保使用以下版本的依赖库:
|
||||
- Vue: 2.6.x
|
||||
- Vue Router: 3.x
|
||||
- Vuex: 3.x
|
||||
- iView (view-design): 4.x
|
||||
- Axios: 0.21.x+
|
||||
|
||||
### 2. 路径问题
|
||||
如果无法加载 admin-framework.js,检查路径是否正确:
|
||||
```html
|
||||
<!-- 确保路径指向正确的文件 -->
|
||||
<script src="../dist/admin-framework.js"></script>
|
||||
```
|
||||
|
||||
### 3. API 地址
|
||||
记得修改配置中的 API 地址为实际的后端地址:
|
||||
```javascript
|
||||
const config = {
|
||||
apiUrl: 'http://your-real-api.com/api/',
|
||||
uploadUrl: 'http://your-real-api.com/api/upload'
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 跨域问题
|
||||
如果遇到跨域问题,需要配置后端 CORS 或使用代理。
|
||||
|
||||
## 开发建议
|
||||
|
||||
1. **开发时使用 build:dev**
|
||||
- 生成 sourcemap,方便调试
|
||||
- 代码不压缩,易读
|
||||
|
||||
2. **生产时使用 build**
|
||||
- 代码压缩,体积小
|
||||
- 无 sourcemap,安全
|
||||
|
||||
3. **使用浏览器调试工具**
|
||||
```javascript
|
||||
// 所有实例都挂载到 window 上,方便调试
|
||||
window.app // Vue 实例
|
||||
window.framework // 框架实例
|
||||
```
|
||||
|
||||
## 更多信息
|
||||
|
||||
查看完整文档:`../_doc/完整使用文档.md`
|
||||
|
||||
248
admin/config/README.md
Normal file
248
admin/config/README.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# Admin 前端配置说明
|
||||
|
||||
## 📁 配置文件结构
|
||||
|
||||
```
|
||||
admin/
|
||||
├── config/
|
||||
│ ├── index.js # 主配置文件(支持多环境)
|
||||
│ └── README.md # 配置说明文档(本文件)
|
||||
├── env.development # 开发环境变量
|
||||
├── env.test # 测试环境变量
|
||||
└── env.production # 生产环境变量
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置项说明
|
||||
|
||||
### 基础配置
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `title` | String | '仓库管理系统' | 系统标题 |
|
||||
| `apiUrl` | String | - | 后端 API 地址 |
|
||||
| `uploadUrl` | String | - | 文件上传地址 |
|
||||
| `showSettings` | Boolean | true | 是否显示设置按钮 |
|
||||
| `showTagsView` | Boolean | true | 是否显示标签栏 |
|
||||
| `fixedHeader` | Boolean | true | 是否固定头部 |
|
||||
| `sidebarLogo` | Boolean | true | 是否显示侧边栏 Logo |
|
||||
| `cookieExpires` | Number | 1 | Token 在 Cookie 中存储的天数 |
|
||||
| `themeColor` | String | '#2d8cf0' | 系统主题色 |
|
||||
| `debug` | Boolean | false | 是否开启调试模式 |
|
||||
|
||||
---
|
||||
|
||||
## 🌍 环境配置
|
||||
|
||||
### 开发环境(development)
|
||||
|
||||
```javascript
|
||||
{
|
||||
apiUrl: 'http://localhost:9098/admin_api/',
|
||||
uploadUrl: 'http://localhost:9098/admin_api/upload',
|
||||
debug: true // 开发环境显示调试信息
|
||||
}
|
||||
```
|
||||
|
||||
**启动命令**:
|
||||
```bash
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### 测试环境(test)
|
||||
|
||||
```javascript
|
||||
{
|
||||
apiUrl: 'http://test.yourdomain.com/admin_api/',
|
||||
uploadUrl: 'http://test.yourdomain.com/admin_api/upload',
|
||||
debug: false
|
||||
}
|
||||
```
|
||||
|
||||
**启动命令**:
|
||||
```bash
|
||||
npm run build:test
|
||||
```
|
||||
|
||||
### 生产环境(production)
|
||||
|
||||
```javascript
|
||||
{
|
||||
apiUrl: 'https://api.yourdomain.com/admin_api/',
|
||||
uploadUrl: 'https://api.yourdomain.com/admin_api/upload',
|
||||
debug: false
|
||||
}
|
||||
```
|
||||
|
||||
**启动命令**:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 使用方法
|
||||
|
||||
### 在组件中使用配置
|
||||
|
||||
框架已将配置挂载到 `Vue.prototype.$config`,可以在任何组件中使用:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ $config.title }}</h1>
|
||||
<p>API 地址: {{ $config.apiUrl }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
console.log('系统标题:', this.$config.title)
|
||||
console.log('API 地址:', this.$config.apiUrl)
|
||||
console.log('是否调试模式:', this.$config.debug)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 在 API 服务中使用配置
|
||||
|
||||
HTTP 工具已自动使用 `apiUrl` 作为基础路径:
|
||||
|
||||
```javascript
|
||||
// 无需手动拼接 apiUrl,框架会自动处理
|
||||
this.$http.get('/user/list')
|
||||
// 实际请求: http://localhost:9098/admin_api/user/list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 修改配置
|
||||
|
||||
### 修改基础配置
|
||||
|
||||
编辑 `config/index.js` 中的 `baseConfig`:
|
||||
|
||||
```javascript
|
||||
const baseConfig = {
|
||||
title: '你的系统名称', // 修改系统标题
|
||||
themeColor: '#409EFF', // 修改主题色
|
||||
// ... 其他配置
|
||||
}
|
||||
```
|
||||
|
||||
### 修改环境配置
|
||||
|
||||
编辑对应环境的配置对象:
|
||||
|
||||
```javascript
|
||||
// 开发环境配置
|
||||
const developmentConfig = {
|
||||
...baseConfig,
|
||||
apiUrl: 'http://localhost:9098/admin_api/', // 修改开发环境 API 地址
|
||||
uploadUrl: 'http://localhost:9098/admin_api/upload',
|
||||
debug: true
|
||||
}
|
||||
|
||||
// 生产环境配置
|
||||
const productionConfig = {
|
||||
...baseConfig,
|
||||
apiUrl: 'https://api.yourdomain.com/admin_api/', // 修改生产环境 API 地址
|
||||
uploadUrl: 'https://api.yourdomain.com/admin_api/upload',
|
||||
debug: false
|
||||
}
|
||||
```
|
||||
|
||||
### 添加自定义配置
|
||||
|
||||
可以在任何环境配置中添加自定义字段:
|
||||
|
||||
```javascript
|
||||
const developmentConfig = {
|
||||
...baseConfig,
|
||||
apiUrl: 'http://localhost:9098/admin_api/',
|
||||
uploadUrl: 'http://localhost:9098/admin_api/upload',
|
||||
|
||||
// 自定义配置
|
||||
enableMock: true,
|
||||
websocketUrl: 'ws://localhost:9099',
|
||||
maxFileSize: 10 * 1024 * 1024, // 10MB
|
||||
allowedFileTypes: ['image/jpeg', 'image/png', 'application/pdf']
|
||||
}
|
||||
```
|
||||
|
||||
然后在组件中使用:
|
||||
|
||||
```javascript
|
||||
if (this.$config.enableMock) {
|
||||
console.log('启用 Mock 数据')
|
||||
}
|
||||
|
||||
const ws = new WebSocket(this.$config.websocketUrl)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 部署说明
|
||||
|
||||
### 开发环境部署
|
||||
|
||||
```bash
|
||||
# 启动开发服务器
|
||||
npm run serve
|
||||
|
||||
# 或使用 yarn
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### 测试环境部署
|
||||
|
||||
```bash
|
||||
# 构建测试环境代码
|
||||
npm run build:test
|
||||
|
||||
# 将 dist 目录部署到测试服务器
|
||||
```
|
||||
|
||||
### 生产环境部署
|
||||
|
||||
```bash
|
||||
# 构建生产环境代码
|
||||
npm run build
|
||||
|
||||
# 将 dist 目录部署到生产服务器
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **不要将敏感信息写入配置文件**
|
||||
- API 密钥、数据库密码等敏感信息应通过环境变量传递
|
||||
- 使用 `process.env.VUE_APP_*` 格式定义环境变量
|
||||
|
||||
2. **修改配置后需要重启**
|
||||
- 修改配置文件后,需要重启开发服务器才能生效
|
||||
- `Ctrl + C` 停止服务器,然后重新运行 `npm run serve`
|
||||
|
||||
3. **生产环境配置检查**
|
||||
- 部署前务必检查生产环境的 `apiUrl` 是否正确
|
||||
- 确保关闭 `debug` 模式
|
||||
|
||||
4. **跨域问题**
|
||||
- 开发环境如遇到跨域问题,可以在 `vue.config.js` 中配置代理
|
||||
- 生产环境需要后端配置 CORS
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [Vue CLI 环境变量和模式](https://cli.vuejs.org/zh/guide/mode-and-env.html)
|
||||
- [AdminFramework 完整文档](../../_doc/admin_core完整使用文档.md)
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2025-10-10
|
||||
|
||||
60
admin/config/index.js
Normal file
60
admin/config/index.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Admin 前端配置文件
|
||||
* 支持多环境:development、sit、prod
|
||||
* 打包时由 webpack DefinePlugin 注入 __APP_BUILD_ENV__(.env.sit / .env.prod)
|
||||
*/
|
||||
const buildEnv = (typeof __APP_BUILD_ENV__ !== 'undefined' ? __APP_BUILD_ENV__ : (typeof process !== 'undefined' && process.env && process.env.BUILD_ENV)) || (typeof process !== 'undefined' && process.env && process.env.NODE_ENV) || 'development'
|
||||
|
||||
// 基础配置
|
||||
const baseConfig = {
|
||||
title: '沁羿物流 · 仓库管理系统',
|
||||
showSettings: true,
|
||||
showTagsView: true,
|
||||
fixedHeader: true,
|
||||
sidebarLogo: true,
|
||||
cookieExpires: 1,
|
||||
themeColor: '#2d8cf0'
|
||||
}
|
||||
|
||||
// 开发环境
|
||||
const developmentConfig = {
|
||||
...baseConfig,
|
||||
apiUrl: 'http://localhost:9098/admin_api/',
|
||||
uploadUrl: 'http://localhost:9098/admin_api/upload',
|
||||
debug: true
|
||||
}
|
||||
|
||||
// SIT 环境(build:sit)- tennis 接口
|
||||
const sitConfig = {
|
||||
...baseConfig,
|
||||
apiUrl: 'http://*/admin_api/',
|
||||
uploadUrl: 'http://*/admin_api/upload',
|
||||
debug: false
|
||||
}
|
||||
|
||||
// 生产环境(build:prod)— 与线上 Node 同域
|
||||
const productionConfig = {
|
||||
...baseConfig,
|
||||
apiUrl: 'http://*/admin_api/',
|
||||
uploadUrl: 'http://*/admin_api/upload',
|
||||
debug: false
|
||||
}
|
||||
|
||||
const configMap = {
|
||||
development: developmentConfig,
|
||||
sit: sitConfig,
|
||||
production: productionConfig,
|
||||
prod: productionConfig
|
||||
}
|
||||
|
||||
const config = configMap[buildEnv] || developmentConfig
|
||||
|
||||
export default config
|
||||
|
||||
export {
|
||||
baseConfig,
|
||||
developmentConfig,
|
||||
sitConfig,
|
||||
productionConfig
|
||||
}
|
||||
|
||||
7704
admin/package-lock.json
generated
Normal file
7704
admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
admin/package.json
Normal file
41
admin/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "admin-framework-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Admin Framework 本地示例",
|
||||
"scripts": {
|
||||
"install:deps": "npm install",
|
||||
"dev": "webpack serve --mode development --open",
|
||||
"build": "webpack --mode production",
|
||||
"build:sit": "webpack --mode production --env env_file=.env.sit",
|
||||
"build:prod": "webpack --mode production --env env_file=.env.prod",
|
||||
"build:test": "webpack --mode test"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"jsbarcode": "^3.11.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"view-design": "^4.7.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-router": "^3.5.3",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.0",
|
||||
"@babel/preset-env": "^7.12.0",
|
||||
"babel-loader": "^8.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.0.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"less": "^4.4.2",
|
||||
"less-loader": "^12.3.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"vue-loader": "^15.9.0",
|
||||
"vue-style-loader": "^4.1.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^5.0.0",
|
||||
"webpack-cli": "^4.0.0",
|
||||
"webpack-dev-server": "^4.0.0"
|
||||
}
|
||||
}
|
||||
14
admin/public/index.html
Normal file
14
admin/public/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>沁羿物流 · 仓库管理系统</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
31
admin/src/api/demo/tplDemoServer.js
Normal file
31
admin/src/api/demo/tplDemoServer.js
Normal file
@@ -0,0 +1,31 @@
|
||||
class TplDemoServer {
|
||||
async page(row) {
|
||||
return await window.framework.http.post("/tpl_demo/page", row);
|
||||
}
|
||||
|
||||
async all(param) {
|
||||
return await window.framework.http.get("/tpl_demo/all", param || {});
|
||||
}
|
||||
|
||||
async detail(param) {
|
||||
return await window.framework.http.get("/tpl_demo/detail", param);
|
||||
}
|
||||
|
||||
async add(row) {
|
||||
return await window.framework.http.post("/tpl_demo/add", row);
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
return await window.framework.http.post("/tpl_demo/edit", row);
|
||||
}
|
||||
|
||||
async del(row) {
|
||||
return await window.framework.http.post("/tpl_demo/del", row);
|
||||
}
|
||||
|
||||
exportCsv(params) {
|
||||
return window.framework.http.fileExport("/tpl_demo/export", params);
|
||||
}
|
||||
}
|
||||
|
||||
export default new TplDemoServer();
|
||||
4
admin/src/assets/css/index.less
Normal file
4
admin/src/assets/css/index.less
Normal file
@@ -0,0 +1,4 @@
|
||||
.ivu-menu-submenu .ivu-menu-item{
|
||||
padding-top: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
}
|
||||
66
admin/src/components/CustomTabPane.vue
Normal file
66
admin/src/components/CustomTabPane.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div v-show="isActive" class="tab-pane">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CustomTabPane',
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isActive: false,
|
||||
parentTabs: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.findParentTabs()
|
||||
if (this.parentTabs) {
|
||||
this.parentTabs.registerTab({
|
||||
label: this.label,
|
||||
name: this.name
|
||||
})
|
||||
this.updateActiveState(this.parentTabs.activeTab)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.parentTabs) {
|
||||
this.parentTabs.unregisterTab(this.name)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
findParentTabs() {
|
||||
let parent = this.$parent
|
||||
while (parent) {
|
||||
if (parent.$options.name === 'CustomTabs') {
|
||||
this.parentTabs = parent
|
||||
break
|
||||
}
|
||||
parent = parent.$parent
|
||||
}
|
||||
},
|
||||
updateActiveState(activeTab) {
|
||||
this.isActive = activeTab === this.name
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tab-pane {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
118
admin/src/components/CustomTabs.vue
Normal file
118
admin/src/components/CustomTabs.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="custom-tabs">
|
||||
<div class="tabs-header">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.name"
|
||||
:class="['tab-item', { active: activeTab === tab.name }]"
|
||||
@click="handleTabClick(tab.name)">
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CustomTabs',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: this.value || '',
|
||||
tabs: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.activeTab = newVal
|
||||
this.updateChildren()
|
||||
},
|
||||
activeTab(newVal) {
|
||||
this.updateChildren()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateChildren()
|
||||
},
|
||||
methods: {
|
||||
handleTabClick(name) {
|
||||
this.activeTab = name
|
||||
this.$emit('input', name)
|
||||
this.$emit('change', name)
|
||||
this.updateChildren()
|
||||
},
|
||||
registerTab(tab) {
|
||||
if (!this.tabs.find(t => t.name === tab.name)) {
|
||||
this.tabs.push(tab)
|
||||
// 如果没有激活的tab,设置第一个为激活
|
||||
if (!this.activeTab && this.tabs.length > 0) {
|
||||
this.activeTab = this.tabs[0].name
|
||||
this.$emit('input', this.activeTab)
|
||||
}
|
||||
}
|
||||
},
|
||||
unregisterTab(tabName) {
|
||||
const index = this.tabs.findIndex(t => t.name === tabName)
|
||||
if (index > -1) {
|
||||
this.tabs.splice(index, 1)
|
||||
}
|
||||
},
|
||||
updateChildren() {
|
||||
this.$children.forEach(child => {
|
||||
if (child.$options.name === 'CustomTabPane') {
|
||||
child.updateActiveState(this.activeTab)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.custom-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e8eaec;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #333;
|
||||
border-bottom-color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
padding: 20px 0;
|
||||
min-height: 400px;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
32
admin/src/framework/admin-framework.js
Normal file
32
admin/src/framework/admin-framework.js
Normal file
File diff suppressed because one or more lines are too long
1106
admin/src/framework/admin-framework.md
Normal file
1106
admin/src/framework/admin-framework.md
Normal file
File diff suppressed because it is too large
Load Diff
29
admin/src/main.js
Normal file
29
admin/src/main.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// 引入 Admin Framework(框架内部已包含所有依赖和样式)
|
||||
import AdminFramework from "./framework/admin-framework.js";
|
||||
import Vue from "vue";
|
||||
import componentMap from "./router/component-map.js";
|
||||
|
||||
import CustomTabs from "./components/CustomTabs.vue";
|
||||
import CustomTabPane from "./components/CustomTabPane.vue";
|
||||
|
||||
import config from "../config/index.js";
|
||||
|
||||
import "./assets/css/index.less";
|
||||
|
||||
const apiUrl = config.apiUrl;
|
||||
|
||||
const app = AdminFramework.createApp({
|
||||
title: "前后端模板",
|
||||
apiUrl: apiUrl,
|
||||
componentMap: componentMap,
|
||||
onReady() {
|
||||
console.log("应用已准备就绪");
|
||||
},
|
||||
});
|
||||
|
||||
AdminFramework.registerComponents(Vue, {
|
||||
CustomTabs: CustomTabs,
|
||||
CustomTabPane: CustomTabPane,
|
||||
});
|
||||
|
||||
app.$mount("#app");
|
||||
461
admin/src/mixins/tableMixin.js
Normal file
461
admin/src/mixins/tableMixin.js
Normal file
@@ -0,0 +1,461 @@
|
||||
// 通用表格页面混入
|
||||
import dynamicModelApi from '@/api/entity/dynamicModelApi.js';
|
||||
import { mapFormFieldsToEditColumns } from '@/utils/formEditColumns.js';
|
||||
import { getForeignRowLabel, rowsToIdOptions } from '@/utils/foreignRowLabel.js';
|
||||
import { orderActionBtnsViewEditFirst } from '@/utils/actionBtnOrder.js';
|
||||
|
||||
export default {
|
||||
created() {
|
||||
this.ensure_meta_shape();
|
||||
},
|
||||
methods: {
|
||||
/** 操作列:查看、编辑排在最左侧相邻,其余按钮顺序在后 */
|
||||
renderRowActionBtns(h, btns) {
|
||||
return window.framework.uiTool.getBtn(h, orderActionBtnsViewEditFirst(btns));
|
||||
},
|
||||
|
||||
field_display_name(f) {
|
||||
if (!f) return "";
|
||||
return f.label || f.title || f.key || "";
|
||||
},
|
||||
|
||||
/**
|
||||
* 按 meta.formFields 整理行数据,供 editModal 绑定(InputNumber 不能接收 "")。
|
||||
* 外键数字字段空值用 undefined;其它 number 空值或非数字用 0。
|
||||
*/
|
||||
normalizeRowForEditModal(row) {
|
||||
if (!row || typeof row !== 'object') return row;
|
||||
const fields =
|
||||
this.meta && Array.isArray(this.meta.formFields) ? this.meta.formFields : [];
|
||||
if (!fields.length) return { ...row };
|
||||
const o = { ...row };
|
||||
for (const f of fields) {
|
||||
if (!f || f.form === false || f.type !== 'number') continue;
|
||||
const k = f.key;
|
||||
const v = o[k];
|
||||
if (/_id$/.test(k)) {
|
||||
if (v === '' || v === null || v === undefined) {
|
||||
o[k] = undefined;
|
||||
} else {
|
||||
const n = Number(v);
|
||||
o[k] = Number.isNaN(n) ? undefined : n;
|
||||
}
|
||||
} else {
|
||||
if (v === '' || v === null || v === undefined) {
|
||||
o[k] = 0;
|
||||
} else {
|
||||
const n = Number(v);
|
||||
o[k] = Number.isNaN(n) ? 0 : n;
|
||||
}
|
||||
}
|
||||
}
|
||||
return o;
|
||||
},
|
||||
|
||||
// 兼容页面只定义 fields 的场景,统一补齐 formFields / columns
|
||||
ensure_meta_shape() {
|
||||
if (!this.meta || typeof this.meta !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = Array.isArray(this.meta.formFields)
|
||||
? this.meta.formFields
|
||||
: (Array.isArray(this.meta.fields) ? this.meta.fields : []);
|
||||
|
||||
// code 字段:后端自动生成,表单中不显示
|
||||
fields.forEach((f) => {
|
||||
if (f && f.key === 'code') {
|
||||
f.form = false;
|
||||
f.required = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!Array.isArray(this.meta.formFields)) {
|
||||
const formFields = fields.filter((f) => f && f.form !== false);
|
||||
this.$set(this.meta, 'formFields', formFields);
|
||||
}
|
||||
|
||||
if (!Array.isArray(this.meta.columns)) {
|
||||
const columns = fields
|
||||
.filter((f) => f && f.list)
|
||||
.map((f) => ({
|
||||
key: f.key,
|
||||
title: f.label || f.title,
|
||||
minWidth: f.minWidth,
|
||||
}));
|
||||
|
||||
this.$set(this.meta, 'columns', columns);
|
||||
}
|
||||
},
|
||||
|
||||
// 通用的编辑列配置生成方法(实现见 utils/formEditColumns.js)
|
||||
generateEditColumns(formFields, selectSources) {
|
||||
return mapFormFieldsToEditColumns(formFields, selectSources);
|
||||
},
|
||||
|
||||
/**
|
||||
* 标准列表列:外键列显示关联名称(select_sources)、布尔、日期等
|
||||
* meta 需含 columns,以及 fields 或 formFields(带 type)
|
||||
*/
|
||||
buildListColumnsFromMeta(meta, selectSources) {
|
||||
if (!meta || !Array.isArray(meta.columns)) return [];
|
||||
const allFields = meta.fields || meta.formFields || [];
|
||||
return this.generateListColumns(meta.columns, allFields, selectSources);
|
||||
},
|
||||
|
||||
// 通用的列表列配置生成方法
|
||||
generateListColumns(columns, allFields, selectSources) {
|
||||
if (!columns || !Array.isArray(columns)) return [];
|
||||
|
||||
const fields = Array.isArray(allFields) ? allFields : [];
|
||||
|
||||
/** 系统统一时间字段(与后端 create_time / last_modify_time 一致),排在业务列之后、操作列之前 */
|
||||
const colList = [...columns];
|
||||
const keySet = new Set(colList.map((c) => c && c.key).filter(Boolean));
|
||||
if (!keySet.has('create_time')) {
|
||||
colList.push({ key: 'create_time', title: '创建时间', minWidth: 160 });
|
||||
keySet.add('create_time');
|
||||
}
|
||||
if (!keySet.has('last_modify_time')) {
|
||||
colList.push({ key: 'last_modify_time', title: '最后修改时间', minWidth: 160 });
|
||||
keySet.add('last_modify_time');
|
||||
}
|
||||
|
||||
const boolKeys = new Set(fields.filter((f) => f.type === 'bool').map((f) => f.key));
|
||||
const selectKeys = new Set(fields.filter((f) => f.type === 'select').map((f) => f.key));
|
||||
|
||||
return colList.map((c) => {
|
||||
// 外键:已加载下拉数据时显示名称(含仓库/库区/巷道及 mat_sku 等)
|
||||
if (
|
||||
selectSources &&
|
||||
selectSources[c.key] &&
|
||||
Array.isArray(selectSources[c.key]) &&
|
||||
selectSources[c.key].length &&
|
||||
/_id$/.test(c.key)
|
||||
) {
|
||||
return {
|
||||
...c,
|
||||
render: (h, params) => {
|
||||
const id = params.row[c.key];
|
||||
const source = selectSources[c.key];
|
||||
const item = source && source.find((w) => String(w.key) === String(id));
|
||||
return h('span', item ? item.value : id != null && id !== '' ? String(id) : '-');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 处理选择字段显示(status 按字段 source_key)
|
||||
if (selectKeys.has(c.key)) {
|
||||
const fieldDef = fields.find((f) => f.key === c.key);
|
||||
const source_key = fieldDef && fieldDef.source_key;
|
||||
return {
|
||||
...c,
|
||||
render: (h, params) => {
|
||||
const value = params.row[c.key];
|
||||
let source;
|
||||
if (source_key && selectSources && selectSources[source_key]) {
|
||||
source = selectSources[source_key];
|
||||
} else if (c.key === 'type') {
|
||||
source = selectSources && selectSources.warehouse_type_options;
|
||||
} else if (c.key === 'tray_type_id') {
|
||||
source = selectSources && selectSources.tray_type_id;
|
||||
} else if (c.key === 'lock_type') {
|
||||
source = selectSources && selectSources.lock_type_options;
|
||||
} else if (c.key === 'status' && source_key) {
|
||||
source = selectSources && selectSources[source_key];
|
||||
} else if (c.key === 'status') {
|
||||
source = selectSources && selectSources.tray_status_options;
|
||||
} else {
|
||||
source = selectSources && selectSources.yes_no_options;
|
||||
}
|
||||
const option = source && source.find((opt) => String(opt.key) === String(value));
|
||||
return h('span', option ? option.value : value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 处理布尔字段显示
|
||||
if (boolKeys.has(c.key)) {
|
||||
return {
|
||||
...c,
|
||||
render: (h, params) => {
|
||||
const v = params.row[c.key];
|
||||
const on = v === true || v === 1 || v === '1';
|
||||
return h('Tag', { props: { color: on ? 'green' : 'default' } }, on ? '是' : '否');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 处理日期字段显示
|
||||
if (this.isDateLikeColumnKey && this.isDateLikeColumnKey(c.key)) {
|
||||
return {
|
||||
...c,
|
||||
render: (h, params) => {
|
||||
const raw =
|
||||
c.key === 'create_time' || c.key === 'last_modify_time'
|
||||
? this.pickRowTimeCell(params.row, c.key)
|
||||
: params.row[c.key];
|
||||
return h('span', this.formatCellDate(raw, c.key));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { ...c };
|
||||
});
|
||||
},
|
||||
|
||||
// 从 Vuex store (wms_dict) 获取字典选项
|
||||
getDictOptions(key) {
|
||||
if (!this.$store || !this.$store.state.wms_dict) return [];
|
||||
return this.$store.state.wms_dict[key] || [];
|
||||
},
|
||||
|
||||
// 通用的数据源加载方法(从 store 读取字典,不再请求 sys_parameter)
|
||||
async loadSelectSources(formFields, idFieldToModelFn, getRowLabelFn) {
|
||||
const sources = {};
|
||||
|
||||
sources.yes_no_options = this.getDictOptions('yes_no');
|
||||
sources.warehouse_type_options = this.getDictOptions('warehouse_type');
|
||||
sources.lock_type_options = this.getDictOptions('lock_type');
|
||||
sources.tray_status_options = this.getDictOptions('tray_status');
|
||||
sources.tray_type_status_options = this.getDictOptions('tray_type_status');
|
||||
sources.receiving_status_options = this.getDictOptions('receiving_status');
|
||||
sources.inbound_status_options = this.getDictOptions('inbound_status');
|
||||
sources.send_status_options = this.getDictOptions('send_status');
|
||||
sources.outbound_status_options = this.getDictOptions('outbound_status');
|
||||
sources.move_status_options = this.getDictOptions('move_status');
|
||||
sources.damage_status_options = this.getDictOptions('damage_status');
|
||||
sources.count_status_options = this.getDictOptions('count_status');
|
||||
sources.receiving_type_options = this.getDictOptions('receiving_type');
|
||||
sources.send_type_options = this.getDictOptions('send_type');
|
||||
sources.fee_charge_type_options = this.getDictOptions('fee_charge_type_options');
|
||||
sources.fee_generate_type_options = this.getDictOptions('fee_generate_type_options');
|
||||
sources.fee_collection_type_options = this.getDictOptions('fee_collection_type_options');
|
||||
sources.fee_item_inbound_options = this.getDictOptions('fee_item_inbound_options');
|
||||
sources.fee_item_outbound_options = this.getDictOptions('fee_item_outbound_options');
|
||||
sources.packaging_status_options = this.getDictOptions('packaging_status_options');
|
||||
|
||||
// 加载外键数据
|
||||
const idFields = formFields
|
||||
.filter((f) => f.type === 'number' && /_id$/.test(f.key))
|
||||
.map((f) => f.key);
|
||||
|
||||
for (const k of idFields) {
|
||||
const model = idFieldToModelFn(k);
|
||||
if (!model) {
|
||||
sources[k] = [];
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const res = await dynamicModelApi.all(model);
|
||||
const rows = res && res.code === 0 && res.data ? res.data : [];
|
||||
sources[k] = rowsToIdOptions(rows, getRowLabelFn || getForeignRowLabel);
|
||||
} catch (e) {
|
||||
sources[k] = [];
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
},
|
||||
|
||||
// 通用的空表单构建方法
|
||||
generateEmptyForm(formFields) {
|
||||
const o = {};
|
||||
if (!formFields) return o;
|
||||
|
||||
for (const f of formFields) {
|
||||
if (f.type === 'bool') o[f.key] = false;
|
||||
else if (f.type === 'number') {
|
||||
// 外键用 Select 时,0 易被当成「已选」且无法匹配 Option,校验与展示都不直观
|
||||
o[f.key] = /_id$/.test(f.key) ? undefined : 0;
|
||||
} else if (f.type === 'select') {
|
||||
if (f.key === 'lock_type') o[f.key] = 0;
|
||||
else if (f.data_type === 'number' && /_id$/.test(f.key)) o[f.key] = undefined;
|
||||
else o[f.key] = '';
|
||||
} else o[f.key] = '';
|
||||
}
|
||||
return o;
|
||||
},
|
||||
|
||||
// 通用的日期相关方法
|
||||
isDateLikeColumnKey(key) {
|
||||
if (!key) return false;
|
||||
return (
|
||||
key === 'stat_date' ||
|
||||
key === 'create_time' ||
|
||||
key === 'last_modify_time' ||
|
||||
/_time$/.test(key) ||
|
||||
/_date$/.test(key)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 列表「创建时间 / 最后修改时间」与后端 Sequelize 字段对齐:
|
||||
|
||||
*/
|
||||
pickRowTimeCell(row, key) {
|
||||
if (!row || !key) return undefined;
|
||||
if (key === 'create_time') {
|
||||
return row.create_time;
|
||||
}
|
||||
if (key === 'last_modify_time') {
|
||||
return row.last_modify_time;
|
||||
}
|
||||
return row[key];
|
||||
},
|
||||
|
||||
formatCellDate(v, key) {
|
||||
if (v === null || v === undefined || v === '') return '-';
|
||||
const d = v instanceof Date ? v : new Date(v);
|
||||
if (Number.isNaN(d.getTime())) return String(v);
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const y = d.getFullYear();
|
||||
const m = pad(d.getMonth() + 1);
|
||||
const day = pad(d.getDate());
|
||||
const hh = pad(d.getHours());
|
||||
const mm = pad(d.getMinutes());
|
||||
const dateOnly = key === 'stat_date' || (key && /_date$/.test(key) && !/_time$/.test(key));
|
||||
if (dateOnly) return y + '-' + m + '-' + day;
|
||||
return y + '-' + m + '-' + day + ' ' + hh + ':' + mm;
|
||||
},
|
||||
|
||||
/** 外键行展示文案;实现见 @/utils/foreignRowLabel.js */
|
||||
getRowLabel(row) {
|
||||
return getForeignRowLabel(row);
|
||||
},
|
||||
|
||||
// 通用的数据规范化方法
|
||||
generatePayload(raw, formFields) {
|
||||
const o = { ...raw };
|
||||
if (!formFields) return o;
|
||||
|
||||
const typeByKey = {};
|
||||
const selectDataTypeByKey = {};
|
||||
for (const f of formFields) {
|
||||
if (!f || !f.key) continue;
|
||||
typeByKey[f.key] = f.type;
|
||||
if (f.data_type) selectDataTypeByKey[f.key] = f.data_type;
|
||||
}
|
||||
|
||||
for (const k of Object.keys(o)) {
|
||||
const t = typeByKey[k];
|
||||
let v = o[k];
|
||||
|
||||
if (t === 'number') {
|
||||
if (v === '' || v === null || v === undefined) {
|
||||
o[k] = null;
|
||||
} else {
|
||||
const n = Number(v);
|
||||
o[k] = Number.isNaN(n) ? v : n;
|
||||
}
|
||||
} else if (t === 'bool') {
|
||||
o[k] = v === true || v === 1 || v === '1';
|
||||
} else if (t === 'select') {
|
||||
const numSelect = k === 'lock_type' || selectDataTypeByKey[k] === 'number';
|
||||
if (numSelect) {
|
||||
if (v === '' || v === null || v === undefined) {
|
||||
o[k] = null;
|
||||
} else {
|
||||
const n = Number(v);
|
||||
o[k] = Number.isNaN(n) ? v : n;
|
||||
}
|
||||
}
|
||||
} else if (t === 'date' || t === 'datetime') {
|
||||
if (v instanceof Date) {
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
if (t === 'date') {
|
||||
o[k] = v.getFullYear() + '-' + pad(v.getMonth() + 1) + '-' + pad(v.getDate());
|
||||
} else {
|
||||
o[k] = v.getFullYear() + '-' + pad(v.getMonth() + 1) + '-' + pad(v.getDate()) +
|
||||
' ' + pad(v.getHours()) + ':' + pad(v.getMinutes()) + ':' + pad(v.getSeconds());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return o;
|
||||
},
|
||||
|
||||
// 通用的表单验证规则生成方法(与 iView Form / async-validator 约定一致:输入类 blur,选择类 change)
|
||||
generateEditRules(formFields) {
|
||||
if (!formFields) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const rules = {};
|
||||
for (const f of formFields) {
|
||||
if (f.form === false) continue;
|
||||
if (!f.required) continue;
|
||||
if (f.type === 'bool') continue;
|
||||
const fieldTitle = f.label || f.title || f.key;
|
||||
|
||||
if (f.type === 'date' || f.type === 'datetime') {
|
||||
rules[f.key] = [
|
||||
{ required: true, type: 'date', message: '请选择' + fieldTitle, trigger: 'change' }
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 数字 id 用 Select 渲染(如 tenant_id:type=select + data_type=number),Option 常为字符串;
|
||||
// 仅用 async-validator required 易与 iView 绑定不一致导致「已选仍报错」
|
||||
if (f.type === 'select' && f.data_type === 'number' && /_id$/.test(f.key)) {
|
||||
rules[f.key] = [
|
||||
{
|
||||
required: true,
|
||||
trigger: 'change',
|
||||
validator(rule, value, callback) {
|
||||
const n = value === '' || value === null || value === undefined ? NaN : Number(value);
|
||||
if (Number.isNaN(n) || n === 0) {
|
||||
callback(new Error('请选择' + fieldTitle));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (f.type === 'select') {
|
||||
rules[f.key] = [
|
||||
{ required: true, message: '请选择' + fieldTitle, trigger: 'change' }
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 外键:generateEditColumns 里用 Select,值为数字 id,但 Option 上可能是字符串;不能用 type:'number' 直接校验
|
||||
if (f.type === 'number' && /_id$/.test(f.key)) {
|
||||
rules[f.key] = [
|
||||
{
|
||||
required: true,
|
||||
trigger: 'change',
|
||||
validator(rule, value, callback) {
|
||||
const n = value === '' || value === null || value === undefined ? NaN : Number(value);
|
||||
if (Number.isNaN(n) || n === 0) {
|
||||
callback(new Error('请选择' + fieldTitle));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (f.type === 'number') {
|
||||
rules[f.key] = [
|
||||
{
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请填写' + fieldTitle,
|
||||
trigger: 'blur'
|
||||
}
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
rules[f.key] = [{ required: true, message: '请填写' + fieldTitle, trigger: 'blur' }];
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
};
|
||||
10
admin/src/router/component-map.js
Normal file
10
admin/src/router/component-map.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// 动态路由组件映射:key 与 sys_menu.component 一致(可带或不带 .vue)
|
||||
|
||||
import TplDemoPage from "../views/demo/tpl_demo.vue";
|
||||
|
||||
const componentMap = {
|
||||
"demo/tpl_demo": TplDemoPage,
|
||||
tpl_demo: TplDemoPage,
|
||||
};
|
||||
|
||||
export default componentMap;
|
||||
17
admin/src/utils/actionBtnOrder.js
Normal file
17
admin/src/utils/actionBtnOrder.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 列表操作列:将「查看」「编辑」排到最前且相邻(先查看后编辑),其余按钮保持原有相对顺序在后。
|
||||
* @param {{ title?: string, type?: string, click?: Function }[]} btns
|
||||
*/
|
||||
export function orderActionBtnsViewEditFirst(btns) {
|
||||
if (!Array.isArray(btns) || btns.length === 0) {
|
||||
return btns || [];
|
||||
}
|
||||
const firstByTitle = (t) => btns.find((b) => b && b.title === t);
|
||||
const v = firstByTitle('查看');
|
||||
const e = firstByTitle('编辑');
|
||||
const head = [];
|
||||
if (v) head.push(v);
|
||||
if (e) head.push(e);
|
||||
const tail = btns.filter((b) => b && b !== v && b !== e);
|
||||
return [...head, ...tail];
|
||||
}
|
||||
40
admin/src/utils/foreignRowLabel.js
Normal file
40
admin/src/utils/foreignRowLabel.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 外键关联行 → 下拉/表格可读文案(物料、仓库、单号等)。
|
||||
* 与 tableMixin.getRowLabel 语义一致;页面加载 select_sources 时请用 rowsToIdOptions,避免各处重复 map。
|
||||
*/
|
||||
|
||||
export function getForeignRowLabel(row) {
|
||||
if (!row) return '';
|
||||
return (
|
||||
row.sku_name ||
|
||||
row.spu_name ||
|
||||
row.category_name ||
|
||||
row.name ||
|
||||
row.code ||
|
||||
row.spu_code ||
|
||||
row.sku_code ||
|
||||
row.title ||
|
||||
row.label ||
|
||||
String(row.id != null ? row.id : '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 /model/all 等接口返回的行列表转为 Select 用的 { key, value }(key 与数字 id 对齐)
|
||||
* @param {Array} rows
|
||||
* @param {function(object): string} [getLabel=getForeignRowLabel] 自定义展示,例如收货明细「编码 - 名称」
|
||||
*/
|
||||
export function rowsToIdOptions(rows, getLabel = getForeignRowLabel) {
|
||||
if (!Array.isArray(rows)) return [];
|
||||
return rows.map((r) => {
|
||||
const rawId = r.id;
|
||||
const key =
|
||||
rawId !== null && rawId !== undefined && rawId !== ''
|
||||
? Number(rawId)
|
||||
: rawId;
|
||||
return {
|
||||
key: Number.isFinite(key) ? key : rawId,
|
||||
value: getLabel(r),
|
||||
};
|
||||
});
|
||||
}
|
||||
82
admin/src/utils/formEditColumns.js
Normal file
82
admin/src/utils/formEditColumns.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 将 meta.formFields / meta.fields 转为 editModal 用的列配置(与 tableMixin.generateEditColumns 一致)。
|
||||
* 纯函数,便于单测;页面仍通过 mixin 的 this.generateEditColumns 调用。
|
||||
*
|
||||
* @param {Array<{ key, type, label?, title?, form?, required?, source_key? }>} formFields
|
||||
* @param {Record<string, Array<{ key, value }>>} [selectSources]
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function mapFormFieldsToEditColumns(formFields, selectSources) {
|
||||
if (!formFields || !Array.isArray(formFields)) return [];
|
||||
const visible = formFields.filter((f) => f && f.form !== false);
|
||||
|
||||
return visible.map((f) => {
|
||||
const fieldTitle = f.label || f.title || f.key;
|
||||
const col = { key: f.key, title: fieldTitle, required: !!f.required };
|
||||
|
||||
switch (f.type) {
|
||||
case 'number':
|
||||
if (/_id$/.test(f.key)) {
|
||||
col.com = 'Select';
|
||||
col.data_type = 'number';
|
||||
col.placeholder = '请选择' + fieldTitle;
|
||||
col.source = (selectSources && selectSources[f.key]) || [];
|
||||
} else {
|
||||
col.com = 'InputNumber';
|
||||
col.data_type = 'number';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
col.com = 'Select';
|
||||
// 与后端数字枚举一致时用 number,否则 Option 与 v-model 类型不一致无法显示已选
|
||||
col.data_type = f.data_type === 'number' ? 'number' : 'string';
|
||||
col.placeholder = '请选择' + fieldTitle;
|
||||
col.source = (selectSources && selectSources[f.source_key]) || [];
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
col.com = 'TextArea';
|
||||
col.rows = 2;
|
||||
break;
|
||||
|
||||
case 'password':
|
||||
col.com = 'Input';
|
||||
col.type = 'password';
|
||||
break;
|
||||
|
||||
case 'bool':
|
||||
col.com = 'Switch';
|
||||
col.data_type = 'boolean';
|
||||
col.required = false;
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
col.com = 'DatePicker';
|
||||
col.data_type = 'date';
|
||||
col.type = 'date';
|
||||
col.format = 'yyyy-MM-dd';
|
||||
col.transfer = true;
|
||||
col.placeholder = '请选择' + fieldTitle;
|
||||
break;
|
||||
|
||||
case 'datetime':
|
||||
col.com = 'DatePicker';
|
||||
col.data_type = 'date';
|
||||
col.type = 'datetime';
|
||||
col.format = 'yyyy-MM-dd HH:mm';
|
||||
col.transfer = true;
|
||||
col.placeholder = '请选择' + fieldTitle;
|
||||
break;
|
||||
|
||||
default:
|
||||
col.com = 'Input';
|
||||
}
|
||||
|
||||
if (f.disabled) {
|
||||
col.disabled = true;
|
||||
}
|
||||
|
||||
return col;
|
||||
});
|
||||
}
|
||||
9
admin/src/utils/listColumn.js
Normal file
9
admin/src/utils/listColumn.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 列表列简写:生成 { key, title, minWidth },供 meta.columns 使用。
|
||||
* import { col } from '@/utils/listColumn.js' 与原先页面内 function col 等价。
|
||||
*/
|
||||
export function listCol(key, title, minWidth) {
|
||||
return { key, title, minWidth };
|
||||
}
|
||||
|
||||
export { listCol as col };
|
||||
210
admin/src/views/demo/tpl_demo.vue
Normal file
210
admin/src/views/demo/tpl_demo.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="content-view" v-if="meta">
|
||||
<div class="table-head-tool">
|
||||
<div class="table-head-row">
|
||||
<div class="table-head-actions">
|
||||
<Button type="primary" @click="showAdd">新增</Button>
|
||||
<Button type="default" @click="query(1)" class="ml10">刷新</Button>
|
||||
<Button type="default" @click="exportCsv" class="ml10">导出</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Form ref="searchForm" :model="gridOption.param.seachOption" inline :label-width="80" class="search-form">
|
||||
<FormItem :label-width="20" class="search-input-group">
|
||||
<Select v-model="gridOption.param.seachOption.key" style="width: 180px" :placeholder="seachTypePlaceholder">
|
||||
<Option v-for="item in meta.seachTypes" :value="item.key" :key="item.key">{{ item.value }}</Option>
|
||||
</Select>
|
||||
<Input class="ml10" v-model="gridOption.param.seachOption.value" style="width: 280px" search
|
||||
placeholder="请输入关键字" @on-search="query(1)" />
|
||||
</FormItem>
|
||||
<FormItem class="search-action-group">
|
||||
<Button type="primary" @click="query(1)">查询</Button>
|
||||
<Button type="default" @click="resetQuery" class="ml10">重置</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<tables :columns="listColumns" :value="gridOption.data" :pageOption="gridOption.param.pageOption"
|
||||
@changePage="query"></tables>
|
||||
</div>
|
||||
<editModal ref="editModal" :columns="editColumns" :rules="gridOption.rules" width="640" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tplDemoServer from "@/api/demo/tplDemoServer.js";
|
||||
import tableMixin from "@/mixins/tableMixin.js";
|
||||
|
||||
export default {
|
||||
name: "TplDemoPage",
|
||||
mixins: [tableMixin],
|
||||
data() {
|
||||
return {
|
||||
select_sources: {},
|
||||
gridOption: {
|
||||
param: { seachOption: { key: "", value: "" }, pageOption: { page: 1, pageSize: 20, total: 0 } },
|
||||
data: [],
|
||||
rules: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modelName() {
|
||||
return "tpl_demo";
|
||||
},
|
||||
meta() {
|
||||
return {
|
||||
title: "演示数据",
|
||||
seachTypes: [
|
||||
{ key: "title", value: "标题" },
|
||||
{ key: "remark", value: "备注" },
|
||||
],
|
||||
fields: [
|
||||
{ key: "id", title: "ID", type: "number", list: true, form: false, minWidth: 80 },
|
||||
{ key: "title", title: "标题", type: "input", list: true, form: true, required: true, minWidth: 200 },
|
||||
{ key: "remark", title: "备注", type: "input", list: true, form: true, required: false, minWidth: 220 },
|
||||
],
|
||||
};
|
||||
},
|
||||
seachTypePlaceholder() {
|
||||
const st = this.meta && this.meta.seachTypes;
|
||||
if (!st || !st.length) {
|
||||
return "请选择";
|
||||
}
|
||||
const k = this.gridOption.param.seachOption.key;
|
||||
const f = st.find((x) => x.key === k);
|
||||
return f ? f.value : "请选择搜索字段";
|
||||
},
|
||||
editColumns() {
|
||||
if (!this.meta || !Array.isArray(this.meta.fields)) {
|
||||
return [];
|
||||
}
|
||||
return this.generateEditColumns(this.meta.fields.filter((f) => f.form !== false), this.select_sources);
|
||||
},
|
||||
listColumns() {
|
||||
if (!this.meta || !Array.isArray(this.meta.fields)) {
|
||||
return [];
|
||||
}
|
||||
const listFields = this.meta.fields.filter((f) => f.list !== false);
|
||||
const cols = this.generateListColumns(listFields, this.meta.fields, this.select_sources);
|
||||
cols.push({
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 180,
|
||||
type: "template",
|
||||
render: (h, params) => {
|
||||
return this.renderRowActionBtns(h, [
|
||||
{ title: "编辑", type: "info", click: () => this.showEdit(params.row) },
|
||||
{ title: "删除", type: "error", click: () => this.delConfirm(params.row) },
|
||||
]);
|
||||
},
|
||||
});
|
||||
return cols;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.load_select_sources();
|
||||
this.resetSearchFromMeta();
|
||||
this.syncEditRules();
|
||||
this.query(1);
|
||||
},
|
||||
methods: {
|
||||
id_field_to_model() {
|
||||
return "";
|
||||
},
|
||||
async load_select_sources() {
|
||||
this.select_sources = await this.loadSelectSources(
|
||||
this.meta.fields,
|
||||
this.id_field_to_model.bind(this),
|
||||
this.getRowLabel.bind(this)
|
||||
);
|
||||
},
|
||||
syncEditRules() {
|
||||
this.gridOption.rules = this.generateEditRules(this.meta.fields.filter((f) => f.form !== false));
|
||||
},
|
||||
resetSearchFromMeta() {
|
||||
if (!this.meta) {
|
||||
return;
|
||||
}
|
||||
const firstKey = (this.meta.seachTypes[0] && this.meta.seachTypes[0].key) || "title";
|
||||
this.gridOption.param.seachOption = { key: firstKey, value: "" };
|
||||
this.gridOption.param.pageOption = { page: 1, pageSize: 20, total: 0 };
|
||||
},
|
||||
buildEmptyForm() {
|
||||
return this.generateEmptyForm(this.meta.fields.filter((f) => f.form !== false));
|
||||
},
|
||||
normalizePayload(raw) {
|
||||
return this.generatePayload(raw, this.meta.fields.filter((f) => f.form !== false));
|
||||
},
|
||||
async query(page) {
|
||||
if (!this.modelName || !this.meta) {
|
||||
return;
|
||||
}
|
||||
if (page) {
|
||||
this.gridOption.param.pageOption.page = page;
|
||||
}
|
||||
try {
|
||||
const res = await tplDemoServer.page(this.gridOption.param);
|
||||
if (res && res.code === 0) {
|
||||
this.gridOption.data = res.data.rows || [];
|
||||
this.gridOption.param.pageOption.total = res.data.count || 0;
|
||||
} else {
|
||||
this.$Message.error((res && (res.message || res.Msg)) || "查询失败");
|
||||
}
|
||||
} catch (e) {
|
||||
this.$Message.error("查询失败: " + (e && e.message ? e.message : String(e)));
|
||||
}
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetSearchFromMeta();
|
||||
this.query(1);
|
||||
},
|
||||
showAdd() {
|
||||
this.$refs.editModal.addShow(this.buildEmptyForm(), async (data) => {
|
||||
await this.persist(data, false);
|
||||
});
|
||||
},
|
||||
showEdit(row) {
|
||||
this.$refs.editModal.editShow(this.normalizeRowForEditModal({ ...row }), async (data) => {
|
||||
await this.persist(data, true);
|
||||
});
|
||||
},
|
||||
async persist(data, isEdit) {
|
||||
const payload = this.normalizePayload(data);
|
||||
const req = isEdit ? tplDemoServer.edit(payload) : tplDemoServer.add(payload);
|
||||
const res = await req;
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success(isEdit ? "保存成功" : "新增成功");
|
||||
this.query(1);
|
||||
} else {
|
||||
this.$Message.error((res && (res.message || res.Msg)) || "保存失败");
|
||||
throw new Error("save failed");
|
||||
}
|
||||
},
|
||||
delConfirm(row) {
|
||||
this.$Modal.confirm({
|
||||
title: "确认删除",
|
||||
content: "确定删除该条记录?",
|
||||
onOk: async () => {
|
||||
const res = await tplDemoServer.del({ id: row.id });
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success("已删除");
|
||||
this.query(1);
|
||||
} else {
|
||||
this.$Message.error((res && (res.message || res.Msg)) || "删除失败");
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
exportCsv() {
|
||||
if (!this.modelName || !this.meta) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
tplDemoServer.exportCsv({ param: this.gridOption.param });
|
||||
} catch (e) {
|
||||
this.$Message.error("导出失败: " + (e && e.message ? e.message : String(e)));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
118
admin/webpack.config.js
Normal file
118
admin/webpack.config.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
// 仅当传入 env_file 时加载 .env(build:sit / build:prod),否则 dev 用 development,build 用 prod
|
||||
const envFile = env && env.env_file
|
||||
if (envFile) {
|
||||
require('dotenv').config({ path: path.resolve(__dirname, envFile) })
|
||||
}
|
||||
const buildEnv = process.env.BUILD_ENV || (argv.mode === 'production' ? 'prod' : 'development')
|
||||
return {
|
||||
entry: './src/main.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'js/[name].[contenthash:8].js',
|
||||
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
|
||||
clean: true
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
minSize: 20000,
|
||||
maxSize: 244000,
|
||||
cacheGroups: {
|
||||
// Vue 相关库单独打包
|
||||
vue: {
|
||||
test: /[\\/]node_modules[\\/](vue|vue-router|vuex|vue-loader|vue-template-compiler)[\\/]/,
|
||||
name: 'vue',
|
||||
priority: 30,
|
||||
reuseExistingChunk: true
|
||||
},
|
||||
// UI 库单独打包
|
||||
ui: {
|
||||
test: /[\\/]node_modules[\\/](view-design|iview)[\\/]/,
|
||||
name: 'ui',
|
||||
priority: 25,
|
||||
reuseExistingChunk: true
|
||||
},
|
||||
// 其他第三方库打包
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendors',
|
||||
priority: 10,
|
||||
reuseExistingChunk: true
|
||||
},
|
||||
// 公共代码
|
||||
common: {
|
||||
name: 'common',
|
||||
minChunks: 2,
|
||||
priority: 5,
|
||||
reuseExistingChunk: true
|
||||
}
|
||||
}
|
||||
},
|
||||
// 运行时代码单独打包
|
||||
runtimeChunk: {
|
||||
name: 'runtime'
|
||||
},
|
||||
// 生产环境启用压缩
|
||||
minimize: process.env.NODE_ENV === 'production'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['vue-style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'less-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg|woff2?|eot|ttf|otf)$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'assets/[name].[hash:8][ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__APP_BUILD_ENV__: JSON.stringify(buildEnv),
|
||||
'process.env.BUILD_ENV': JSON.stringify(buildEnv)
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './public/index.html',
|
||||
title: '沁羿物流 · 仓库管理系统'
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
hot: true,
|
||||
open: true,
|
||||
port: 8080,
|
||||
historyApiFallback: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user