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 @@
|
||||
# 生产环境:与 config/index.js 中 productionConfig 对应,按需修改域名
|
||||
BUILD_ENV=prod
|
||||
2
admin/.env.sit
Normal file
2
admin/.env.sit
Normal file
@@ -0,0 +1,2 @@
|
||||
# SIT 环境:与 config/index.js 中 sitConfig 对应,按需修改域名
|
||||
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 | 'Tennis管理系统' | 系统标题 |
|
||||
| `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
|
||||
|
||||
9
admin/config/config.example.js
Normal file
9
admin/config/config.example.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 在业务组件中使用配置的示例(按需复制到 .vue 或独立模块)
|
||||
*/
|
||||
export const exampleComponent = {
|
||||
mounted() {
|
||||
console.log('title:', this.$config.title)
|
||||
console.log('apiUrl:', this.$config.apiUrl)
|
||||
}
|
||||
}
|
||||
57
admin/config/index.js
Normal file
57
admin/config/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Admin 前端配置(模板)
|
||||
* 多环境:development / sit / prod(由 webpack DefinePlugin 注入 __APP_BUILD_ENV__)
|
||||
*/
|
||||
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)— 请在 .env.sit 中配合 BUILD_ENV=sit,并按需改域名
|
||||
const sitConfig = {
|
||||
...baseConfig,
|
||||
apiUrl: 'https://your-sit-domain.com/admin_api/',
|
||||
uploadUrl: 'https://your-sit-domain.com/admin_api/upload',
|
||||
debug: false
|
||||
}
|
||||
|
||||
// 生产环境(build:prod)
|
||||
const productionConfig = {
|
||||
...baseConfig,
|
||||
apiUrl: 'https://your-prod-domain.com/admin_api/',
|
||||
uploadUrl: 'https://your-prod-domain.com/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
|
||||
}
|
||||
7490
admin/package-lock.json
generated
Normal file
7490
admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
admin/package.json
Normal file
39
admin/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "admin-framework-demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Admin 管理端模板(基于 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",
|
||||
"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",
|
||||
"css-loader": "^5.0.0",
|
||||
"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",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.0.3"
|
||||
}
|
||||
}
|
||||
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>
|
||||
21
admin/src/api/ad/sysAdServer.js
Normal file
21
admin/src/api/ad/sysAdServer.js
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
|
||||
class SysAdServer {
|
||||
async getAll(param) {
|
||||
return await window.framework.http.get("/sys_ad/index", param);
|
||||
}
|
||||
|
||||
async add(row) {
|
||||
return await window.framework.http.post("/sys_ad/add", row);
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
return await window.framework.http.post("/sys_ad/edit", row);
|
||||
}
|
||||
|
||||
async del(row) {
|
||||
return await window.framework.http.post("/sys_ad/del", row);
|
||||
}
|
||||
}
|
||||
const sysAdServer = new SysAdServer();
|
||||
export default sysAdServer;
|
||||
11
admin/src/api/biz/biz_payment_server.js
Normal file
11
admin/src/api/biz/biz_payment_server.js
Normal file
@@ -0,0 +1,11 @@
|
||||
class BizPaymentServer {
|
||||
async confirmOffline(row) {
|
||||
return window.framework.http.post("/biz_payment/confirm-offline", row);
|
||||
}
|
||||
|
||||
async confirmLink(row) {
|
||||
return window.framework.http.post("/biz_payment/confirm-link", row);
|
||||
}
|
||||
}
|
||||
|
||||
export default new BizPaymentServer();
|
||||
27
admin/src/api/biz/biz_plan_server.js
Normal file
27
admin/src/api/biz/biz_plan_server.js
Normal file
@@ -0,0 +1,27 @@
|
||||
class BizPlanServer {
|
||||
async page(row) {
|
||||
return window.framework.http.post("/biz_plan/page", row);
|
||||
}
|
||||
|
||||
async add(row) {
|
||||
return window.framework.http.post("/biz_plan/add", row);
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
return window.framework.http.post("/biz_plan/edit", row);
|
||||
}
|
||||
|
||||
async del(row) {
|
||||
return window.framework.http.post("/biz_plan/del", row);
|
||||
}
|
||||
|
||||
async toggle(row) {
|
||||
return window.framework.http.post("/biz_plan/toggle", row);
|
||||
}
|
||||
|
||||
async all() {
|
||||
return window.framework.http.get("/biz_plan/all", {});
|
||||
}
|
||||
}
|
||||
|
||||
export default new BizPlanServer();
|
||||
27
admin/src/api/biz/biz_subscription_server.js
Normal file
27
admin/src/api/biz/biz_subscription_server.js
Normal file
@@ -0,0 +1,27 @@
|
||||
class BizSubscriptionServer {
|
||||
async page(row) {
|
||||
return window.framework.http.post("/biz_subscription/page", row);
|
||||
}
|
||||
|
||||
async byUser(userId) {
|
||||
return window.framework.http.get("/biz_subscription/by_user", { user_id: userId });
|
||||
}
|
||||
|
||||
async open(row) {
|
||||
return window.framework.http.post("/biz_subscription/open", row);
|
||||
}
|
||||
|
||||
async upgrade(row) {
|
||||
return window.framework.http.post("/biz_subscription/upgrade", row);
|
||||
}
|
||||
|
||||
async renew(row) {
|
||||
return window.framework.http.post("/biz_subscription/renew", row);
|
||||
}
|
||||
|
||||
async cancel(row) {
|
||||
return window.framework.http.post("/biz_subscription/cancel", row);
|
||||
}
|
||||
}
|
||||
|
||||
export default new BizSubscriptionServer();
|
||||
15
admin/src/api/biz/biz_token_server.js
Normal file
15
admin/src/api/biz/biz_token_server.js
Normal file
@@ -0,0 +1,15 @@
|
||||
class BizTokenServer {
|
||||
async page(row) {
|
||||
return window.framework.http.post("/biz_token/page", row);
|
||||
}
|
||||
|
||||
async create(row) {
|
||||
return window.framework.http.post("/biz_token/create", row);
|
||||
}
|
||||
|
||||
async revoke(row) {
|
||||
return window.framework.http.post("/biz_token/revoke", row);
|
||||
}
|
||||
}
|
||||
|
||||
export default new BizTokenServer();
|
||||
27
admin/src/api/biz/biz_user_server.js
Normal file
27
admin/src/api/biz/biz_user_server.js
Normal file
@@ -0,0 +1,27 @@
|
||||
class BizUserServer {
|
||||
async page(row) {
|
||||
return window.framework.http.post("/biz_user/page", row);
|
||||
}
|
||||
|
||||
async add(row) {
|
||||
return window.framework.http.post("/biz_user/add", row);
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
return window.framework.http.post("/biz_user/edit", row);
|
||||
}
|
||||
|
||||
async del(row) {
|
||||
return window.framework.http.post("/biz_user/del", row);
|
||||
}
|
||||
|
||||
async detail(id) {
|
||||
return window.framework.http.get(`/biz_user/detail?id=${encodeURIComponent(id)}`, {});
|
||||
}
|
||||
|
||||
async disable(row) {
|
||||
return window.framework.http.post("/biz_user/disable", row);
|
||||
}
|
||||
}
|
||||
|
||||
export default new BizUserServer();
|
||||
33
admin/src/api/resources/sys_file_server.js
Normal file
33
admin/src/api/resources/sys_file_server.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import request from '@/libs/http'
|
||||
|
||||
export const getList = (params) => {
|
||||
return request({
|
||||
url: '/sys_file/page',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const add = (data) => {
|
||||
return request({
|
||||
url: '/sys_file',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const edit = (data) => {
|
||||
return request({
|
||||
url: '/sys_file',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const del = (params) => {
|
||||
return request({
|
||||
url: '/sys_file',
|
||||
method: 'delete',
|
||||
params
|
||||
})
|
||||
}
|
||||
36
admin/src/api/system/fileServe.js
Normal file
36
admin/src/api/system/fileServe.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import config from '../../../config/index.js'
|
||||
|
||||
/**
|
||||
* 使用原生 fetch 发送 multipart/form-data,不设置 Content-Type,由浏览器自动带 boundary,
|
||||
* 确保后端从 ctx.request.files 接收文件
|
||||
*/
|
||||
async function uploadOosImgWithFormData(formData) {
|
||||
const apiUrl = (config.apiUrl || '').replace(/\/$/, '')
|
||||
const url = `${apiUrl}/sys_file/upload_oos_img`
|
||||
const headers = {}
|
||||
if (window.framework && typeof window.framework.getToken === 'function') {
|
||||
const token = window.framework.getToken()
|
||||
if (token) headers['Authorization'] = token
|
||||
}
|
||||
const response = await fetch(url, { method: 'POST', headers, body: formData })
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return data
|
||||
}
|
||||
|
||||
class FileServe {
|
||||
async upload_oos_img(row) {
|
||||
if (row instanceof FormData) {
|
||||
return uploadOosImgWithFormData(row)
|
||||
}
|
||||
let res = await window.framework.http.postFormData('/sys_file/upload_oos_img', row)
|
||||
return res
|
||||
}
|
||||
|
||||
async upload_Img(row) {
|
||||
let res = await window.framework.http.postFormData("/file/upload_Img", row);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const fileServe = new FileServe();
|
||||
export default fileServe;
|
||||
30
admin/src/api/system/rolePermissionServer.js
Normal file
30
admin/src/api/system/rolePermissionServer.js
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
class RolePermissionServer {
|
||||
async getRoles(callback) {
|
||||
let res = await window.framework.http.get('/SysRolePermission/Query', {})
|
||||
return res
|
||||
}
|
||||
|
||||
async getRole(row) {
|
||||
let res = await window.framework.http.get('/SysRolePermission/QueryByRoleId', row)
|
||||
return res
|
||||
}
|
||||
|
||||
async add(row) {
|
||||
let res = await window.framework.http.post('/SysRolePermission/add', row)
|
||||
return res
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
let res = await window.framework.http.post('/SysRolePermission/edit', row)
|
||||
return res
|
||||
}
|
||||
|
||||
async del(row) {
|
||||
let res = await window.framework.http.post('/SysRolePermission/del', row)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
const rolePermissionServer = new RolePermissionServer()
|
||||
export default rolePermissionServer
|
||||
26
admin/src/api/system/roleServer.js
Normal file
26
admin/src/api/system/roleServer.js
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
class RoleServer {
|
||||
async list() {
|
||||
let res = await window.framework.http.get("/sys_role/index", {});
|
||||
return res;
|
||||
}
|
||||
|
||||
async add(row) {
|
||||
let res = await window.framework.http.post("/sys_role/add", row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
let res = await window.framework.http.post("/sys_role/edit", row);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async del(row) {
|
||||
let res = await window.framework.http.post("/sys_role/del", row);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const roleServer = new RoleServer();
|
||||
export default roleServer;
|
||||
10
admin/src/api/system/sysAddressServer.js
Normal file
10
admin/src/api/system/sysAddressServer.js
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
class SysAddress {
|
||||
async index(param) {
|
||||
let res = await window.framework.http.get("/sys_address/index", param);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const sysAddress = new SysAddress();
|
||||
export default sysAddress;
|
||||
30
admin/src/api/system/sysModuleServer.js
Normal file
30
admin/src/api/system/sysModuleServer.js
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
class SysModuleServer {
|
||||
async all() {
|
||||
let res = await window.framework.http.get("/sys_menu/all", {});
|
||||
return res;
|
||||
}
|
||||
|
||||
async list(row) {
|
||||
let res = await window.framework.http.get("/sys_menu/all", row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async add(row) {
|
||||
let res = await window.framework.http.post("/sys_menu/add", row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
let res = await window.framework.http.post("/sys_menu/edit", row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async del(row) {
|
||||
let res = await window.framework.http.post("/sys_menu/del", row);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const sysModuleServer = new SysModuleServer();
|
||||
export default sysModuleServer;
|
||||
30
admin/src/api/system/sys_log_serve.js
Normal file
30
admin/src/api/system/sys_log_serve.js
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
class SysLogServe {
|
||||
async all(param) {
|
||||
let res = await window.framework.http.get("/sys_log/all", param);
|
||||
return res;
|
||||
}
|
||||
|
||||
async detail(param) {
|
||||
let res = await window.framework.http.get("/sys_log/detail", param);
|
||||
return res;
|
||||
}
|
||||
|
||||
async delete(param) {
|
||||
let res = await window.framework.http.get("/sys_log/delete", param);
|
||||
return res;
|
||||
}
|
||||
|
||||
async delete_all(param) {
|
||||
let res = await window.framework.http.get("/sys_log/delete_all", param);
|
||||
return res;
|
||||
}
|
||||
|
||||
async operates(param) {
|
||||
let res = await window.framework.http.get("/sys_log/operates", param);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const sys_log_serve = new SysLogServe();
|
||||
export default sys_log_serve;
|
||||
38
admin/src/api/system/systemType_server.js
Normal file
38
admin/src/api/system/systemType_server.js
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
class systemTypeClServer {
|
||||
async all(param) {
|
||||
let res= await window.framework.http.get('/sys_project_type/all', param);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
async page(row) {
|
||||
let res= await window.framework.http.post('/sys_project_type/page', row);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
async exportCsv(row) {
|
||||
let res = window.framework.http.fileExport("/sys_project_type/export", row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async add(row) {
|
||||
let res= await window.framework.http.post('/sys_project_type/add', row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
let res= await window.framework.http.post('/sys_project_type/edit', row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async del(row) {
|
||||
let res= await window.framework.http.post('/sys_project_type/del', row);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const systemTypeServer = new systemTypeClServer();
|
||||
export default systemTypeServer;
|
||||
|
||||
31
admin/src/api/system/tableServer.js
Normal file
31
admin/src/api/system/tableServer.js
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
class TableServer {
|
||||
async getAll(callback) {
|
||||
return await window.framework.http.get('/table/index', {})
|
||||
}
|
||||
|
||||
async add(row, callback) {
|
||||
return await window.framework.http.post('/table/add', row)
|
||||
}
|
||||
|
||||
async edit(row, callback) {
|
||||
return await window.framework.http.post('/table/edit', row, function(res) {
|
||||
callback && callback(res)
|
||||
})
|
||||
}
|
||||
|
||||
async del(row, callback) {
|
||||
return await window.framework.http.post('/table/del', row)
|
||||
}
|
||||
|
||||
async autoApi(id) {
|
||||
return await window.framework.http.get('/template/api', { id: id })
|
||||
}
|
||||
|
||||
async autoDb(id) {
|
||||
return await window.framework.http.get('/template/autoDb', { id: id })
|
||||
}
|
||||
}
|
||||
|
||||
const tableServer = new TableServer()
|
||||
export default tableServer
|
||||
40
admin/src/api/system/userServer.js
Normal file
40
admin/src/api/system/userServer.js
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
class UserServer {
|
||||
async login(row) {
|
||||
let res = await window.framework.http.post("/sys_user/login", row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async all() {
|
||||
let res = await window.framework.http.get("/sys_user/index", {});
|
||||
return res;
|
||||
}
|
||||
|
||||
async exportCsv(row) {
|
||||
let res = window.framework.http.fileExport("/sys_user/export", row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async authorityMenus() {
|
||||
let res = await window.framework.http.post("/sys_user/authorityMenus", {});
|
||||
return res;
|
||||
}
|
||||
|
||||
async add(row) {
|
||||
let res = await window.framework.http.post("/sys_user/add", row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async edit(row) {
|
||||
let res = await window.framework.http.post("/sys_user/edit", row);
|
||||
return res;
|
||||
}
|
||||
|
||||
async del(row) {
|
||||
let res = await window.framework.http.post("/sys_user/del", row);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const userServer = new UserServer();
|
||||
export default userServer;
|
||||
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 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 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
39
admin/src/main.js
Normal file
39
admin/src/main.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// 引入 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'
|
||||
|
||||
const apiUrl = config.apiUrl
|
||||
|
||||
// 【超级简化】只需一个函数调用!
|
||||
const app = AdminFramework.createApp({
|
||||
title: '管理后台',
|
||||
apiUrl: apiUrl, // API 地址(uploadUrl 会自动设置为 apiUrl + 'upload')
|
||||
componentMap: componentMap, // 传入组件映射表,用于动态路由
|
||||
onReady() {
|
||||
// 可选:应用启动完成后的回调
|
||||
console.log('应用已准备就绪!')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 注册全局组件
|
||||
AdminFramework.registerComponents(Vue, {
|
||||
'CustomTabs': CustomTabs,
|
||||
'CustomTabPane': CustomTabPane
|
||||
})
|
||||
|
||||
// 挂载应用
|
||||
app.$mount('#app')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
18
admin/src/router/component-map.js
Normal file
18
admin/src/router/component-map.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// 组件映射表:后端菜单返回的 component 路径需与此处 key 一致(不含 .vue)
|
||||
import TestPage from '../views/test/test.vue'
|
||||
import BizUsers from '../views/biz/biz_users.vue'
|
||||
import BizPlans from '../views/biz/biz_plans.vue'
|
||||
import BizSubscriptions from '../views/biz/biz_subscriptions.vue'
|
||||
import BizTokens from '../views/biz/biz_tokens.vue'
|
||||
import BizPayment from '../views/biz/biz_payment.vue'
|
||||
|
||||
const componentMap = {
|
||||
'test/test': TestPage,
|
||||
'biz/user': BizUsers,
|
||||
'biz/plan': BizPlans,
|
||||
'biz/subscription': BizSubscriptions,
|
||||
'biz/token': BizTokens,
|
||||
'biz/payment': BizPayment,
|
||||
}
|
||||
|
||||
export default componentMap;
|
||||
96
admin/src/views/biz/biz_payment.vue
Normal file
96
admin/src/views/biz/biz_payment.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="biz-page">
|
||||
<h2 class="biz-title">支付确认(轻量)</h2>
|
||||
<p class="biz-desc">将 <code>pending</code> 订阅置为 <code>active</code>,并写入支付单号。</p>
|
||||
<Card dis-hover title="线下确认" style="max-width: 520px; margin-bottom: 16px">
|
||||
<Form :label-width="110">
|
||||
<FormItem label="订阅ID">
|
||||
<Input v-model="offline.subscription_id" type="number" placeholder="biz_subscriptions.id" />
|
||||
</FormItem>
|
||||
<FormItem label="支付单号">
|
||||
<Input v-model="offline.payment_ref" placeholder="流水号/凭证号" />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" :loading="loading1" @click="doOffline">确认线下收款</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card dis-hover title="链接支付确认" style="max-width: 520px">
|
||||
<Form :label-width="110">
|
||||
<FormItem label="订阅ID">
|
||||
<Input v-model="link.subscription_id" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="第三方单号">
|
||||
<Input v-model="link.payment_ref" />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" :loading="loading2" @click="doLink">确认链接支付</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bizPaymentServer from '@/api/biz/biz_payment_server.js'
|
||||
|
||||
export default {
|
||||
name: 'BizPayment',
|
||||
data() {
|
||||
return {
|
||||
offline: { subscription_id: '', payment_ref: '' },
|
||||
link: { subscription_id: '', payment_ref: '' },
|
||||
loading1: false,
|
||||
loading2: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async doOffline() {
|
||||
this.loading1 = true
|
||||
try {
|
||||
const res = await bizPaymentServer.confirmOffline({
|
||||
subscription_id: Number(this.offline.subscription_id),
|
||||
payment_ref: this.offline.payment_ref,
|
||||
})
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已确认,订阅已激活')
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '失败')
|
||||
}
|
||||
} finally {
|
||||
this.loading1 = false
|
||||
}
|
||||
},
|
||||
async doLink() {
|
||||
this.loading2 = true
|
||||
try {
|
||||
const res = await bizPaymentServer.confirmLink({
|
||||
subscription_id: Number(this.link.subscription_id),
|
||||
payment_ref: this.link.payment_ref,
|
||||
})
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已确认,订阅已激活')
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '失败')
|
||||
}
|
||||
} finally {
|
||||
this.loading2 = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.biz-page {
|
||||
padding: 16px;
|
||||
}
|
||||
.biz-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.biz-desc {
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
258
admin/src/views/biz/biz_plans.vue
Normal file
258
admin/src/views/biz/biz_plans.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<div class="biz-page">
|
||||
<div class="biz-toolbar">
|
||||
<h2 class="biz-title">套餐</h2>
|
||||
<Button type="primary" @click="openEdit(null)">新增套餐</Button>
|
||||
<Button class="ml8" @click="load(1)">刷新</Button>
|
||||
</div>
|
||||
<div class="biz-search">
|
||||
<Form inline :label-width="70">
|
||||
<FormItem label="条件">
|
||||
<Select v-model="param.seachOption.key" style="width: 140px">
|
||||
<Option value="plan_code">编码</Option>
|
||||
<Option value="plan_name">名称</Option>
|
||||
<Option value="status">状态</Option>
|
||||
</Select>
|
||||
<Input v-model="param.seachOption.value" class="ml8" style="width: 220px" search @on-search="load(1)" />
|
||||
</FormItem>
|
||||
<Button type="primary" @click="load(1)">查询</Button>
|
||||
</Form>
|
||||
</div>
|
||||
<Table :columns="columns" :data="rows" border stripe />
|
||||
<div class="biz-page-bar">
|
||||
<Page
|
||||
:total="total"
|
||||
:current="param.pageOption.page"
|
||||
:page-size="param.pageOption.pageSize"
|
||||
show-total
|
||||
@on-change="onPage"
|
||||
@on-page-size-change="onSize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal v-model="modal" :title="form.id ? '编辑套餐' : '新增套餐'" width="720" :loading="saving" @on-ok="save">
|
||||
<Form ref="formRef" :model="form" :rules="rules" :label-width="120">
|
||||
<FormItem label="套餐编码" prop="plan_code">
|
||||
<Input v-model="form.plan_code" :disabled="!!form.id" />
|
||||
</FormItem>
|
||||
<FormItem label="套餐名称" prop="plan_name">
|
||||
<Input v-model="form.plan_name" />
|
||||
</FormItem>
|
||||
<FormItem label="月费">
|
||||
<Input v-model="form.monthly_price" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="授权费">
|
||||
<Input v-model="form.auth_fee" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="账号上限">
|
||||
<Input v-model="form.account_limit" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="活跃上限">
|
||||
<Input v-model="form.active_user_limit" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="消息额度">
|
||||
<Input v-model="form.msg_quota" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="群发额度">
|
||||
<Input v-model="form.mass_quota" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="好友额度">
|
||||
<Input v-model="form.friend_quota" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="朋友圈额度">
|
||||
<Input v-model="form.sns_quota" type="number" />
|
||||
</FormItem>
|
||||
<FormItem label="功能点 JSON">
|
||||
<Input v-model="featuresText" type="textarea" :rows="4" placeholder='如 {"msg":true} 或 ["msg","mass"]' />
|
||||
</FormItem>
|
||||
<FormItem label="状态" prop="status">
|
||||
<Select v-model="form.status" style="width: 100%">
|
||||
<Option value="active">上线</Option>
|
||||
<Option value="inactive">下线</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bizPlanServer from '@/api/biz/biz_plan_server.js'
|
||||
|
||||
export default {
|
||||
name: 'BizPlans',
|
||||
data() {
|
||||
return {
|
||||
rows: [],
|
||||
total: 0,
|
||||
param: {
|
||||
seachOption: { key: 'plan_code', value: '' },
|
||||
pageOption: { page: 1, pageSize: 20, total: 0 },
|
||||
},
|
||||
modal: false,
|
||||
saving: false,
|
||||
form: {},
|
||||
featuresText: '{}',
|
||||
rules: {
|
||||
plan_code: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
plan_name: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '必选', trigger: 'change' }],
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns() {
|
||||
return [
|
||||
{ title: 'ID', key: 'id', width: 70 },
|
||||
{ title: '编码', key: 'plan_code', width: 120 },
|
||||
{ title: '名称', key: 'plan_name', minWidth: 140 },
|
||||
{ title: '月费', key: 'monthly_price', width: 90 },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'a',
|
||||
width: 260,
|
||||
render: (h, p) =>
|
||||
h('div', [
|
||||
h('Button', { props: { type: 'info', size: 'small' }, on: { click: () => this.openEdit(p.row) } }, '编辑'),
|
||||
h('Button', { props: { type: 'warning', size: 'small' }, class: { ml8: true }, on: { click: () => this.toggle(p.row) } }, '上下线'),
|
||||
h('Button', { props: { type: 'error', size: 'small' }, class: { ml8: true }, on: { click: () => this.doDel(p.row) } }, '删除'),
|
||||
]),
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load(1)
|
||||
},
|
||||
methods: {
|
||||
async load(page) {
|
||||
if (page) this.param.pageOption.page = page
|
||||
const res = await bizPlanServer.page({ param: this.param })
|
||||
if (res && res.code === 0) {
|
||||
this.rows = res.data.rows || []
|
||||
this.total = res.data.count || 0
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '加载失败')
|
||||
}
|
||||
},
|
||||
onPage(p) {
|
||||
this.param.pageOption.page = p
|
||||
this.load()
|
||||
},
|
||||
onSize(s) {
|
||||
this.param.pageOption.pageSize = s
|
||||
this.load(1)
|
||||
},
|
||||
openEdit(row) {
|
||||
if (row) {
|
||||
this.form = { ...row }
|
||||
this.featuresText =
|
||||
row.enabled_features == null
|
||||
? ''
|
||||
: typeof row.enabled_features === 'string'
|
||||
? row.enabled_features
|
||||
: JSON.stringify(row.enabled_features, null, 2)
|
||||
} else {
|
||||
this.form = {
|
||||
plan_code: '',
|
||||
plan_name: '',
|
||||
monthly_price: 0,
|
||||
auth_fee: 0,
|
||||
account_limit: 0,
|
||||
active_user_limit: 0,
|
||||
msg_quota: 0,
|
||||
mass_quota: 0,
|
||||
friend_quota: 0,
|
||||
sns_quota: 0,
|
||||
status: 'active',
|
||||
}
|
||||
this.featuresText = '{}'
|
||||
}
|
||||
this.modal = true
|
||||
},
|
||||
save() {
|
||||
this.saving = true
|
||||
this.$refs.formRef.validate(async (ok) => {
|
||||
if (!ok) {
|
||||
this.saving = false
|
||||
return
|
||||
}
|
||||
let enabled_features = null
|
||||
const t = (this.featuresText || '').trim()
|
||||
if (t) {
|
||||
try {
|
||||
enabled_features = JSON.parse(t)
|
||||
} catch (e) {
|
||||
this.$Message.error('功能点 JSON 格式错误')
|
||||
this.saving = false
|
||||
return
|
||||
}
|
||||
}
|
||||
const payload = { ...this.form, enabled_features }
|
||||
try {
|
||||
const res = this.form.id ? await bizPlanServer.edit(payload) : await bizPlanServer.add(payload)
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('保存成功')
|
||||
this.modal = false
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '保存失败')
|
||||
}
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
})
|
||||
},
|
||||
async toggle(row) {
|
||||
const res = await bizPlanServer.toggle({ id: row.id })
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('状态已更新为 ' + (res.data && res.data.status))
|
||||
this.load()
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '失败')
|
||||
}
|
||||
},
|
||||
doDel(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '删除套餐',
|
||||
content: '确认删除?若已被订阅引用可能失败。',
|
||||
onOk: async () => {
|
||||
const res = await bizPlanServer.del({ id: row.id })
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已删除')
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.biz-page {
|
||||
padding: 16px;
|
||||
}
|
||||
.biz-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.biz-title {
|
||||
display: inline-block;
|
||||
margin: 0 16px 0 0;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ml8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.biz-search {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.biz-page-bar {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
280
admin/src/views/biz/biz_subscriptions.vue
Normal file
280
admin/src/views/biz/biz_subscriptions.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="biz-page">
|
||||
<div class="biz-toolbar">
|
||||
<h2 class="biz-title">订阅</h2>
|
||||
<Button type="primary" @click="openOpen">开通订阅</Button>
|
||||
<Button class="ml8" @click="load(1)">刷新</Button>
|
||||
</div>
|
||||
<div class="biz-search">
|
||||
<Form inline>
|
||||
<FormItem label="用户ID">
|
||||
<Input v-model="param.seachOption.value" style="width: 140px" placeholder="筛选 user_id" />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Select v-model="param.seachOption.key" style="width: 120px">
|
||||
<Option value="user_id">用户ID</Option>
|
||||
<Option value="status">状态</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<Button type="primary" @click="load(1)">查询</Button>
|
||||
</Form>
|
||||
</div>
|
||||
<Table :columns="columns" :data="rows" border stripe />
|
||||
<div class="biz-page-bar">
|
||||
<Page
|
||||
:total="total"
|
||||
:current="param.pageOption.page"
|
||||
:page-size="param.pageOption.pageSize"
|
||||
show-total
|
||||
@on-change="onPage"
|
||||
@on-page-size-change="onSize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal v-model="openModal" title="开通订阅" width="640" :loading="saving" @on-ok="submitOpen">
|
||||
<Form :label-width="110">
|
||||
<FormItem label="用户ID"><Input v-model="openForm.user_id" type="number" /></FormItem>
|
||||
<FormItem label="套餐ID"><Input v-model="openForm.plan_id" type="number" /></FormItem>
|
||||
<FormItem label="开始时间"><Input v-model="openForm.start_time" placeholder="2025-01-01 00:00:00" /></FormItem>
|
||||
<FormItem label="结束时间"><Input v-model="openForm.end_time" placeholder="2025-12-31 23:59:59" /></FormItem>
|
||||
<FormItem label="状态">
|
||||
<Select v-model="openForm.status" style="width: 100%">
|
||||
<Option value="pending">pending</Option>
|
||||
<Option value="active">active</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="续费方式">
|
||||
<Select v-model="openForm.renew_mode" style="width: 100%">
|
||||
<Option value="manual">manual</Option>
|
||||
<Option value="auto">auto</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="支付渠道">
|
||||
<Select v-model="openForm.payment_channel" clearable style="width: 100%">
|
||||
<Option value="offline">offline</Option>
|
||||
<Option value="pay_link">pay_link</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="支付单号"><Input v-model="openForm.payment_ref" /></FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="renewModal" title="续费" :loading="saving" @on-ok="submitRenew">
|
||||
<Form :label-width="100">
|
||||
<FormItem label="新结束时间"><Input v-model="renewForm.end_time" /></FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="upgradeModal" title="升级套餐" :loading="saving" @on-ok="submitUpgrade">
|
||||
<Form :label-width="100">
|
||||
<FormItem label="新套餐ID"><Input v-model="upgradeForm.new_plan_id" type="number" /></FormItem>
|
||||
<FormItem label="开始"><Input v-model="upgradeForm.start_time" /></FormItem>
|
||||
<FormItem label="结束"><Input v-model="upgradeForm.end_time" /></FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bizSubscriptionServer from '@/api/biz/biz_subscription_server.js'
|
||||
|
||||
export default {
|
||||
name: 'BizSubscriptions',
|
||||
data() {
|
||||
return {
|
||||
rows: [],
|
||||
total: 0,
|
||||
param: {
|
||||
seachOption: { key: 'user_id', value: '' },
|
||||
pageOption: { page: 1, pageSize: 20, total: 0 },
|
||||
},
|
||||
openModal: false,
|
||||
renewModal: false,
|
||||
upgradeModal: false,
|
||||
saving: false,
|
||||
currentRow: null,
|
||||
openForm: {},
|
||||
renewForm: { end_time: '' },
|
||||
upgradeForm: { new_plan_id: '', start_time: '', end_time: '' },
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns() {
|
||||
return [
|
||||
{ title: 'ID', key: 'id', width: 70 },
|
||||
{ title: '用户', key: 'user_id', width: 90 },
|
||||
{ title: '套餐', key: 'plan_id', width: 90 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '开始', key: 'start_time', minWidth: 150 },
|
||||
{ title: '结束', key: 'end_time', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'a',
|
||||
width: 200,
|
||||
render: (h, p) =>
|
||||
h('div', [
|
||||
h('Button', { props: { size: 'small' }, on: { click: () => this.openRenew(p.row) } }, '续费'),
|
||||
h('Button', { props: { size: 'small' }, class: { ml8: true }, on: { click: () => this.openUpgrade(p.row) } }, '升级'),
|
||||
h('Button', { props: { type: 'error', size: 'small' }, class: { ml8: true }, on: { click: () => this.doCancel(p.row) } }, '取消'),
|
||||
]),
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load(1)
|
||||
},
|
||||
methods: {
|
||||
async load(page) {
|
||||
if (page) this.param.pageOption.page = page
|
||||
const res = await bizSubscriptionServer.page({ param: this.param })
|
||||
if (res && res.code === 0) {
|
||||
this.rows = res.data.rows || []
|
||||
this.total = res.data.count || 0
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '加载失败')
|
||||
}
|
||||
},
|
||||
onPage(p) {
|
||||
this.param.pageOption.page = p
|
||||
this.load()
|
||||
},
|
||||
onSize(s) {
|
||||
this.param.pageOption.pageSize = s
|
||||
this.load(1)
|
||||
},
|
||||
openOpen() {
|
||||
const now = new Date()
|
||||
const end = new Date(now)
|
||||
end.setFullYear(end.getFullYear() + 1)
|
||||
const fmt = (d) =>
|
||||
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(
|
||||
d.getHours()
|
||||
).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:00`
|
||||
this.openForm = {
|
||||
user_id: '',
|
||||
plan_id: '',
|
||||
start_time: fmt(now),
|
||||
end_time: fmt(end),
|
||||
status: 'pending',
|
||||
renew_mode: 'manual',
|
||||
payment_channel: '',
|
||||
payment_ref: '',
|
||||
}
|
||||
this.openModal = true
|
||||
},
|
||||
async submitOpen() {
|
||||
this.saving = true
|
||||
try {
|
||||
const body = {
|
||||
user_id: Number(this.openForm.user_id),
|
||||
plan_id: Number(this.openForm.plan_id),
|
||||
start_time: this.openForm.start_time,
|
||||
end_time: this.openForm.end_time,
|
||||
status: this.openForm.status,
|
||||
renew_mode: this.openForm.renew_mode,
|
||||
payment_channel: this.openForm.payment_channel || null,
|
||||
payment_ref: this.openForm.payment_ref || null,
|
||||
}
|
||||
const res = await bizSubscriptionServer.open(body)
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已创建订阅')
|
||||
this.openModal = false
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '失败')
|
||||
}
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
openRenew(row) {
|
||||
this.currentRow = row
|
||||
this.renewForm = { end_time: row.end_time || '' }
|
||||
this.renewModal = true
|
||||
},
|
||||
async submitRenew() {
|
||||
if (!this.currentRow) return
|
||||
this.saving = true
|
||||
try {
|
||||
const res = await bizSubscriptionServer.renew({
|
||||
subscription_id: this.currentRow.id,
|
||||
end_time: this.renewForm.end_time,
|
||||
})
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已续费')
|
||||
this.renewModal = false
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '失败')
|
||||
}
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
openUpgrade(row) {
|
||||
this.currentRow = row
|
||||
this.upgradeForm = { new_plan_id: row.plan_id, start_time: '', end_time: '' }
|
||||
this.upgradeModal = true
|
||||
},
|
||||
async submitUpgrade() {
|
||||
if (!this.currentRow) return
|
||||
this.saving = true
|
||||
try {
|
||||
const res = await bizSubscriptionServer.upgrade({
|
||||
subscription_id: this.currentRow.id,
|
||||
new_plan_id: Number(this.upgradeForm.new_plan_id),
|
||||
start_time: this.upgradeForm.start_time || undefined,
|
||||
end_time: this.upgradeForm.end_time || undefined,
|
||||
})
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已升级')
|
||||
this.upgradeModal = false
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '失败')
|
||||
}
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
doCancel(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '取消订阅',
|
||||
content: '确认取消?',
|
||||
onOk: async () => {
|
||||
const res = await bizSubscriptionServer.cancel({ subscription_id: row.id })
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已取消')
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.biz-page {
|
||||
padding: 16px;
|
||||
}
|
||||
.biz-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.biz-title {
|
||||
display: inline-block;
|
||||
margin: 0 16px 0 0;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ml8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.biz-page-bar {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
193
admin/src/views/biz/biz_tokens.vue
Normal file
193
admin/src/views/biz/biz_tokens.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="biz-page">
|
||||
<div class="biz-toolbar">
|
||||
<h2 class="biz-title">API Token</h2>
|
||||
<Button type="primary" @click="openCreate">创建 Token</Button>
|
||||
<Button class="ml8" @click="load(1)">刷新</Button>
|
||||
</div>
|
||||
<div class="biz-search">
|
||||
<Form inline>
|
||||
<FormItem label="用户ID">
|
||||
<Input v-model="param.seachOption.value" style="width: 140px" />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Select v-model="param.seachOption.key" style="width: 120px">
|
||||
<Option value="user_id">用户ID</Option>
|
||||
<Option value="status">状态</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<Button type="primary" @click="load(1)">查询</Button>
|
||||
</Form>
|
||||
</div>
|
||||
<Table :columns="columns" :data="rows" border stripe />
|
||||
<div class="biz-page-bar">
|
||||
<Page
|
||||
:total="total"
|
||||
:current="param.pageOption.page"
|
||||
:page-size="param.pageOption.pageSize"
|
||||
show-total
|
||||
@on-change="onPage"
|
||||
@on-page-size-change="onSize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal v-model="createModal" title="创建 Token" width="560" :loading="saving" @on-ok="submitCreate">
|
||||
<Form :label-width="100">
|
||||
<FormItem label="用户ID"><Input v-model="createForm.user_id" type="number" /></FormItem>
|
||||
<FormItem label="名称"><Input v-model="createForm.token_name" placeholder="default" /></FormItem>
|
||||
<FormItem label="过期时间"><Input v-model="createForm.expire_at" placeholder="2026-12-31 23:59:59" /></FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="plainModal" title="请立即保存 Token 明文" width="560" :closable="false">
|
||||
<Alert type="error">仅此一次展示,关闭后无法再次查看明文。</Alert>
|
||||
<Input type="textarea" :rows="4" v-model="plainToken" readonly />
|
||||
<div slot="footer">
|
||||
<Button type="primary" @click="plainModal = false">已保存</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bizTokenServer from '@/api/biz/biz_token_server.js'
|
||||
|
||||
export default {
|
||||
name: 'BizTokens',
|
||||
data() {
|
||||
return {
|
||||
rows: [],
|
||||
total: 0,
|
||||
param: {
|
||||
seachOption: { key: 'user_id', value: '' },
|
||||
pageOption: { page: 1, pageSize: 20, total: 0 },
|
||||
},
|
||||
createModal: false,
|
||||
plainModal: false,
|
||||
plainToken: '',
|
||||
saving: false,
|
||||
createForm: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns() {
|
||||
return [
|
||||
{ title: 'ID', key: 'id', width: 70 },
|
||||
{ title: '用户', key: 'user_id', width: 90 },
|
||||
{ title: '套餐', key: 'plan_id', width: 90 },
|
||||
{ title: '名称', key: 'token_name', width: 120 },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{ title: '过期', key: 'expire_at', minWidth: 150 },
|
||||
{ title: '最后使用', key: 'last_used_at', minWidth: 150 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'a',
|
||||
width: 100,
|
||||
render: (h, p) =>
|
||||
h(
|
||||
'Button',
|
||||
{
|
||||
props: { type: 'error', size: 'small' },
|
||||
on: {
|
||||
click: () => this.doRevoke(p.row),
|
||||
},
|
||||
},
|
||||
'吊销'
|
||||
),
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load(1)
|
||||
},
|
||||
methods: {
|
||||
async load(page) {
|
||||
if (page) this.param.pageOption.page = page
|
||||
const res = await bizTokenServer.page({ param: this.param })
|
||||
if (res && res.code === 0) {
|
||||
this.rows = res.data.rows || []
|
||||
this.total = res.data.count || 0
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '加载失败')
|
||||
}
|
||||
},
|
||||
onPage(p) {
|
||||
this.param.pageOption.page = p
|
||||
this.load()
|
||||
},
|
||||
onSize(s) {
|
||||
this.param.pageOption.pageSize = s
|
||||
this.load(1)
|
||||
},
|
||||
openCreate() {
|
||||
const d = new Date()
|
||||
d.setFullYear(d.getFullYear() + 1)
|
||||
const fmt = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(
|
||||
2,
|
||||
'0'
|
||||
)} 23:59:59`
|
||||
this.createForm = { user_id: '', token_name: 'default', expire_at: fmt }
|
||||
this.createModal = true
|
||||
},
|
||||
async submitCreate() {
|
||||
this.saving = true
|
||||
try {
|
||||
const res = await bizTokenServer.create({
|
||||
user_id: Number(this.createForm.user_id),
|
||||
token_name: this.createForm.token_name || 'default',
|
||||
expire_at: this.createForm.expire_at,
|
||||
})
|
||||
if (res && res.code === 0) {
|
||||
if (res.data.warn) this.$Message.warning(res.data.warn)
|
||||
this.createModal = false
|
||||
this.plainToken = res.data.plain_token
|
||||
this.plainModal = true
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '创建失败')
|
||||
}
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
doRevoke(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '吊销 Token',
|
||||
content: '确认吊销?',
|
||||
onOk: async () => {
|
||||
const res = await bizTokenServer.revoke({ id: row.id })
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已吊销')
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.biz-page {
|
||||
padding: 16px;
|
||||
}
|
||||
.biz-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.biz-title {
|
||||
display: inline-block;
|
||||
margin: 0 16px 0 0;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ml8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.biz-page-bar {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
270
admin/src/views/biz/biz_users.vue
Normal file
270
admin/src/views/biz/biz_users.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="biz-page">
|
||||
<div class="biz-toolbar">
|
||||
<h2 class="biz-title">业务用户</h2>
|
||||
<Button type="primary" @click="openEdit(null)">新增</Button>
|
||||
<Button class="ml8" @click="load(1)">刷新</Button>
|
||||
</div>
|
||||
<div class="biz-search">
|
||||
<Form inline :label-width="70">
|
||||
<FormItem label="条件">
|
||||
<Select v-model="param.seachOption.key" style="width: 140px">
|
||||
<Option value="mobile">手机</Option>
|
||||
<Option value="company_name">公司</Option>
|
||||
<Option value="status">状态</Option>
|
||||
</Select>
|
||||
<Input v-model="param.seachOption.value" placeholder="关键字" style="width: 220px" class="ml8" search @on-search="load(1)" />
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button type="primary" @click="load(1)">查询</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
<Table :columns="columns" :data="rows" border stripe />
|
||||
<div class="biz-page-bar">
|
||||
<Page
|
||||
:total="total"
|
||||
:current="param.pageOption.page"
|
||||
:page-size="param.pageOption.pageSize"
|
||||
show-total
|
||||
@on-change="onPage"
|
||||
@on-page-size-change="onSize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal v-model="modal" :title="form.id ? '编辑用户' : '新增用户'" width="640" :loading="saving" @on-ok="save">
|
||||
<Form ref="formRef" :model="form" :rules="rules" :label-width="100">
|
||||
<FormItem label="名称" prop="name">
|
||||
<Input v-model="form.name" />
|
||||
</FormItem>
|
||||
<FormItem label="手机" prop="mobile">
|
||||
<Input v-model="form.mobile" />
|
||||
</FormItem>
|
||||
<FormItem label="邮箱">
|
||||
<Input v-model="form.email" />
|
||||
</FormItem>
|
||||
<FormItem label="公司">
|
||||
<Input v-model="form.company_name" />
|
||||
</FormItem>
|
||||
<FormItem label="状态" prop="status">
|
||||
<Select v-model="form.status" style="width: 100%">
|
||||
<Option value="active">正常</Option>
|
||||
<Option value="disabled">禁用</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="detailVisible" title="用户详情" width="720" footer-hide>
|
||||
<p v-if="detail">Token 数量:{{ detail.tokenCount }}</p>
|
||||
<Table v-if="detail && detail.subscriptions" :columns="subCols" :data="detail.subscriptions" size="small" border />
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bizUserServer from '@/api/biz/biz_user_server.js'
|
||||
|
||||
export default {
|
||||
name: 'BizUsers',
|
||||
data() {
|
||||
return {
|
||||
rows: [],
|
||||
total: 0,
|
||||
param: {
|
||||
seachOption: { key: 'mobile', value: '' },
|
||||
pageOption: { page: 1, pageSize: 20, total: 0 },
|
||||
},
|
||||
modal: false,
|
||||
saving: false,
|
||||
form: {},
|
||||
rules: {
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
},
|
||||
detailVisible: false,
|
||||
detail: null,
|
||||
subCols: [
|
||||
{ title: 'ID', key: 'id', width: 80 },
|
||||
{ title: '套餐ID', key: 'plan_id', width: 90 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '开始', key: 'start_time', minWidth: 160 },
|
||||
{ title: '结束', key: 'end_time', minWidth: 160 },
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns() {
|
||||
return [
|
||||
{ title: 'ID', key: 'id', width: 80 },
|
||||
{ title: '名称', key: 'name', minWidth: 120 },
|
||||
{ title: '手机', key: 'mobile', width: 130 },
|
||||
{ title: '公司', key: 'company_name', minWidth: 140 },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'a',
|
||||
width: 220,
|
||||
render: (h, p) => {
|
||||
return h('div', [
|
||||
h(
|
||||
'Button',
|
||||
{
|
||||
props: { type: 'info', size: 'small' },
|
||||
on: { click: () => this.openEdit(p.row) },
|
||||
},
|
||||
'编辑'
|
||||
),
|
||||
h(
|
||||
'Button',
|
||||
{
|
||||
props: { type: 'default', size: 'small' },
|
||||
class: { ml8: true },
|
||||
on: { click: () => this.showDetail(p.row) },
|
||||
},
|
||||
'详情'
|
||||
),
|
||||
h(
|
||||
'Button',
|
||||
{
|
||||
props: { type: 'warning', size: 'small' },
|
||||
class: { ml8: true },
|
||||
on: { click: () => this.doDisable(p.row) },
|
||||
},
|
||||
'禁用'
|
||||
),
|
||||
h(
|
||||
'Button',
|
||||
{
|
||||
props: { type: 'error', size: 'small' },
|
||||
class: { ml8: true },
|
||||
on: { click: () => this.doDel(p.row) },
|
||||
},
|
||||
'删除'
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load(1)
|
||||
},
|
||||
methods: {
|
||||
async load(page) {
|
||||
if (page) this.param.pageOption.page = page
|
||||
const res = await bizUserServer.page({ param: this.param })
|
||||
if (res && res.code === 0) {
|
||||
this.rows = res.data.rows || []
|
||||
this.total = res.data.count || 0
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '加载失败')
|
||||
}
|
||||
},
|
||||
onPage(p) {
|
||||
this.param.pageOption.page = p
|
||||
this.load()
|
||||
},
|
||||
onSize(s) {
|
||||
this.param.pageOption.pageSize = s
|
||||
this.load(1)
|
||||
},
|
||||
openEdit(row) {
|
||||
if (row) {
|
||||
this.form = { ...row }
|
||||
} else {
|
||||
this.form = { name: '', mobile: '', email: '', company_name: '', status: 'active' }
|
||||
}
|
||||
this.modal = true
|
||||
},
|
||||
save() {
|
||||
this.saving = true
|
||||
this.$refs.formRef.validate(async (ok) => {
|
||||
if (!ok) {
|
||||
this.saving = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = this.form.id
|
||||
? await bizUserServer.edit(this.form)
|
||||
: await bizUserServer.add(this.form)
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('保存成功')
|
||||
this.modal = false
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '保存失败')
|
||||
}
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
})
|
||||
},
|
||||
async showDetail(row) {
|
||||
const res = await bizUserServer.detail(row.id)
|
||||
if (res && res.code === 0) {
|
||||
this.detail = res.data
|
||||
this.detailVisible = true
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '加载详情失败')
|
||||
}
|
||||
},
|
||||
doDisable(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '禁用用户',
|
||||
content: '确认禁用该用户?',
|
||||
onOk: async () => {
|
||||
const res = await bizUserServer.disable({ id: row.id })
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已禁用')
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '操作失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
doDel(row) {
|
||||
this.$Modal.confirm({
|
||||
title: '删除用户',
|
||||
content: '确认删除?若存在订阅/Token 可能受外键限制。',
|
||||
onOk: async () => {
|
||||
const res = await bizUserServer.del({ id: row.id })
|
||||
if (res && res.code === 0) {
|
||||
this.$Message.success('已删除')
|
||||
this.load(1)
|
||||
} else {
|
||||
this.$Message.error((res && res.message) || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.biz-page {
|
||||
padding: 16px;
|
||||
}
|
||||
.biz-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.biz-title {
|
||||
display: inline-block;
|
||||
margin: 0 16px 0 0;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ml8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.biz-search {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.biz-page-bar {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
35
admin/src/views/test/test.vue
Normal file
35
admin/src/views/test/test.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="test-page">
|
||||
<h2 class="test-page__title">Test</h2>
|
||||
<p class="test-page__desc">基础测试页面(动态菜单的 component 请填 <code>test/test</code>)</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TestPage'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.test-page {
|
||||
padding: 24px;
|
||||
}
|
||||
.test-page__title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.test-page__desc {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.test-page__desc code {
|
||||
padding: 2px 6px;
|
||||
font-size: 13px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
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