Compare commits
20 Commits
862f20e7e2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f27c350e2 | ||
|
|
602fed8f0c | ||
|
|
a6db441c4f | ||
|
|
45f6f330c6 | ||
|
|
daa6a46af6 | ||
|
|
51ff5d5d65 | ||
|
|
992238f352 | ||
|
|
c73afd2325 | ||
| 75149f994f | |||
| 25f3e8e989 | |||
|
|
ae4e342106 | ||
|
|
27dcf1e9b3 | ||
|
|
2b3b9748e1 | ||
|
|
885f86bcc9 | ||
|
|
b13fabb370 | ||
|
|
c3623d4a95 | ||
|
|
6de4936012 | ||
|
|
c3b003304f | ||
|
|
997565a9d1 | ||
|
|
83e22cc032 |
768
README.md
768
README.md
@@ -0,0 +1,768 @@
|
|||||||
|
# Admin Framework(admin-framework2)
|
||||||
|
|
||||||
|
基于 **Vue 2**、**Vue Router 3**、**Vuex 3**、**View Design(iView 4)** 的后台管理框架:登录、布局、动态菜单与路由、权限、HTTP 封装、系统管理页面等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 依赖与环境
|
||||||
|
|
||||||
|
### 1.1 Peer 依赖(由业务项目安装)
|
||||||
|
|
||||||
|
框架不打包以下依赖,需在宿主项目中安装并与框架版本兼容:
|
||||||
|
|
||||||
|
| 包 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| `vue` ^2.6 | 运行时 |
|
||||||
|
| `vue-router` ^3 | 路由 |
|
||||||
|
| `vuex` ^3 | 状态 |
|
||||||
|
| `view-design` ^4 | UI 组件库 |
|
||||||
|
| `axios` | HTTP(与框架内封装配合) |
|
||||||
|
|
||||||
|
### 1.2 构建产物(`dist/`)
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `admin-framework.js` | **UMD**,Terser 压缩,对外默认导出为框架单例;浏览器中为 `window.AdminFramework`,也可由 Webpack 等从该文件解析 `import` |
|
||||||
|
| `admin-framework.md` | 与根目录 `README.md` 内容相同,随 `npm run build` 一并复制到 `dist/`,便于随包分发使用说明 |
|
||||||
|
|
||||||
|
在项目根目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 引入方式
|
||||||
|
|
||||||
|
### 2.1 与 Webpack / Vue CLI 等业务工程配合
|
||||||
|
|
||||||
|
先安装 peer 依赖,将构建好的 `dist/admin-framework.js` 拷贝到业务项目或通过路径引用;在入口中:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import AdminFramework from './vendor/admin-framework.js'
|
||||||
|
// 若已将包发布到 npm 且 package.json 的 main 指向 dist,则:
|
||||||
|
// import AdminFramework from 'admin-framework'
|
||||||
|
```
|
||||||
|
|
||||||
|
打包器需将 `vue`、`vue-router`、`vuex`、`view-design`、`axios` 作为外部依赖或正常解析(与 UMD 的 externals 一致)。
|
||||||
|
|
||||||
|
### 2.2 页面中通过 `<script>` 引入
|
||||||
|
|
||||||
|
按顺序引入 Vue、VueRouter、Vuex、View Design、axios 的 UMD 脚本,再引入 `admin-framework.js`,使用全局 **`AdminFramework`** 调用 `createApp` 等 API。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 核心 API:一键创建应用
|
||||||
|
|
||||||
|
### 3.1 `AdminFramework.createApp(config)`
|
||||||
|
|
||||||
|
创建并配置 Vue 根实例:注册插件、全局属性、`componentMap`、Store、Router、布局等。**返回 Vue 实例**,需自行 `$mount`。
|
||||||
|
|
||||||
|
**`config` 常用字段:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `title` | `string` | 站点标题;未登录或接口失败时用作默认 `document.title` |
|
||||||
|
| `apiUrl` | `string` | 后端 API 根地址(必填,用于 `http` baseURL) |
|
||||||
|
| `uploadUrl` | `string` | 可选;不传时由 `apiUrl` 自动推导为 `apiUrl + 'upload'` 或 `apiUrl + '/upload'` |
|
||||||
|
| `componentMap` | `object` | 动态路由组件映射,见 [§7](#7-组件映射与动态路由) |
|
||||||
|
| `HomePage` | `Vue` 组件 | 可选,自定义首页;默认框架内置欢迎页 |
|
||||||
|
| `onReady` | `function` | 可选,根实例 `created` 末尾调用,`this` 为 Vue 根实例 |
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import AdminFramework from 'admin-framework'
|
||||||
|
import componentMap from './router/component-map.js'
|
||||||
|
|
||||||
|
const app = AdminFramework.createApp({
|
||||||
|
title: '某某管理系统',
|
||||||
|
apiUrl: 'https://api.example.com/admin_api/',
|
||||||
|
componentMap,
|
||||||
|
HomePage: null,
|
||||||
|
onReady() {
|
||||||
|
console.log('框架与路由就绪', this.$route.path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.$mount('#app')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 框架实例(`AdminFramework` / `window.framework`)
|
||||||
|
|
||||||
|
默认导出为**单例**。构建后也会挂到 `window.framework`(浏览器环境)。
|
||||||
|
|
||||||
|
### 4.1 常用属性
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `version` | 框架版本号字符串 |
|
||||||
|
| `config` | 最近一次 `createApp` 传入的配置 |
|
||||||
|
| `store` | Vuex Store 实例(`createApp` 之后可用) |
|
||||||
|
| `router` | Vue Router 实例 |
|
||||||
|
| `HomePage` | 当前使用的首页组件引用 |
|
||||||
|
| `http` | HTTP 封装实例,同 [§5](#5-http-模块) |
|
||||||
|
| `uiTool` | UI 与菜单工具类,同 [§6](#6-uitool-模块) |
|
||||||
|
| `tools` | 工具方法集合,同 [§6.2](#62-通用工具-tools) |
|
||||||
|
| `systemApi` | `src/api/system` 聚合导出,同 [§8](#8-系统-api-systemapi) |
|
||||||
|
| `pages` | 框架内置页面组件集合(登录、错误页、部分系统页等) |
|
||||||
|
| `components` | 布局与子组件导出(Main、TreeGrid 等) |
|
||||||
|
| `storeModules` | 默认 `user`、`app` 模块描述,扩展 Store 时可参考 |
|
||||||
|
| `createBaseRoutes` / `setupRouterGuards` / `registerComponents` | 高级自定义路由/组件时使用 |
|
||||||
|
|
||||||
|
### 4.2 `addComponentMap(customMap)`
|
||||||
|
|
||||||
|
在运行时追加组件映射(与 `createApp` 里的 `componentMap` 语义相同)。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import Games from './views/ball/games.vue'
|
||||||
|
|
||||||
|
AdminFramework.addComponentMap({
|
||||||
|
'ball/games': Games,
|
||||||
|
'ball/games.vue': Games
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 `initHttp(config, store)`
|
||||||
|
|
||||||
|
若未走 `createApp`、自行组装 Store 时,可用此方法初始化 `http`(一般使用 `createApp` 即可,内部已调用)。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
AdminFramework.initHttp(
|
||||||
|
{ apiUrl: 'https://api.example.com/admin_api/', timeout: 60000 },
|
||||||
|
storeInstance
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. HTTP 模块
|
||||||
|
|
||||||
|
通过 **`this.$http`**(组件内)或 **`AdminFramework.http`** / **`window.framework.http`** 访问。
|
||||||
|
|
||||||
|
### 5.1 初始化
|
||||||
|
|
||||||
|
由 `createApp({ apiUrl, ... })` 自动完成;请求头会带 `admin-token`(来自 `store.state.user.token`)。
|
||||||
|
|
||||||
|
### 5.2 约定
|
||||||
|
|
||||||
|
- 成功:后端 JSON 需 `code === 0`,否则走全局错误提示并 `reject`。
|
||||||
|
- `401`:清空 token,并跳转登录路由(依赖 `window.framework.router`)。
|
||||||
|
|
||||||
|
### 5.3 方法说明
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `init(config, store)` | 合并 `timeout` 等,绑定 Store |
|
||||||
|
| `baseUrl()` | 当前 `apiUrl` |
|
||||||
|
| `ImgSrc(src)` | 拼接图片完整 URL:`baseUrl() + src` |
|
||||||
|
| `getHttpInstance(extra)` | 返回带拦截器的 `axios` 实例 |
|
||||||
|
| `get(url, params, config)` | GET;`params` 会序列化,Date 会格式化为字符串 |
|
||||||
|
| `post(url, body, config)` | POST JSON |
|
||||||
|
| `postFormData(url, data)` | `application/x-www-form-urlencoded` |
|
||||||
|
| `fileExport(url, param, filename, is_down)` | 下载导出;内部可用 `uiTool.downloadFile` |
|
||||||
|
| `formatParamete` / `formatFormDataParam` | 参数预处理 |
|
||||||
|
| `showLoad` / `hideLoad` / `showError` | 与全局 loading、Message 联动 |
|
||||||
|
|
||||||
|
**示例(在 Vue 组件中):**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default {
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
const res = await this.$http.get('/sys_user/index', { page: 1, pageSize: 20 })
|
||||||
|
this.list = res.data
|
||||||
|
} catch (e) {
|
||||||
|
// 已全局提示
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
隐藏 loading:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await this.$http.get('/xxx', { id: 1 }, { hideLoad: true })
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. uiTool 模块
|
||||||
|
|
||||||
|
通过 **`this.$uiTool`** 或 **`AdminFramework.uiTool`** 使用(类静态方法)。
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `setComponentMap(map)` | 合并组件映射表 |
|
||||||
|
| `getComponent(path)` | 按路径取组件,`path` 可带或不带 `.vue` |
|
||||||
|
| `downloadFile(res, fileName)` | Blob 下载 |
|
||||||
|
| `setRem()` | 根据屏宽设置根字体(自适应) |
|
||||||
|
| `getImgSrc(src)` | 图片地址(同 http 规则) |
|
||||||
|
| `getBtn(h, options)` | Render 函数里生成操作按钮组 |
|
||||||
|
| `getDropdown(h, items)` | 下拉更多菜单 |
|
||||||
|
| `delConfirm(callback)` | 删除确认框 |
|
||||||
|
| `showConfirm({ title, content }, callback)` | 通用确认框 |
|
||||||
|
| `transformTree(list, cb)` | 扁平列表转树 |
|
||||||
|
| `subTree` / `menuToRoute` / `getRoutes` | 菜单转路由(框架内部与权限菜单配合) |
|
||||||
|
|
||||||
|
**树表示例:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const tree = this.$uiTool.transformTree(flatListFromApi)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.2 通用工具 `tools`
|
||||||
|
|
||||||
|
通过 **`this.$tools`** 使用(具体以源码 `src/utils/tools.js` 为准)。
|
||||||
|
|
||||||
|
常用包括:
|
||||||
|
|
||||||
|
- **Cookie / Token**:`getToken`、`setToken`、`TOKEN_KEY`
|
||||||
|
- **日期**:`formatDate(val, fmt)`(dayjs)
|
||||||
|
- **数组**:`forEach`、`hasOneOf`、`getUnion`、`getIntersection`
|
||||||
|
- **对象**:`objEqual`、`removeEmptyObject`、`isNullorEmpty`
|
||||||
|
- **路由/菜单**:`getBreadCrumbList`、`getHomeRoute`、`getMenuByRouter`、`filterMenu`
|
||||||
|
- **本地存储**:`localSave`、`localRead`
|
||||||
|
- **其它**:`generateUUID`、`getUrlParam`、`downStream`、`scrollTop` 等
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (this.$tools.getToken()) { /* 已登录 */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 组件映射与动态路由
|
||||||
|
|
||||||
|
后端菜单里配置的「组件地址」会对应到**字符串路径**(如 `system/sys_user`)。框架通过 **`componentMap`** 把路径映射到真实 Vue 组件。
|
||||||
|
|
||||||
|
**在 `createApp` 中传入:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import SysUser from './views/system/sys_user.vue'
|
||||||
|
|
||||||
|
AdminFramework.createApp({
|
||||||
|
apiUrl: '...',
|
||||||
|
componentMap: {
|
||||||
|
'system/sys_user': SysUser,
|
||||||
|
'system/sys_user.vue': SysUser
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
未映射的路径会显示占位提示(控制台 `warn`),便于排查。
|
||||||
|
|
||||||
|
框架内置已注册一批系统页(如 `system/sys_menu` 等),见 `src/views/index.js` 中 `setupComponentMap`;业务模块在 `componentMap` 中追加即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 系统 API(`systemApi`)
|
||||||
|
|
||||||
|
`AdminFramework.systemApi` 聚合导出 `src/api/system` 下各 `*Server`,例如:
|
||||||
|
|
||||||
|
| 导出名 | 模块文件 | 典型用途 |
|
||||||
|
|--------|----------|----------|
|
||||||
|
| `userServer` | `userServer.js` | 登录、用户 CRUD、权限菜单 |
|
||||||
|
| `menuServer` | `menuServer.js` | 菜单管理 |
|
||||||
|
| `roleServer` / `rolePermissionServer` | 角色与权限 |
|
||||||
|
| `paramSetupServer` | 系统参数(如站点标题、Logo) |
|
||||||
|
| `fileServe` | 文件上传下载 |
|
||||||
|
| `modelServer` / `tableServer` / `formServer` 等 | 低代码/表单相关 |
|
||||||
|
|
||||||
|
**调用示例:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { userServer } = AdminFramework.systemApi
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
const res = await userServer.login({ username: 'admin', password: '***' })
|
||||||
|
if (res.code === 0) {
|
||||||
|
// 写入 token、拉菜单等由登录页与 store 配合完成
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实际请求仍走全局配置的 `http`(`apiUrl`、token 头)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Vuex(`this.$store`)
|
||||||
|
|
||||||
|
默认模块:
|
||||||
|
|
||||||
|
### 9.1 `user`(`namespaced: true`)
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| `state` | `token`、`userName`、`authorityMenus`、`menuList` 等 |
|
||||||
|
| `mutations` | `setToken`、`setUserName`、`setAuthorityMenus`、`setMenuList` |
|
||||||
|
| `actions` | `setAuthorityMenus`:拉取或传入菜单 JSON,生成动态路由并写入 `localStorage` |
|
||||||
|
|
||||||
|
**登录后刷新菜单示例:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await this.$store.dispatch('user/setAuthorityMenus', {
|
||||||
|
Main: this.$framework.components.Main,
|
||||||
|
ParentView: this.$framework.components.ParentView,
|
||||||
|
Page404: this.$framework.pages.Page404,
|
||||||
|
HomePage: this.$framework.HomePage
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
(具体参数以当前框架版本 `user` 模块为准。)
|
||||||
|
|
||||||
|
### 9.2 `app`(`namespaced: true`)
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| `state` | `sysFormModel`(标题、Logo)、面包屑、首页路由 |
|
||||||
|
| `actions` | `getSysTitle`:根据参数与接口更新站点标题与 Logo |
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await this.$store.dispatch('app/getSysTitle', {
|
||||||
|
defaultTitle: this.$config.title,
|
||||||
|
defaultLogo: ''
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 全局 Vue 原型(组件内)
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `this.$config` | `createApp` 的 `config` |
|
||||||
|
| `this.$http` | HTTP 封装 |
|
||||||
|
| `this.$tools` | 工具集 |
|
||||||
|
| `this.$uiTool` | UI 工具类 |
|
||||||
|
| `this.$framework` | 框架单例(同 `AdminFramework`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 内置页面(`pages`)
|
||||||
|
|
||||||
|
- **页面**:`LoginPage`、`Page401`、`Page404`、`Page500`、`HomePage` 及多套 `system/*` 管理页(见 `src/views/index.js`)。
|
||||||
|
- 业务中也可通过 `AdminFramework.pages` 取到上述导出。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 组件使用说明(`src/components`)
|
||||||
|
|
||||||
|
`createApp` 执行时会对下列组件**全局注册**(见 `src/components/index.js` 中 `registerGlobalComponents`),在任意 `.vue` 模板中可直接写 **PascalCase 或 kebab-case** 标签名(如 `<Tables>` / `<tree-grid>`)。也可按需:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { Tables, TreeGrid, editModal } from 'admin-framework'
|
||||||
|
// 或在源码工程中:import Tables from '@/components/tables'
|
||||||
|
const { TreeGrid, Main } = AdminFramework.components
|
||||||
|
```
|
||||||
|
|
||||||
|
以下按功能分类;**Props / 事件以源码为准**,此处列出常用项。
|
||||||
|
|
||||||
|
### 12.1 布局与路由壳
|
||||||
|
|
||||||
|
| 组件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **Main** | 后台主布局:左侧 `SideMenu`、顶栏 `HeaderBar`、用户信息、`Terminal`、主内容区 `<router-view />`、内嵌 `LoadFlower`、返回顶部。无业务 Props,标题/Logo 来自 Vuex `app/sysFormModel`,菜单来自 `user/menuList`。 |
|
||||||
|
| **ParentView** | 仅渲染一层 `<router-view />`,用于菜单多级嵌套时的中间层。 |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 动态路由中由框架注册,一般不在页面手写 Main -->
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Main 子模块说明(`src/components/main/components`)
|
||||||
|
|
||||||
|
以下组件**默认未在 `registerGlobalComponents` 中注册**,由 **Main** 内部引用;若要在自定义布局中复用,需从路径手动 `import`(示例路径以源码工程 `@/components/...` 为准)。
|
||||||
|
|
||||||
|
**组合关系(`main.vue`)**:`Layout` 左侧为 `SideMenu` + 品牌区;右侧顶部 `HeaderBar`,其默认插槽内为 `Terminal` 与 `User`;内容区为 `<router-view />`、`LoadFlower`、`ABackTop`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**1. `SideMenu`(`side-menu/side-menu.vue`)**
|
||||||
|
|
||||||
|
侧栏菜单,基于 iView `Menu`,支持折叠态下的 `CollapsedMenu` 下拉。
|
||||||
|
|
||||||
|
| Props | 类型 | 说明 |
|
||||||
|
|-------|------|------|
|
||||||
|
| `menuList` | `Array` | 菜单树,项需含 `name`、`meta`(如 `icon`、`title`)、子节点 `children` 等,与动态路由菜单结构一致 |
|
||||||
|
| `collapsed` | `Boolean` | 是否折叠(仅图标 / 下拉) |
|
||||||
|
| `theme` | `String` | 菜单主题,默认 `light`;Main 中传入 `dark` |
|
||||||
|
| `activeName` | `String` | 当前激活菜单 `name`,一般传 `$route.name` |
|
||||||
|
| `openNames` | `Array` | 初始展开的 Submenu 名称列表 |
|
||||||
|
| `rootIconSize` / `iconSize` | `Number` | 折叠/展开时图标大小,默认 `20` / `16` |
|
||||||
|
|
||||||
|
| 插槽 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 默认 | 菜单上方的额外区域(可选) |
|
||||||
|
|
||||||
|
| 事件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `on-select` | 选中菜单项时,参数为路由 `name` 字符串(或外链特殊格式 `isTurnByHref_xxx`,由 Main 中 `turnToPage` 处理) |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<SideMenu
|
||||||
|
theme="dark"
|
||||||
|
:menu-list="menuList"
|
||||||
|
:collapsed="collapsed"
|
||||||
|
:active-name="$route.name"
|
||||||
|
@on-select="handleMenuSelect"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. `SideMenuItem`(`side-menu/side-menu-item.vue`)**
|
||||||
|
|
||||||
|
`SideMenu` 内部递归子组件:渲染 `Submenu` 标题与一层子 `menu-item`。依赖 mixin 提供的 `parentItem`、`children` 等逻辑,**一般不单独使用**。
|
||||||
|
|
||||||
|
**3. `CollapsedMenu`(`side-menu/collapsed-menu.vue`)**
|
||||||
|
|
||||||
|
侧栏折叠时的下拉菜单(`Dropdown` 嵌套)。由 `SideMenu` 使用。
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `parentItem` | 当前菜单节点 |
|
||||||
|
| `hideTitle` | 为 true 时仅图标(侧栏窄条场景) |
|
||||||
|
| `rootIconSize` / `iconSize` | 图标尺寸 |
|
||||||
|
| `theme` | 与侧栏主题一致,用于图标颜色 |
|
||||||
|
|
||||||
|
| 事件 | `on-click` — 点击子项时传出菜单 `name` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**4. `HeaderBar`(`header-bar/header-bar.vue`)**
|
||||||
|
|
||||||
|
顶栏:左侧 **折叠开关** + **面包屑**,右侧 **默认插槽**(Main 里放终端图标、用户区等)。
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `collapsed` | 当前侧栏是否折叠,传给 `SiderTrigger` |
|
||||||
|
|
||||||
|
| 插槽 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 默认 | 顶栏右侧自定义内容(如 `Terminal`、`User`) |
|
||||||
|
|
||||||
|
| 事件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `on-coll-change` | 折叠按钮切换,`payload` 为**新的** `collapsed` 布尔值(与 `SiderTrigger` 的 `on-change` 一致) |
|
||||||
|
|
||||||
|
面包屑数据来自 **`$route.matched`**(去掉第一项),每项包含 `name`、`path`、`meta`;展示标题用 `tools.showTitle`。
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<HeaderBar :collapsed="collapsed" @on-coll-change="collapsed = $event">
|
||||||
|
<Terminal /><User :userName="name" :user-avator="avatar" />
|
||||||
|
</HeaderBar>
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. `SiderTrigger`(`header-bar/sider-trigger/sider-trigger.vue`)**
|
||||||
|
|
||||||
|
折叠侧栏的图标按钮。
|
||||||
|
|
||||||
|
| Props | `collapsed`、`icon`(默认 `navicon-round`,Main 里为 `md-menu`)、`size`(默认 `26`) |
|
||||||
|
| 事件 | `on-change` — 参数为切换后的 `collapsed` 状态(布尔) |
|
||||||
|
|
||||||
|
**6. `customBreadCrumb`(`header-bar/custom-bread-crumb/custom-bread-crumb.vue`)**
|
||||||
|
|
||||||
|
自定义面包屑(`Breadcrumb`)。
|
||||||
|
|
||||||
|
| Props | `list`( `{ name, path, meta, to?, icon? }[]` )、`fontSize`(默认 `14`)、`showIcon` |
|
||||||
|
| 说明 | 文案通过 `showTitle(item, this)` 解析,支持 `meta.title` 等 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**7. `User`(`user/user.vue`)**
|
||||||
|
|
||||||
|
右上角用户名 + 头像下拉,**退出登录**走 Vuex `user/handleLogOut`。
|
||||||
|
|
||||||
|
| Props | `userName`、`userAvator`(字符串 URL;也可传对象,未用字符串时显示内置默认图) |
|
||||||
|
| 说明 | 内置 `message` 方法曾跳转 `message_page`,可按需扩展菜单项 |
|
||||||
|
|
||||||
|
**8. Terminal(`terminal/index.vue` + `terminal/terminal.vue`)**
|
||||||
|
|
||||||
|
入口为一颗 **iView `Icon`**,点击后用 **`AsyncModal`** 弹层展示内层终端视图。当前实现中日志区域为占位文案(**终端功能暂未启用**),按钮「重新加载 / 清空」会提示未启用。
|
||||||
|
|
||||||
|
复用时需保证工程内已注册 **`AsyncModal`**。`index.vue` 中 `ref="asyncModal"` 调用 `show()` 打开弹窗。
|
||||||
|
|
||||||
|
**9. `Language`(`language/language.vue`)**
|
||||||
|
|
||||||
|
语言下拉(`zh-CN` / `zh-TW` / `en-US`)。需在工程中配置 **`vue-i18n`**(`this.$i18n`)方可切换文案。
|
||||||
|
|
||||||
|
| Props | `lang` 当前语言代码 |
|
||||||
|
| 事件 | `on-lang-change` — 选中的语言 key |
|
||||||
|
|
||||||
|
> `main.vue` 当前模板未挂载 `Language`,若在自定义 Main 中需要多语言顶栏,可自行加入。
|
||||||
|
|
||||||
|
**10. `Fullscreen`(`fullscreen/fullscreen.vue`)**
|
||||||
|
|
||||||
|
全屏切换;非 IE 显示按钮。支持 **`v-model`**(`value` 是否全屏),全屏状态变化时触发 `input`、`on-change`。
|
||||||
|
|
||||||
|
> `main.vue` 当前未使用该组件,可按需放入 `HeaderBar` 插槽。
|
||||||
|
|
||||||
|
**11. `ABackTop`(`a-back-top/index.vue`)**
|
||||||
|
|
||||||
|
回到顶部浮动按钮,监听**滚动容器**滚动。
|
||||||
|
|
||||||
|
| Props | `height`(显示阈值,默认 `400`)、`bottom`、`right`、`duration`(滚动动画 ms)、`container`(默认 `window`,可为选择器字符串如 `'.content-wrapper'`) |
|
||||||
|
| 插槽 | 默认:自定义按钮外观;未提供时用内置箭头图标 |
|
||||||
|
| 方法 | 内部 `scrollTop` 滚动;点击触发回到顶部 |
|
||||||
|
|
||||||
|
Main 中示例:`<ABackTop container=".content-wrapper" :height="100" :bottom="80" :right="50" />`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**小结**:自定义后台壳时,可照搬 `main.vue` 结构,替换或增删 `HeaderBar` 插槽内子组件;侧栏行为与菜单数据结构强相关,需与 **`user/menuList`**、**路由 `meta`** 保持一致。
|
||||||
|
|
||||||
|
### 12.2 表格:`Tables`
|
||||||
|
|
||||||
|
基于 View Design `Table` 封装,支持分页条、导出、列配置、可编辑列等。
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `value` | 表格数据数组 |
|
||||||
|
| `columns` | 列配置(同 iView Table `columns`,框架内会做对齐等默认处理) |
|
||||||
|
| `title` / `tip` | 表格标题与灰色提示 |
|
||||||
|
| `isDown` | 为 true 且 `value` 非空时显示「下载」链接触发导出 |
|
||||||
|
| `pageOption` | 分页:`{ page, pageSize, total }`,存在则显示 `Page` |
|
||||||
|
| `width` / `height` | 传给内部 Table |
|
||||||
|
| `maxHeightOffset` | 表格外层最大高度:`auto`(默认,按视口计算)或数字,用于 `calc(100vh - offset)` |
|
||||||
|
|
||||||
|
| 插槽 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `header` | 标题行右侧区域 |
|
||||||
|
| 默认 | 透传给内部 Table(如 `slot-scope` 列) |
|
||||||
|
| `footer` / `loading` | 透传 Table 同名插槽 |
|
||||||
|
|
||||||
|
| 事件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `changePage` | 分页变更(内部已更新 `pageOption.page`) |
|
||||||
|
| `on-select` | 多选变化 |
|
||||||
|
| `downExecl` | 点击下载时,`{ value, columns }` |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Tables
|
||||||
|
:value="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
:page-option="{ page: 1, pageSize: 10, total: 100 }"
|
||||||
|
title="用户列表"
|
||||||
|
@changePage="loadData"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.3 树形表格:`TreeGrid`
|
||||||
|
|
||||||
|
树形多级表格,表头与行由 `columns`、`data` 驱动(数据需含 `children` 等树结构)。
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `columns` | 列配置 |
|
||||||
|
| `data` | 树形数据 |
|
||||||
|
|
||||||
|
其余属性通过 `v-bind="$attrs"` 传到内部子组件。
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<TreeGrid :columns="gridOption.columns" :data="gridOption.data" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.4 上传:`UploadSingle` / `UploadMultiple`
|
||||||
|
|
||||||
|
| 组件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **UploadSingle** | 单图。`v-model` 绑定服务器返回的**相对路径**字符串;上传地址为 `apiUrl + 'sys_file/upload_oos_img'`,请求头带 `admin-token`。成功:`input`、`change`。预览/删除支持。 |
|
||||||
|
| **UploadMultiple** | 多图(内部列表最多约 4 张)。`@handleSuccess` 回调 `{ file, uploadList }`;可调用 `setList(list)` / `getList()` 同步列表。 |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<UploadSingle v-model="form.avatar" />
|
||||||
|
<UploadMultiple ref="multi" @handleSuccess="onUploaded" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.5 富文本:`Editor`
|
||||||
|
|
||||||
|
基于 **wangEditor**。使用 `this.$config.apiUrl`(或等价配置)作为图片上传地址。
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `value` | HTML 字符串,支持 `v-model` |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Editor v-model="form.content" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.6 弹窗:`AsyncModal` / `editModal`
|
||||||
|
|
||||||
|
**AsyncModal** — 简单确认弹窗:
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `title` | 标题 |
|
||||||
|
| `width` | 宽度,默认约 `80`(Modal 数字宽度) |
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `show(callback)` | 打开;确定时**先**执行 `callback`(可 async),再 `on-ok` 并关闭 |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<AsyncModal ref="asyncModal" title="提示">
|
||||||
|
<p>自定义内容</p>
|
||||||
|
</AsyncModal>
|
||||||
|
<!-- this.$refs.asyncModal.show(async () => { await api(); }) -->
|
||||||
|
```
|
||||||
|
|
||||||
|
**editModal** — 由 `columns` 驱动表单项(内部用 `FieldRenderer`),用于新增/编辑:
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `columns` | 列/字段配置:`key`、`title`、`com`(Input、Select、Radio、`editRender` 等) |
|
||||||
|
| `width` | 弹窗宽度 |
|
||||||
|
| `rules` | 可通过父级传入覆盖默认校验(与 `Form` 一致) |
|
||||||
|
|
||||||
|
| 插槽 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `top` | 表单最上方 |
|
||||||
|
| `bottom` | 表单最下方(如额外表单项) |
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `addShow(row, callback)` | 新增;确定后 `callback(提交行数据)` |
|
||||||
|
| `editShow(row, callback)` | 编辑 |
|
||||||
|
| `showModal(row)` | 有 `id` 走编辑,否则新增 |
|
||||||
|
| `hide` / `setFooter` / `refresh` | 关闭、控制底部按钮、强制重渲染表单 |
|
||||||
|
|
||||||
|
| 事件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `on-save` | `{ data, isEdit }` |
|
||||||
|
| `on-visible-change` | Modal 显隐 |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<editModal ref="editModal" :columns="editColumns" :rules="rules">
|
||||||
|
<div slot="bottom"><fieldItem name="备注"><Input v-model="extra" /></fieldItem></div>
|
||||||
|
</editModal>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.7 表单项辅助:`fieldItem` / `FieldRenderer`
|
||||||
|
|
||||||
|
| 组件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **fieldItem** | 简单包装:`name`(标签)、`required`,默认插槽放控件。 |
|
||||||
|
| **FieldRenderer** | 根据 `col.com` 渲染 Input、Select、Radio、`UploadSingle`、`DatePicker` 等;一般被 `editModal` 使用,也可在自定义表单中复用。 |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<fieldItem name="名称" :required="true">
|
||||||
|
<Input v-model="form.name" />
|
||||||
|
</fieldItem>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.8 卡片:`InfoCard`
|
||||||
|
|
||||||
|
左侧色块 + 图标,右侧自定义内容。
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `left` | 左侧宽度占比,默认 `36`(即 36%) |
|
||||||
|
| `color` | 左侧背景色 |
|
||||||
|
| `icon` / `iconSize` | 图标名与尺寸(走 `CommonIcon`) |
|
||||||
|
| `shadow` | 是否 Card 阴影 |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<InfoCard icon="md-person" color="#19be6b">
|
||||||
|
<p>自定义统计文案</p>
|
||||||
|
</InfoCard>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.9 分割面板:`SplitPane`
|
||||||
|
|
||||||
|
可拖拽调整左右或上下比例。
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `value` | `0~1` 或带 `px` 的字符串,初始分割位置 |
|
||||||
|
| `mode` | `horizontal`(左右)或 `vertical`(上下) |
|
||||||
|
| `min` / `max` | 拖拽极限,默认约 `40px` |
|
||||||
|
|
||||||
|
| 插槽 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `left` / `right` 或 `top` / `bottom` | 两侧内容 |
|
||||||
|
| `trigger` | 自定义拖拽条 |
|
||||||
|
|
||||||
|
| 事件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `on-move-start` / `on-moving` / `on-move-end` | 拖拽过程;`on-moving` 事件对象上可带 `atMin`、`atMax` |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<SplitPane v-model="split" mode="horizontal">
|
||||||
|
<div slot="left">左</div>
|
||||||
|
<div slot="right">右</div>
|
||||||
|
</SplitPane>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.10 侧滑层:`FloatPanel`
|
||||||
|
|
||||||
|
从一侧滑出的面板,带返回/关闭。
|
||||||
|
|
||||||
|
| Props | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `title` | 标题 |
|
||||||
|
| `width` / `height` | 尺寸,支持数字或带单位字符串 |
|
||||||
|
| `position` | `left` / `right` / `top` / `bottom` / `center` |
|
||||||
|
| `showBack` / `backText` | 返回按钮与文案 |
|
||||||
|
| `showClose` | 右上角关闭 |
|
||||||
|
| `closeOnClickBackdrop` | 点击遮罩是否关闭 |
|
||||||
|
| `mask` | 是否遮罩 |
|
||||||
|
| `zIndex` | 层级 |
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `show(callback)` | 显示;可选回调 |
|
||||||
|
| `hide()` | 隐藏 |
|
||||||
|
|
||||||
|
| 插槽 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 默认 | 主体内容 |
|
||||||
|
| `header-right` | 标题栏右侧 |
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<FloatPanel ref="panel" title="详情" position="right" :width="480">
|
||||||
|
<p>内容</p>
|
||||||
|
</FloatPanel>
|
||||||
|
<!-- this.$refs.panel.show() -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.11 其他全局组件
|
||||||
|
|
||||||
|
| 组件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **TextArea** | 多行输入,`value` + `input` 事件,封装 iView `Input type="textarea"`。 |
|
||||||
|
| **CommonIcon** | `type`:普通 `Icon` 名;自定义图标以 `_` 开头配合 `md-icons`。`size`、`color` 可选。 |
|
||||||
|
| **LoadFlower** | 全屏 loading 占位(与 `http` 的 `showLoad` 通过 DOM `id` 联动),一般随 `Main` 挂载,无需单独使用。 |
|
||||||
|
|
||||||
|
### 12.12 未全局注册、按需引用的组件
|
||||||
|
|
||||||
|
以下在 `src/components` 下,需在页面中 **import** 后注册或局部引用:
|
||||||
|
|
||||||
|
| 路径/名称 | 说明 |
|
||||||
|
|-----------|------|
|
||||||
|
| `login-form` | 登录表单:`userNameRules`、`passwordRules`;校验通过 `@on-success-valid` 抛出 `{ userName, password }`。 |
|
||||||
|
| `date-picker/index.vue` | 对 `DatePicker` 的 `v-model` 封装,`change`/`input`。 |
|
||||||
|
| `switch/index.vue` | 对 `Switch` 的 `v-model` 封装。 |
|
||||||
|
| `cron-input` | Cron 表达式下拉选择。 |
|
||||||
|
| `main/pageHead.vue` | 页面顶栏条:当 `$route.meta.type === '功能'` 时显示返回;默认插槽放按钮。 |
|
||||||
|
| `markdown`、`cropper` 等 | 见各目录 `index.js` / 入口组件。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 调试
|
||||||
|
|
||||||
|
- 浏览器中:`window.framework`、`window.rootVue`(根实例,在 `createApp` 创建后设置)。
|
||||||
|
- 需要未压缩源码调试时,可在业务工程里直接引用框架 **`src/index.js`**(需配置好 alias、loader 与 peer 依赖),或使用浏览器「在源映射中黑盒脚本」等能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Demo 项目
|
||||||
|
|
||||||
|
`demo/` 为示例:请先在**仓库根目录**执行 `npm run build` 生成 `dist/admin-framework.js`,再在 `demo` 目录安装依赖并执行 `npm run dev`。`demo/src/main.js` 通过相对路径引用上级 `dist` 中的 UMD 包。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
以上为框架对外使用方式的说明;内部实现以 `src/` 源码为准。若升级版本,请同时核对 peer 依赖与本文档中的 API 是否变更。
|
||||||
|
|||||||
781
_doc/使用说明.md
781
_doc/使用说明.md
@@ -1,781 +0,0 @@
|
|||||||
# Admin Framework 使用说明
|
|
||||||
|
|
||||||
一个基于 Vue2 的通用后台管理系统框架,包含完整的系统功能、登录、路由管理、布局等核心功能。
|
|
||||||
|
|
||||||
## 📦 框架特性
|
|
||||||
|
|
||||||
### ✨ 核心功能
|
|
||||||
- ✅ **简化的 API** - 只需调用 `createApp()` 即可完成所有初始化
|
|
||||||
- ✅ **模块化设计** - 组件、路由、状态管理等功能按模块组织
|
|
||||||
- ✅ **完整的系统管理页面** - 用户、角色、菜单、日志等管理
|
|
||||||
- ✅ **登录和权限管理** - 完整的登录流程和权限控制
|
|
||||||
- ✅ **动态路由管理** - 基于权限菜单的动态路由生成
|
|
||||||
- ✅ **Vuex 状态管理** - 用户、应用状态管理
|
|
||||||
- ✅ **全局组件库** - Tables、Editor、Upload、TreeGrid、FieldRenderer、FloatPanel 等
|
|
||||||
- ✅ **工具库** - HTTP、日期、Token、Cookie 等工具
|
|
||||||
- ✅ **内置样式** - base.less、animate.css、iconfont 等
|
|
||||||
- ✅ **响应式布局** - 支持移动端适配
|
|
||||||
|
|
||||||
### 🎯 内置页面组件
|
|
||||||
- **主页组件** (`HomePage`) - 欢迎页面,显示系统标题
|
|
||||||
- **系统管理页面** (`SysUser`, `SysRole`, `SysLog`, `SysParamSetup`)
|
|
||||||
- **高级管理页面** (`SysMenu`, `SysControl`, `SysTitle`)
|
|
||||||
- **登录页面** (`LoginPage`)
|
|
||||||
- **错误页面** (`Page401`, `Page404`, `Page500`)
|
|
||||||
|
|
||||||
### 🛠️ 内置工具
|
|
||||||
- **HTTP 工具** (`http`) - 封装了 axios,支持拦截器、文件上传下载
|
|
||||||
- **UI 工具** (`uiTool`) - 删除确认、树形转换、响应式设置、文件下载
|
|
||||||
- **通用工具** (`tools`) - 日期格式化、UUID 生成、Cookie 操作、深拷贝等
|
|
||||||
- **文件下载** - 支持 CSV 等格式的文件下载,自动处理换行符
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 方式一:使用 Demo 项目(推荐)
|
|
||||||
|
|
||||||
我们提供了一个完整的 demo 项目,可以直接运行查看效果:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 进入 demo 项目
|
|
||||||
cd demo
|
|
||||||
|
|
||||||
# 2. 安装依赖
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 3. 启动开发服务器
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
浏览器会自动打开 `http://localhost:8080`,查看:
|
|
||||||
- `/login` - 登录页面
|
|
||||||
- `/home` - 主页
|
|
||||||
- `/system/user` - 用户管理
|
|
||||||
- `/ball/games` - 业务示例页面
|
|
||||||
|
|
||||||
### 方式二:构建框架
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 安装依赖
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 2. 构建框架
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# 3. 产物在 dist/admin-framework.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 极简使用方式
|
|
||||||
|
|
||||||
### 只需 3 步即可完成集成!
|
|
||||||
|
|
||||||
#### 1. 引入框架
|
|
||||||
```javascript
|
|
||||||
import AdminFramework from './admin-framework.js'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 创建应用
|
|
||||||
```javascript
|
|
||||||
const app = AdminFramework.createApp({
|
|
||||||
title: '我的管理系统',
|
|
||||||
apiUrl: 'http://localhost:9098/admin_api/',
|
|
||||||
componentMap: {
|
|
||||||
'business/product': ProductComponent,
|
|
||||||
'business/order': OrderComponent
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. 挂载应用
|
|
||||||
```javascript
|
|
||||||
app.$mount('#app')
|
|
||||||
```
|
|
||||||
|
|
||||||
**就这么简单!** 框架会自动完成所有初始化工作。
|
|
||||||
|
|
||||||
## 📖 完整使用指南
|
|
||||||
|
|
||||||
### 1. 项目结构准备
|
|
||||||
|
|
||||||
```
|
|
||||||
your-project/
|
|
||||||
├── src/
|
|
||||||
│ ├── config/
|
|
||||||
│ │ └── index.js # 配置文件
|
|
||||||
│ ├── libs/
|
|
||||||
│ │ └── admin-framework.js # 框架文件
|
|
||||||
│ ├── views/
|
|
||||||
│ │ └── business/ # 业务页面
|
|
||||||
│ ├── api/
|
|
||||||
│ │ └── business/ # 业务 API
|
|
||||||
│ ├── App.vue
|
|
||||||
│ └── main.js
|
|
||||||
├── package.json
|
|
||||||
└── webpack.config.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install vue vue-router vuex view-design axios dayjs js-cookie vuex-persistedstate
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 创建配置文件
|
|
||||||
|
|
||||||
在 `src/config/index.js` 中:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
module.exports = {
|
|
||||||
title: '你的系统名称',
|
|
||||||
homeName: '首页',
|
|
||||||
apiUrl: 'http://localhost:9090/admin_api/',
|
|
||||||
uploadUrl: 'http://localhost:9090/admin_api/upload',
|
|
||||||
cookieExpires: 7,
|
|
||||||
uploadMaxLimitSize: 10,
|
|
||||||
oss: {
|
|
||||||
region: 'oss-cn-shanghai',
|
|
||||||
accessKeyId: 'your-key',
|
|
||||||
accessKeySecret: 'your-secret',
|
|
||||||
bucket: 'your-bucket',
|
|
||||||
url: 'http://your-bucket.oss-cn-shanghai.aliyuncs.com',
|
|
||||||
basePath: 'your-path/'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 创建 main.js(新版本 - 推荐)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import AdminFramework from './libs/admin-framework.js'
|
|
||||||
|
|
||||||
// 导入业务组件(根据权限菜单接口的 component 字段)
|
|
||||||
import GamesComponent from './views/ball/games.vue'
|
|
||||||
import PayOrdersComponent from './views/order/pay_orders.vue'
|
|
||||||
|
|
||||||
// 🎉 只需一行代码!框架自动完成所有初始化
|
|
||||||
const app = AdminFramework.createApp({
|
|
||||||
title: '我的管理系统',
|
|
||||||
apiUrl: 'http://localhost:9098/admin_api/',
|
|
||||||
componentMap: {
|
|
||||||
'ball/games': GamesComponent,
|
|
||||||
'order/pay_orders': PayOrdersComponent
|
|
||||||
// 添加更多业务组件...
|
|
||||||
},
|
|
||||||
onReady() {
|
|
||||||
console.log('应用已启动!')
|
|
||||||
// 应用启动完成后的回调
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 挂载应用
|
|
||||||
app.$mount('#app')
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### 5. 创建 App.vue
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div id="app">
|
|
||||||
<router-view/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'App'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 API 使用指南
|
|
||||||
|
|
||||||
### 框架实例方法
|
|
||||||
|
|
||||||
#### createApp(config) - 推荐使用
|
|
||||||
创建应用实例(新版本 API)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const app = AdminFramework.createApp({
|
|
||||||
title: '我的管理系统', // 应用标题(必需)
|
|
||||||
apiUrl: 'http://localhost:9098/admin_api/', // API 基础地址(必需)
|
|
||||||
uploadUrl: 'http://localhost:9098/admin_api/upload', // 上传地址(可选,默认为 apiUrl + 'upload')
|
|
||||||
componentMap: { // 业务组件映射(可选)
|
|
||||||
'business/product': ProductComponent,
|
|
||||||
'business/order': OrderComponent
|
|
||||||
},
|
|
||||||
onReady() { // 应用启动完成回调(可选)
|
|
||||||
console.log('应用已启动!')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
### 工具库使用
|
|
||||||
|
|
||||||
#### HTTP 工具
|
|
||||||
```javascript
|
|
||||||
// 在组件中使用
|
|
||||||
export default {
|
|
||||||
async mounted() {
|
|
||||||
// GET 请求
|
|
||||||
const res = await this.$http.get('/api/users', { page: 1 })
|
|
||||||
|
|
||||||
// POST 请求
|
|
||||||
const result = await this.$http.post('/api/users', { name: 'test' })
|
|
||||||
|
|
||||||
// 文件导出
|
|
||||||
await this.$http.fileExport('/api/export', { type: 'excel' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在非 Vue 组件中使用
|
|
||||||
import AdminFramework from './libs/admin-framework.js'
|
|
||||||
const res = await AdminFramework.http.get('/api/users')
|
|
||||||
```
|
|
||||||
|
|
||||||
#### UI 工具
|
|
||||||
```javascript
|
|
||||||
// 在组件中使用
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
handleDelete() {
|
|
||||||
// 删除确认
|
|
||||||
this.$uiTool.delConfirm(() => {
|
|
||||||
// 执行删除逻辑
|
|
||||||
})
|
|
||||||
|
|
||||||
// 设置响应式字体
|
|
||||||
this.$uiTool.setRem()
|
|
||||||
|
|
||||||
// 树形转换
|
|
||||||
const treeData = this.$uiTool.transformTree(flatData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 功能工具
|
|
||||||
```javascript
|
|
||||||
// 在组件中使用
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
downloadFile() {
|
|
||||||
// 文件下载
|
|
||||||
this.$uiTool.downloadFile(response, 'filename.csv')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 通用工具
|
|
||||||
```javascript
|
|
||||||
// 在组件中使用
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
formatDate() {
|
|
||||||
// 日期格式化
|
|
||||||
return this.$tools.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
|
||||||
},
|
|
||||||
|
|
||||||
generateId() {
|
|
||||||
// UUID 生成
|
|
||||||
return this.$tools.generateUUID()
|
|
||||||
},
|
|
||||||
|
|
||||||
setCookie() {
|
|
||||||
// Cookie 操作
|
|
||||||
this.$tools.setCookie('name', 'value')
|
|
||||||
const value = this.$tools.getCookie('name')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Store 模块使用
|
|
||||||
|
|
||||||
#### user 模块
|
|
||||||
```javascript
|
|
||||||
// 登录
|
|
||||||
await this.$store.dispatch('user/handleLogin', {
|
|
||||||
userFrom: { username, password },
|
|
||||||
Main: AdminFramework.Main,
|
|
||||||
ParentView: AdminFramework.ParentView,
|
|
||||||
Page404: AdminFramework.Page404
|
|
||||||
})
|
|
||||||
|
|
||||||
// 登出
|
|
||||||
this.$store.dispatch('user/handleLogOut')
|
|
||||||
|
|
||||||
// 设置权限菜单
|
|
||||||
this.$store.dispatch('user/setAuthorityMenus', {
|
|
||||||
Main: AdminFramework.Main,
|
|
||||||
ParentView: AdminFramework.ParentView,
|
|
||||||
Page404: AdminFramework.Page404
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
const userName = this.$store.getters['user/userName']
|
|
||||||
const token = this.$store.state.user.token
|
|
||||||
```
|
|
||||||
|
|
||||||
#### app 模块
|
|
||||||
```javascript
|
|
||||||
// 设置面包屑
|
|
||||||
this.$store.commit('app/setBreadCrumb', route)
|
|
||||||
|
|
||||||
// 获取系统标题
|
|
||||||
this.$store.dispatch('app/getSysTitle', {
|
|
||||||
defaultTitle: '系统名称',
|
|
||||||
defaultLogo: '/logo.png'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取系统配置
|
|
||||||
const sysFormModel = this.$store.getters['app/sysFormModel']
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🗂️ 组件映射配置
|
|
||||||
|
|
||||||
### 业务组件映射
|
|
||||||
|
|
||||||
当后端权限菜单接口返回组件路径时,需要配置映射表:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 1. 导入业务组件
|
|
||||||
import GamesComponent from './views/ball/games.vue'
|
|
||||||
import PayOrdersComponent from './views/order/pay_orders.vue'
|
|
||||||
|
|
||||||
// 2. 配置映射
|
|
||||||
const componentMap = {
|
|
||||||
'ball/games': GamesComponent,
|
|
||||||
'ball/games.vue': GamesComponent, // 支持带 .vue 后缀
|
|
||||||
'order/pay_orders': PayOrdersComponent,
|
|
||||||
'order/pay_orders.vue': PayOrdersComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 在 Vue.use 时传入
|
|
||||||
Vue.use(AdminFramework, {
|
|
||||||
config,
|
|
||||||
ViewUI,
|
|
||||||
VueRouter,
|
|
||||||
Vuex,
|
|
||||||
createPersistedState,
|
|
||||||
componentMap // 传入组件映射表
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 框架已自动映射的系统组件
|
|
||||||
|
|
||||||
以下组件**无需配置**,框架已自动映射:
|
|
||||||
- ✅ `home/index` - 主页
|
|
||||||
- ✅ `system/sys_user` - 用户管理
|
|
||||||
- ✅ `system/sys_role` - 角色管理
|
|
||||||
- ✅ `system/sys_log` - 日志管理
|
|
||||||
- ✅ `system/sys_param_setup` - 参数设置
|
|
||||||
- ✅ `system/sys_menu` - 菜单管理
|
|
||||||
- ✅ `system/sys_control` - 控制器管理
|
|
||||||
- ✅ `system/sys_title` - 系统标题设置
|
|
||||||
|
|
||||||
## 🌐 全局访问
|
|
||||||
|
|
||||||
### window.framework
|
|
||||||
|
|
||||||
框架实例会自动暴露到全局,可以在任何地方访问:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 在非 Vue 组件中使用
|
|
||||||
const http = window.framework.http
|
|
||||||
const uiTool = window.framework.uiTool
|
|
||||||
const config = window.framework.config
|
|
||||||
|
|
||||||
// HTTP 请求
|
|
||||||
const res = await window.framework.http.get('/api/users')
|
|
||||||
|
|
||||||
// UI 工具
|
|
||||||
window.framework.uiTool.delConfirm(() => {
|
|
||||||
// 删除逻辑
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Vue 原型方法
|
|
||||||
|
|
||||||
在 Vue 组件中可以直接使用:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
async loadData() {
|
|
||||||
// 直接使用 this.$xxx
|
|
||||||
const res = await this.$http.get('/api/users')
|
|
||||||
this.$uiTool.delConfirm(() => {})
|
|
||||||
this.$tools.formatDate(new Date())
|
|
||||||
this.$uiTool.downloadFile(response, 'file.csv')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 文件下载功能
|
|
||||||
|
|
||||||
### 使用 downloadFile 方法
|
|
||||||
|
|
||||||
框架提供了便捷的文件下载功能,支持 CSV 等格式:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 在 Vue 组件中使用
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
// 导出数据
|
|
||||||
exportData() {
|
|
||||||
// 调用 API 获取数据
|
|
||||||
this.$http.fileExport('/api/export', params).then(res => {
|
|
||||||
// 使用 downloadFile 下载
|
|
||||||
this.$uiTool.downloadFile(res, '数据导出.csv')
|
|
||||||
this.$Message.success('导出成功!')
|
|
||||||
}).catch(error => {
|
|
||||||
this.$Message.error('导出失败:' + error.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 支持的数据格式
|
|
||||||
|
|
||||||
- **CSV 格式**:自动处理换行符,保持表格格式
|
|
||||||
- **Blob 对象**:支持二进制文件下载
|
|
||||||
- **文本数据**:支持纯文本文件下载
|
|
||||||
|
|
||||||
### 自动处理特性
|
|
||||||
|
|
||||||
- ✅ **换行符保持**:CSV 文件的换行符会被正确保持
|
|
||||||
- ✅ **文件名处理**:自动清理文件名中的特殊字符
|
|
||||||
- ✅ **浏览器兼容**:支持所有现代浏览器
|
|
||||||
- ✅ **内存管理**:自动清理临时 URL 对象
|
|
||||||
|
|
||||||
## 🎨 全局组件使用
|
|
||||||
|
|
||||||
### FloatPanel - 浮动面板组件
|
|
||||||
|
|
||||||
`FloatPanel` 是一个浮动在父窗体上的面板组件,类似于抽屉效果,常用于详情展示、表单编辑等场景。
|
|
||||||
|
|
||||||
**基本使用:**
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Button @click="showPanel">打开浮动面板</Button>
|
|
||||||
|
|
||||||
<FloatPanel
|
|
||||||
ref="floatPanel"
|
|
||||||
title="详情面板"
|
|
||||||
position="right"
|
|
||||||
:show-back="true"
|
|
||||||
back-text="返回"
|
|
||||||
@back="handleBack"
|
|
||||||
>
|
|
||||||
<div>这里是面板内容</div>
|
|
||||||
</FloatPanel>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
showPanel() {
|
|
||||||
// 通过 ref 调用 show 方法显示面板
|
|
||||||
this.$refs.floatPanel.show()
|
|
||||||
},
|
|
||||||
hidePanel() {
|
|
||||||
// 通过 ref 调用 hide 方法隐藏面板
|
|
||||||
this.$refs.floatPanel.hide()
|
|
||||||
},
|
|
||||||
handleBack() {
|
|
||||||
console.log('返回按钮被点击')
|
|
||||||
this.hidePanel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
**属性说明:**
|
|
||||||
|
|
||||||
| 属性 | 类型 | 默认值 | 说明 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `title` | String | `''` | 面板标题 |
|
|
||||||
| `width` | String/Number | `'100%'` | 面板宽度(字符串或数字),默认占满父容器 |
|
|
||||||
| `height` | String/Number | `'100%'` | 面板高度(字符串或数字),默认占满父容器 |
|
|
||||||
| `position` | String | `'right'` | 面板位置:`left`、`right`、`top`、`bottom`、`center` |
|
|
||||||
| `showBack` | Boolean | `true` | 是否显示返回按钮 |
|
|
||||||
| `showClose` | Boolean | `false` | 是否显示关闭按钮 |
|
|
||||||
| `backText` | String | `'返回'` | 返回按钮文字 |
|
|
||||||
| `closeOnClickBackdrop` | Boolean | `false` | 点击遮罩是否关闭 |
|
|
||||||
| `mask` | Boolean | `false` | 是否显示遮罩(默认不显示) |
|
|
||||||
| `zIndex` | Number | `1000` | 层级 |
|
|
||||||
|
|
||||||
**方法:**
|
|
||||||
|
|
||||||
| 方法 | 说明 | 参数 |
|
|
||||||
|------|------|------|
|
|
||||||
| `show(callback)` | 显示面板 | `callback`: 可选的回调函数 |
|
|
||||||
| `hide()` | 隐藏面板 | - |
|
|
||||||
|
|
||||||
**事件:**
|
|
||||||
|
|
||||||
| 事件 | 说明 | 参数 |
|
|
||||||
|------|------|------|
|
|
||||||
| `back` | 点击返回按钮时触发 | - |
|
|
||||||
|
|
||||||
**插槽:**
|
|
||||||
|
|
||||||
| 插槽 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `default` | 面板主体内容 |
|
|
||||||
| `header-right` | 头部右侧内容(可用于添加自定义按钮) |
|
|
||||||
|
|
||||||
**位置说明:**
|
|
||||||
|
|
||||||
- `left`: 从左侧滑入
|
|
||||||
- `right`: 从右侧滑入(默认)
|
|
||||||
- `top`: 从顶部滑入
|
|
||||||
- `bottom`: 从底部滑入
|
|
||||||
- `center`: 居中显示,带缩放动画
|
|
||||||
|
|
||||||
**完整示例:**
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Button @click="openDetailPanel">查看详情</Button>
|
|
||||||
|
|
||||||
<FloatPanel
|
|
||||||
ref="detailPanel"
|
|
||||||
title="用户详情"
|
|
||||||
position="right"
|
|
||||||
:show-back="true"
|
|
||||||
:show-close="true"
|
|
||||||
back-text="返回"
|
|
||||||
@back="handleBack"
|
|
||||||
>
|
|
||||||
<template #header-right>
|
|
||||||
<Button type="primary" @click="handleSave">保存</Button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="detail-content">
|
|
||||||
<Form :model="formData" :label-width="100">
|
|
||||||
<FormItem label="用户名">
|
|
||||||
<Input v-model="formData.username" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label="邮箱">
|
|
||||||
<Input v-model="formData.email" />
|
|
||||||
</FormItem>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</FloatPanel>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
formData: {
|
|
||||||
username: '',
|
|
||||||
email: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openDetailPanel() {
|
|
||||||
this.$refs.detailPanel.show()
|
|
||||||
},
|
|
||||||
handleBack() {
|
|
||||||
this.$refs.detailPanel.hide()
|
|
||||||
},
|
|
||||||
handleSave() {
|
|
||||||
// 保存逻辑
|
|
||||||
console.log('保存数据', this.formData)
|
|
||||||
this.$Message.success('保存成功')
|
|
||||||
this.$refs.detailPanel.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
**特性说明:**
|
|
||||||
|
|
||||||
- ✅ 基于父元素定位,不会遮挡菜单
|
|
||||||
- ✅ 宽度和高度默认 100%,占满父容器
|
|
||||||
- ✅ 无遮罩背景,完全浮在父页面上
|
|
||||||
- ✅ 路由切换或组件销毁时自动关闭
|
|
||||||
- ✅ 支持多种位置和动画效果
|
|
||||||
- ✅ 支持自定义头部右侧内容
|
|
||||||
|
|
||||||
## 📝 业务开发示例
|
|
||||||
|
|
||||||
### 创建业务页面
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- src/views/business/product.vue -->
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h1>产品管理</h1>
|
|
||||||
<Button @click="loadData">加载数据</Button>
|
|
||||||
<Tables :columns="columns" :data="list" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
list: [],
|
|
||||||
columns: [
|
|
||||||
{ title: 'ID', key: 'id' },
|
|
||||||
{ title: '名称', key: 'name' },
|
|
||||||
{ title: '价格', key: 'price' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
await this.loadData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async loadData() {
|
|
||||||
// 使用框架提供的 http 工具
|
|
||||||
const res = await this.$http.get('/product/list', { page: 1 })
|
|
||||||
this.list = res.data
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleDelete(id) {
|
|
||||||
// 使用框架提供的 UI 工具
|
|
||||||
this.$uiTool.delConfirm(async () => {
|
|
||||||
await this.$http.post('/product/delete', { id })
|
|
||||||
this.$Message.success('删除成功')
|
|
||||||
await this.loadData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 创建业务 API
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// src/api/business/productServer.js
|
|
||||||
// 注意:不需要 import http,直接使用 http
|
|
||||||
|
|
||||||
class ProductServer {
|
|
||||||
async getList(params) {
|
|
||||||
return await http.get('/product/list', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(data) {
|
|
||||||
return await http.post('/product/save', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(id) {
|
|
||||||
return await http.post('/product/delete', { id })
|
|
||||||
}
|
|
||||||
|
|
||||||
async exportCsv(params) {
|
|
||||||
return await http.fileExport('/product/export', params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new ProductServer()
|
|
||||||
```
|
|
||||||
|
|
||||||
## ❓ 常见问题
|
|
||||||
|
|
||||||
### Q1: 打包后文件太大怎么办?
|
|
||||||
|
|
||||||
A: 框架已经将 Vue、VueRouter、Vuex、ViewUI、Axios 设置为外部依赖,不会打包进去。确保在项目中单独安装这些依赖。
|
|
||||||
|
|
||||||
### Q2: 如何只使用部分功能?
|
|
||||||
|
|
||||||
A: 可以按需导入:
|
|
||||||
```javascript
|
|
||||||
import { http, uiTool, tools } from './libs/admin-framework.js'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q3: 权限菜单中的业务页面显示 404 怎么办?
|
|
||||||
|
|
||||||
A: 需要配置组件映射表:
|
|
||||||
```javascript
|
|
||||||
Vue.use(AdminFramework, {
|
|
||||||
// ... 其他配置
|
|
||||||
componentMap: {
|
|
||||||
'ball/games': GamesComponent,
|
|
||||||
'order/pay_orders': PayOrdersComponent
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q4: 如何自定义配置?
|
|
||||||
|
|
||||||
A: 修改 `config/index.js` 文件:
|
|
||||||
```javascript
|
|
||||||
module.exports = {
|
|
||||||
title: '你的系统名称',
|
|
||||||
apiUrl: 'http://your-api-url/',
|
|
||||||
// ... 其他配置
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q5: 如何使用登录功能?
|
|
||||||
|
|
||||||
A: 在组件中:
|
|
||||||
```javascript
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
async login() {
|
|
||||||
await this.$store.dispatch('user/handleLogin', {
|
|
||||||
userFrom: { username: 'admin', password: '123456' },
|
|
||||||
Main: AdminFramework.Main,
|
|
||||||
ParentView: AdminFramework.ParentView,
|
|
||||||
Page404: AdminFramework.Page404
|
|
||||||
})
|
|
||||||
this.$router.push({ name: 'home' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q6: 需要单独引入样式文件吗?
|
|
||||||
|
|
||||||
A: **不需要!** 框架已内置所有样式:
|
|
||||||
- ✅ `base.less` - 基础样式
|
|
||||||
- ✅ `animate.css` - 动画样式
|
|
||||||
- ✅ `ivewExpand.less` - ViewUI 扩展样式
|
|
||||||
- ✅ `iconfont.css` - 字体图标样式
|
|
||||||
|
|
||||||
只需引入框架即可:
|
|
||||||
```javascript
|
|
||||||
import AdminFramework from './libs/admin-framework.js'
|
|
||||||
Vue.use(AdminFramework, { ... })
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📦 技术栈
|
|
||||||
|
|
||||||
- Vue 2.6+
|
|
||||||
- Vue Router 3.x
|
|
||||||
- Vuex 3.x
|
|
||||||
- View Design (iView) 4.x
|
|
||||||
- Axios
|
|
||||||
- Less
|
|
||||||
- Webpack 5
|
|
||||||
|
|
||||||
## 📄 许可证
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
## 👨💻 作者
|
|
||||||
|
|
||||||
light
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**祝开发愉快!** 🎉
|
|
||||||
|
|
||||||
如有问题,请查看 Demo 项目示例或联系开发团队。
|
|
||||||
1582
_doc/完整使用文档.md
1582
_doc/完整使用文档.md
File diff suppressed because it is too large
Load Diff
325
_doc/快速开始.md
325
_doc/快速开始.md
@@ -1,325 +0,0 @@
|
|||||||
# AdminFramework 快速开始
|
|
||||||
|
|
||||||
## 🚀 3 分钟上手
|
|
||||||
|
|
||||||
### 第一步:引入框架
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import AdminFramework from './admin-framework.js'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第二步:创建应用
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const app = AdminFramework.createApp({
|
|
||||||
title: '我的管理系统',
|
|
||||||
apiUrl: 'http://localhost:9098/admin_api/',
|
|
||||||
componentMap: {
|
|
||||||
'business/product': ProductComponent,
|
|
||||||
'business/order': OrderComponent
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第三步:挂载应用1
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
app.$mount('#app')
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✨ 就这么简单!
|
|
||||||
|
|
||||||
**完整代码只需 12 行:**
|
|
||||||
|
|
||||||
## 📁 文件下载功能
|
|
||||||
|
|
||||||
框架内置了便捷的文件下载功能:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 在组件中使用
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
exportData() {
|
|
||||||
// 调用 API 获取数据
|
|
||||||
this.$http.fileExport('/api/export', params).then(res => {
|
|
||||||
// 下载文件(自动处理换行符)
|
|
||||||
this.$uiTool.downloadFile(res, '数据导出.csv')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**特性:**
|
|
||||||
- ✅ 自动处理 CSV 换行符
|
|
||||||
- ✅ 支持多种文件格式
|
|
||||||
- ✅ 浏览器兼容性好
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// main.js
|
|
||||||
import AdminFramework from './admin-framework.js'
|
|
||||||
import ProductComponent from './views/business/product.vue'
|
|
||||||
import OrderComponent from './views/business/order.vue'
|
|
||||||
|
|
||||||
const app = AdminFramework.createApp({
|
|
||||||
title: '我的管理系统',
|
|
||||||
apiUrl: 'http://localhost:9098/admin_api/',
|
|
||||||
componentMap: {
|
|
||||||
'business/product': ProductComponent,
|
|
||||||
'business/order': OrderComponent
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.$mount('#app')
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 组件映射表配置
|
|
||||||
|
|
||||||
`componentMap` 用于定义业务页面的动态路由。
|
|
||||||
|
|
||||||
**重要提示:** 只需传递不带 `.vue` 后缀的路径,框架会自动处理带后缀和不带后缀的两种情况!
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 直接在 createApp 中配置
|
|
||||||
const app = AdminFramework.createApp({
|
|
||||||
title: '我的管理系统',
|
|
||||||
apiUrl: 'http://localhost:9098/admin_api/',
|
|
||||||
componentMap: {
|
|
||||||
// ✅ 正确:只传递不带 .vue 的路径
|
|
||||||
'business/product': ProductComponent,
|
|
||||||
'business/order': OrderComponent,
|
|
||||||
'system/user': UserComponent,
|
|
||||||
|
|
||||||
// ❌ 错误:不要同时传递带和不带 .vue 的路径(多余)
|
|
||||||
// 'business/product.vue': ProductComponent, // 框架会自动添加
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
**说明:** 框架内部会自动为每个组件创建两个映射:
|
|
||||||
- `'business/product'` → ProductComponent
|
|
||||||
- `'business/product.vue'` → ProductComponent(自动添加)
|
|
||||||
|
|
||||||
所以后台菜单配置 `business/product` 或 `business/product.vue` 都可以正常工作!
|
|
||||||
|
|
||||||
## ⚙️ 配置说明
|
|
||||||
|
|
||||||
### 必填参数
|
|
||||||
|
|
||||||
| 参数 | 说明 | 示例 |
|
|
||||||
|------|------|------|
|
|
||||||
| `title` | 应用标题 | `'我的管理系统'` |
|
|
||||||
| `apiUrl` | API 基础地址 | `'http://localhost:9098/admin_api/'` |
|
|
||||||
| `componentMap` | 组件映射表 | 见上方示例 |
|
|
||||||
|
|
||||||
### 可选参数
|
|
||||||
|
|
||||||
| 参数 | 说明 | 默认值 |
|
|
||||||
|------|------|--------|
|
|
||||||
| `uploadUrl` | 上传文件地址 | `apiUrl + 'upload'` |
|
|
||||||
| `onReady` | 应用启动回调 | - |
|
|
||||||
|
|
||||||
## 🎯 完整示例
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import AdminFramework from './admin-framework.js'
|
|
||||||
import componentMap from './router/component-map.js'
|
|
||||||
|
|
||||||
const app = AdminFramework.createApp({
|
|
||||||
// 必填配置
|
|
||||||
title: '我的管理系统',
|
|
||||||
apiUrl: 'http://localhost:9098/admin_api/',
|
|
||||||
componentMap: componentMap,
|
|
||||||
|
|
||||||
// 可选配置
|
|
||||||
uploadUrl: 'http://cdn.example.com/upload', // 可选,默认为 apiUrl + 'upload'
|
|
||||||
|
|
||||||
// 应用启动完成回调
|
|
||||||
onReady() {
|
|
||||||
console.log('应用已启动!')
|
|
||||||
console.log('当前登录用户:', this.$store.state.user)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.$mount('#app')
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📦 框架内置功能
|
|
||||||
|
|
||||||
使用 `createApp()` 后,框架会自动提供:
|
|
||||||
|
|
||||||
### 1. 内置页面
|
|
||||||
- ✅ 登录页面 (`/login`)
|
|
||||||
- ✅ 主页 (`/home`)
|
|
||||||
- ✅ 404 页面 (`/404`)
|
|
||||||
- ✅ 401 页面 (`/401`)
|
|
||||||
- ✅ 500 页面 (`/500`)
|
|
||||||
|
|
||||||
### 2. 系统管理页面
|
|
||||||
- ✅ 用户管理 (`/system/user`)
|
|
||||||
- ✅ 角色管理 (`/system/role`)
|
|
||||||
- ✅ 日志管理 (`/system/log`)
|
|
||||||
- ✅ 参数设置 (`/system/param`)
|
|
||||||
- ✅ 菜单管理 (`/system/menu`)
|
|
||||||
- ✅ 权限控制 (`/system/control`)
|
|
||||||
- ✅ 标题设置 (`/system/title`)
|
|
||||||
|
|
||||||
### 3. 全局组件
|
|
||||||
- ✅ `<Tables>` - 增强型表格
|
|
||||||
- ✅ `<UploadSingle>` - 单图上传
|
|
||||||
- ✅ `<UploadMultiple>` - 多图上传
|
|
||||||
- ✅ `<TreeGrid>` - 树形表格
|
|
||||||
- ✅ `<AsyncModal>` - 异步弹窗
|
|
||||||
- ✅ `<Editor>` - 富文本编辑器
|
|
||||||
- ✅ `<CommonIcon>` - 图标选择器
|
|
||||||
- ✅ `<FloatPanel>` - 浮动面板
|
|
||||||
|
|
||||||
### 4. 工具方法
|
|
||||||
|
|
||||||
在任何组件中可以使用:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// HTTP 请求
|
|
||||||
this.$http.get('/api/users')
|
|
||||||
this.$http.post('/api/users', data)
|
|
||||||
|
|
||||||
// UI 工具
|
|
||||||
this.$uiTool.showLoading()
|
|
||||||
this.$uiTool.hideLoading()
|
|
||||||
this.$uiTool.showSuccess('操作成功')
|
|
||||||
|
|
||||||
// 通用工具
|
|
||||||
this.$tools.formatDate(new Date())
|
|
||||||
this.$tools.deepClone(obj)
|
|
||||||
|
|
||||||
// 文件下载
|
|
||||||
this.$uiTool.downloadFile(response, 'filename.xlsx')
|
|
||||||
|
|
||||||
// 配置信息
|
|
||||||
this.$config.title
|
|
||||||
this.$config.apiUrl
|
|
||||||
this.$config.uploadUrl
|
|
||||||
|
|
||||||
// 状态管理
|
|
||||||
this.$store.state.user.token
|
|
||||||
this.$store.state.app.sysTitle
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎨 自定义页面
|
|
||||||
|
|
||||||
### 1. 创建页面组件
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- views/business/product-list.vue -->
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Tables :columns="columns" :data="list" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
list: [],
|
|
||||||
columns: [
|
|
||||||
{ title: '产品名称', key: 'name' },
|
|
||||||
{ title: '价格', key: 'price' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.loadData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async loadData() {
|
|
||||||
const res = await this.$http.get('/products')
|
|
||||||
this.list = res.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 添加到组件映射表
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// router/component-map.js
|
|
||||||
import ProductList from '../views/business/product-list.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
'business/product': ProductList // 路径对应后台菜单的 component 字段
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 在后台配置菜单
|
|
||||||
|
|
||||||
在"菜单管理"中添加菜单项:
|
|
||||||
- 菜单名称: `产品列表`
|
|
||||||
- 路由路径: `/business/product`
|
|
||||||
- 组件路径: `business/product`(对应 componentMap 的 key)
|
|
||||||
|
|
||||||
## 📝 常见问题
|
|
||||||
|
|
||||||
### Q1: uploadUrl 如何配置?
|
|
||||||
|
|
||||||
**A:** 默认情况下无需配置,框架会自动设置为 `apiUrl + 'upload'`。
|
|
||||||
|
|
||||||
如果需要自定义(如使用 CDN),可以手动传入:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
AdminFramework.createApp({
|
|
||||||
apiUrl: 'http://localhost:9098/admin_api/',
|
|
||||||
uploadUrl: 'http://cdn.example.com/upload' // 自定义上传地址
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q2: 如何访问框架实例?
|
|
||||||
|
|
||||||
**A:** 框架实例会自动暴露到全局:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 浏览器控制台
|
|
||||||
window.framework // 框架实例
|
|
||||||
window.app // Vue 实例
|
|
||||||
window.rootVue // Vue 实例(别名)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q3: 如何添加自定义 Vuex 模块?
|
|
||||||
|
|
||||||
**A:** 使用旧的 `install()` 方式:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import AdminFramework from './admin-framework.js'
|
|
||||||
|
|
||||||
AdminFramework.install(Vue, {
|
|
||||||
config: { ... },
|
|
||||||
ViewUI,
|
|
||||||
VueRouter,
|
|
||||||
Vuex,
|
|
||||||
customModules: {
|
|
||||||
myModule: myModuleConfig
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q4: 框架文件太大怎么办?
|
|
||||||
|
|
||||||
**A:** 新版框架(3.6 MB)内置了所有依赖,使用更方便。如果需要减小文件大小,可以使用旧的 `install()` 方式,手动引入依赖。
|
|
||||||
|
|
||||||
## 🔗 相关文档
|
|
||||||
|
|
||||||
- [简化使用说明.md](./简化使用说明.md) - 详细的使用说明和对比
|
|
||||||
- [README.md](./README.md) - 项目介绍和特性
|
|
||||||
- [demo/src/main.js](./demo/src/main.js) - 完整示例代码
|
|
||||||
|
|
||||||
## 💡 最佳实践
|
|
||||||
|
|
||||||
1. **推荐使用 `createApp()`** - 代码更简洁,减少 77% 的代码量
|
|
||||||
2. **组件映射表单独管理** - 方便维护和扩展
|
|
||||||
3. **利用内置组件** - 如 `<Tables>`、`<UploadSingle>` 等,提高开发效率
|
|
||||||
4. **使用 `onReady` 回调** - 在应用启动后执行初始化逻辑
|
|
||||||
|
|
||||||
## 🎉 开始开发吧!
|
|
||||||
|
|
||||||
现在你已经了解了 AdminFramework 的快速使用方法,开始构建你的管理系统吧!
|
|
||||||
@@ -240,13 +240,11 @@ const config = {
|
|||||||
|
|
||||||
## 开发建议
|
## 开发建议
|
||||||
|
|
||||||
1. **开发时使用 build:dev**
|
1. **框架产物**
|
||||||
- 生成 sourcemap,方便调试
|
- 在仓库根目录执行 `npm run build`,生成 `dist/admin-framework.js`(UMD、压缩)。
|
||||||
- 代码不压缩,易读
|
|
||||||
|
|
||||||
2. **生产时使用 build**
|
2. **调试**
|
||||||
- 代码压缩,体积小
|
- 需要跟源码时可在业务工程中直接引用框架 `src/index.js` 并配置 Webpack;或使用 `window.framework` / `window.rootVue`。
|
||||||
- 无 sourcemap,安全
|
|
||||||
|
|
||||||
3. **使用浏览器调试工具**
|
3. **使用浏览器调试工具**
|
||||||
```javascript
|
```javascript
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export default {
|
|||||||
this.gridOption.param.pageOption.total = res.data.count
|
this.gridOption.param.pageOption.total = res.data.count
|
||||||
},
|
},
|
||||||
async showAddWarp() {
|
async showAddWarp() {
|
||||||
this.$refs.editModal.addShow({ 'venue_type': 'indoor', 'surface_type': 'hard', 'court_count': '1', 'status': 'active', 'create_time': 'CURRENT_TIMESTAMP', 'updated_at': 'CURRENT_TIMESTAMP', }, async (newRow) => {
|
this.$refs.editModal.addShow({ 'venue_type': 'indoor', 'surface_type': 'hard', 'court_count': '1', 'status': 'active' }, async (newRow) => {
|
||||||
let res = await venuesServer.add(newRow)
|
let res = await venuesServer.add(newRow)
|
||||||
this.$Message.success('新增成功!')
|
this.$Message.success('新增成功!')
|
||||||
this.init()
|
this.init()
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export default {
|
|||||||
this.gridOption.param.pageOption.total = res.data.count
|
this.gridOption.param.pageOption.total = res.data.count
|
||||||
},
|
},
|
||||||
async showAddWarp() {
|
async showAddWarp() {
|
||||||
this.$refs.editModal.addShow({ 'is_subscribed': '0', 'last_login_time': 'CURRENT_TIMESTAMP', 'create_time': 'CURRENT_TIMESTAMP', 'updated_at': 'CURRENT_TIMESTAMP', }, async (newRow) => {
|
this.$refs.editModal.addShow({ 'is_subscribed': '0', 'last_login_time': 'CURRENT_TIMESTAMP' }, async (newRow) => {
|
||||||
let res = await wch_usersServer.add(newRow)
|
let res = await wch_usersServer.add(newRow)
|
||||||
this.$Message.success('新增成功!')
|
this.$Message.success('新增成功!')
|
||||||
this.init()
|
this.init()
|
||||||
|
|||||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -28,7 +28,6 @@
|
|||||||
"@babel/preset-env": "^7.12.0",
|
"@babel/preset-env": "^7.12.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"babel-loader": "^8.2.0",
|
"babel-loader": "^8.2.0",
|
||||||
"cross-env": "^10.1.0",
|
|
||||||
"css-loader": "^5.0.0",
|
"css-loader": "^5.0.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
@@ -1814,13 +1813,6 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@epic-web/invariant": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -2849,24 +2841,6 @@
|
|||||||
"@cropper/utils": "^2.0.1"
|
"@cropper/utils": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cross-env": {
|
|
||||||
"version": "10.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.1.0.tgz",
|
|
||||||
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@epic-web/invariant": "^1.0.0",
|
|
||||||
"cross-spawn": "^7.0.6"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"cross-env": "dist/bin/cross-env.js",
|
|
||||||
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|||||||
@@ -5,10 +5,8 @@
|
|||||||
"main": "dist/admin-framework.js",
|
"main": "dist/admin-framework.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"build:dev": "cross-env NODE_ENV=development webpack --mode production",
|
|
||||||
"dev": "webpack --mode development --watch",
|
"dev": "webpack --mode development --watch",
|
||||||
"serve": "npm run build && npx http-server -p 8080 -o /demo/index.html",
|
"serve": "npm run build && npx http-server -p 8080 -o /demo/index.html"
|
||||||
"serve:dev": "npm run build:dev && npx http-server -p 8080 -o /demo/index.html"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"admin",
|
"admin",
|
||||||
@@ -45,7 +43,6 @@
|
|||||||
"@babel/preset-env": "^7.12.0",
|
"@babel/preset-env": "^7.12.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"babel-loader": "^8.2.0",
|
"babel-loader": "^8.2.0",
|
||||||
"cross-env": "^10.1.0",
|
|
||||||
"css-loader": "^5.0.0",
|
"css-loader": "^5.0.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export { default as modelFieldServer } from './modelFieldServer'
|
|||||||
export { default as modelServer } from './modelServer'
|
export { default as modelServer } from './modelServer'
|
||||||
export { default as paramSetupServer } from './paramSetupServer'
|
export { default as paramSetupServer } from './paramSetupServer'
|
||||||
export { default as sysControlTypeServer } from './sysControlTypeServer'
|
export { default as sysControlTypeServer } from './sysControlTypeServer'
|
||||||
|
export { default as sysTenantServer } from './sysTenantServer'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
22
src/api/system/sysTenantServer.js
Normal file
22
src/api/system/sysTenantServer.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import http from '@/utils/http'
|
||||||
|
|
||||||
|
class SysTenantServer {
|
||||||
|
async list() {
|
||||||
|
return http.get('/sys_tenant/index', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(row) {
|
||||||
|
return http.post('/sys_tenant/add', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
async edit(row) {
|
||||||
|
return http.post('/sys_tenant/edit', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(row) {
|
||||||
|
return http.post('/sys_tenant/del', row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sysTenantServer = new SysTenantServer()
|
||||||
|
export default sysTenantServer
|
||||||
@@ -6,10 +6,6 @@
|
|||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ivu-select-dropdown-list {
|
|
||||||
height: 200px !important;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
@@ -114,6 +110,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-warp {
|
.table-warp {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.ivu-card-body {
|
.ivu-card-body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -190,20 +189,21 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自适应表格宽度 */
|
/* 与 View Design 一致:横向/纵向滚动由 .ivu-table-body 上的 overflow 类控制,避免 wrapper 与整表同高导致横向条在页面最底 */
|
||||||
.ivu-table-wrapper {
|
.ivu-table-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: auto;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ivu-table {
|
.ivu-table {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移除 table-layout: auto,使用 iView 默认的 fixed 布局以确保列对齐 */
|
/* 移除 table-layout: auto,使用 iView 默认的 fixed 布局以确保列对齐 */
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
} */
|
} */
|
||||||
|
|
||||||
.ivu-table-body {
|
.ivu-table-body {
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 按钮风格一致 */
|
/* 按钮风格一致 */
|
||||||
|
|||||||
@@ -5,14 +5,24 @@
|
|||||||
height: 64px;
|
height: 64px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.custom-content-con {
|
.header-breadcrumb {
|
||||||
height: 64px;
|
margin-left: 30px;
|
||||||
padding-right: 120px;
|
|
||||||
line-height: 64px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-right-slot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
padding-right: 10px;
|
||||||
|
line-height: 64px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="header-bar">
|
<div class="header-bar">
|
||||||
<sider-trigger :collapsed="collapsed" icon="md-menu" @on-change="handleCollpasedChange"></sider-trigger>
|
<sider-trigger :collapsed="collapsed" icon="md-menu" @on-change="handleCollpasedChange"></sider-trigger>
|
||||||
<custom-bread-crumb show-icon style="margin-left: 30px;" :list="breadCrumbList"></custom-bread-crumb>
|
<custom-bread-crumb class="header-breadcrumb" show-icon :list="breadCrumbList"></custom-bread-crumb>
|
||||||
<div class="custom-content-con">
|
<div class="header-right-slot">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
iconclass() {
|
iconclass() {
|
||||||
let curClass = 'terminal-icon ml10'
|
let curClass = 'terminal-icon'
|
||||||
// Terminal 功能暂时禁用
|
// Terminal 功能暂时禁用
|
||||||
// if (this.isServerRun) {
|
// if (this.isServerRun) {
|
||||||
// curClass += ' run'
|
// curClass += ' run'
|
||||||
@@ -39,6 +39,8 @@ export default {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.terminal-box {
|
.terminal-box {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
|
margin-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.terminal-icon {
|
.terminal-icon {
|
||||||
color: #c5c8ce;
|
color: #c5c8ce;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<label class="loginName-box">{{userName}}</label>
|
<label class="loginName-box">{{userName}}</label>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
|
|
||||||
<Avatar :src="typeof userAvator === 'string' ? userAvator : userIcon" />
|
<Avatar :src="avatarSrc" />
|
||||||
|
|
||||||
<Icon :size="18" type="md-arrow-dropdown"></Icon>
|
<Icon :size="18" type="md-arrow-dropdown"></Icon>
|
||||||
<DropdownMenu slot="list">
|
<DropdownMenu slot="list">
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import './user.less'
|
import './user.less'
|
||||||
import { mapMutations, mapActions } from 'vuex'
|
import { mapActions } from 'vuex'
|
||||||
|
|
||||||
const userIcon = require("@/assets/images/administrato.png").default
|
const userIcon = require("@/assets/images/administrato.png").default
|
||||||
|
|
||||||
@@ -39,6 +39,23 @@ export default {
|
|||||||
userIcon: userIcon
|
userIcon: userIcon
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
/** 接口无头像或空串时用本地默认图,避免 Avatar 空 src 显示灰色块 */
|
||||||
|
avatarSrc() {
|
||||||
|
const v = this.userAvator
|
||||||
|
if (v != null && typeof v === 'string' && v.trim() !== '') {
|
||||||
|
const s = v.trim()
|
||||||
|
if (/^https?:\/\//i.test(s)) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if (this.$http && typeof this.$http.ImgSrc === 'function') {
|
||||||
|
return this.$http.ImgSrc(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return this.userIcon
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('user', ['handleLogOut']),
|
...mapActions('user', ['handleLogOut']),
|
||||||
logout() {
|
logout() {
|
||||||
@@ -55,9 +72,9 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.user-avator-dropdown {
|
.user-avator-dropdown {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: 0px;
|
align-items: center;
|
||||||
right: 10px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loginName-box {
|
.loginName-box {
|
||||||
|
|||||||
@@ -16,8 +16,15 @@
|
|||||||
|
|
||||||
<Header class="header-con-main">
|
<Header class="header-con-main">
|
||||||
<headerBar class="header-con" @on-coll-change="collpasedChange" :collapsed="collapsed">
|
<headerBar class="header-con" @on-coll-change="collpasedChange" :collapsed="collapsed">
|
||||||
<Terminal></Terminal>
|
<div class="header-user-area">
|
||||||
<user :userName="userName" :user-avator="userAvator || ''" />
|
<Terminal></Terminal>
|
||||||
|
<span
|
||||||
|
v-if="currentTenant"
|
||||||
|
class="main-tenant-tag"
|
||||||
|
:title="'租户:' + currentTenant.name + '(' + currentTenant.code + ')'"
|
||||||
|
>{{ currentTenant.name }}</span>
|
||||||
|
<user :userName="userName" :user-avator="userAvator || ''" />
|
||||||
|
</div>
|
||||||
</headerBar>
|
</headerBar>
|
||||||
</Header>
|
</Header>
|
||||||
<Layout class="main-layout-content">
|
<Layout class="main-layout-content">
|
||||||
@@ -42,7 +49,7 @@ import User from './components/user'
|
|||||||
import ABackTop from './components/a-back-top'
|
import ABackTop from './components/a-back-top'
|
||||||
import Fullscreen from './components/fullscreen'
|
import Fullscreen from './components/fullscreen'
|
||||||
import Language from './components/language'
|
import Language from './components/language'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import headerBar from './components/header-bar'
|
import headerBar from './components/header-bar'
|
||||||
import loadFlower from '../load-flower/index'
|
import loadFlower from '../load-flower/index'
|
||||||
import Terminal from './components/terminal'
|
import Terminal from './components/terminal'
|
||||||
@@ -77,6 +84,7 @@ export default {
|
|||||||
userName: 'user/userName',
|
userName: 'user/userName',
|
||||||
userAvator: 'user/avatorImgPath'
|
userAvator: 'user/avatorImgPath'
|
||||||
}),
|
}),
|
||||||
|
...mapState('user', ['currentTenant']),
|
||||||
cacheList() {
|
cacheList() {
|
||||||
return ['ParentView']
|
return ['ParentView']
|
||||||
}
|
}
|
||||||
@@ -149,6 +157,30 @@ export default {
|
|||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-user-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-tenant-tag {
|
||||||
|
margin-right: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #2d8cf0;
|
||||||
|
background: rgba(45, 140, 240, 0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
max-width: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-brand {
|
.sidebar-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Switch
|
<!-- iView / View Design 全局注册为 i-switch,勿用 <Switch> 与本组件 name 形成自引用 -->
|
||||||
:value="value"
|
<i-switch
|
||||||
|
:value="innerValue"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@on-change="handleChange"
|
@on-change="handleChange"
|
||||||
/>
|
/>
|
||||||
@@ -8,12 +9,23 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'Switch',
|
name: 'FrameworkSwitch',
|
||||||
props: ['value'],
|
props: ['value'],
|
||||||
|
inheritAttrs: false,
|
||||||
|
computed: {
|
||||||
|
innerValue() {
|
||||||
|
const v = this.value
|
||||||
|
if (v === true || v === 1 || v === '1' || v === 'true') return true
|
||||||
|
if (v === false || v === 0 || v === '0' || v === 'false') return false
|
||||||
|
if (v === '' || v === null || v === undefined) return false
|
||||||
|
return Boolean(v)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleChange(checked) {
|
handleChange(checked) {
|
||||||
this.$emit('input', checked)
|
this.$emit('input', checked)
|
||||||
this.$emit('change', checked)
|
this.$emit('change', checked)
|
||||||
|
this.$emit('on-change', checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- 使用组件映射表来简化条件渲染 -->
|
<!-- 使用组件映射表来简化条件渲染 -->
|
||||||
<component
|
<component
|
||||||
:is="getComponentName(col.com)"
|
:is="getComponentName(col.com)"
|
||||||
:value="col.com === 'Radio' ? radioValue : value"
|
:value="inputBindValue"
|
||||||
v-bind="getComponentProps(col)"
|
v-bind="getComponentProps(col)"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:class="getComponentClass(col.com)"
|
:class="getComponentClass(col.com)"
|
||||||
@@ -58,20 +58,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import templateRender from './templateRender'
|
import templateRender from './templateRender'
|
||||||
|
import FrameworkSwitch from '../switch/index.vue'
|
||||||
// 导入框架中的图标配置
|
import { SELECT_ICON_OPTIONS } from './selectIconOptions'
|
||||||
let icons = []
|
|
||||||
try {
|
|
||||||
icons = require('../../config/icons.json') || []
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('未找到图标配置文件,图标选择功能将不可用', e)
|
|
||||||
icons = []
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldRenderer',
|
name: 'FieldRenderer',
|
||||||
components: {
|
components: {
|
||||||
templateRender
|
templateRender,
|
||||||
|
FrameworkSwitch
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
col: {
|
col: {
|
||||||
@@ -89,21 +83,25 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
icons: []
|
icons: SELECT_ICON_OPTIONS
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
// 直接返回原始值,不进行类型转换
|
|
||||||
radioValue() {
|
radioValue() {
|
||||||
return this.value
|
return this.value
|
||||||
}
|
},
|
||||||
},
|
inputBindValue() {
|
||||||
mounted() {
|
if (this.col.com === 'Radio') {
|
||||||
// 安全处理图标数据
|
return this.radioValue
|
||||||
if (Array.isArray(icons)) {
|
}
|
||||||
this.icons = icons.map((item) => item.trim ? item.trim() : item)
|
if (this.col.com === 'Switch') {
|
||||||
} else {
|
const v = this.value
|
||||||
this.icons = []
|
if (v === true || v === 1 || v === '1' || v === 'true') return true
|
||||||
|
if (v === false || v === 0 || v === '0' || v === 'false') return false
|
||||||
|
if (v === '' || v === null || v === undefined) return false
|
||||||
|
return Boolean(v)
|
||||||
|
}
|
||||||
|
return this.value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -112,13 +110,13 @@ export default {
|
|||||||
const componentMap = {
|
const componentMap = {
|
||||||
'Select': 'Select',
|
'Select': 'Select',
|
||||||
'Radio': 'RadioGroup',
|
'Radio': 'RadioGroup',
|
||||||
'SelectIcon': 'AutoComplete',
|
'SelectIcon': 'Select',
|
||||||
'UploadSingle': 'UploadSingle',
|
'UploadSingle': 'UploadSingle',
|
||||||
'TextArea': 'TextArea',
|
'TextArea': 'TextArea',
|
||||||
'Input': 'Input',
|
'Input': 'Input',
|
||||||
'DatePicker': 'DatePicker',
|
'DatePicker': 'DatePicker',
|
||||||
'TimePicker': 'TimePicker',
|
'TimePicker': 'TimePicker',
|
||||||
'Switch': 'Switch',
|
'Switch': 'FrameworkSwitch',
|
||||||
'Checkbox': 'Checkbox',
|
'Checkbox': 'Checkbox',
|
||||||
'Slider': 'Slider',
|
'Slider': 'Slider',
|
||||||
'Rate': 'Rate',
|
'Rate': 'Rate',
|
||||||
@@ -151,13 +149,18 @@ export default {
|
|||||||
delete baseProps.data_type
|
delete baseProps.data_type
|
||||||
delete baseProps.type
|
delete baseProps.type
|
||||||
delete baseProps.rowStyle
|
delete baseProps.rowStyle
|
||||||
|
delete baseProps.key
|
||||||
|
delete baseProps.title
|
||||||
|
delete baseProps.render
|
||||||
|
|
||||||
// 根据组件类型添加特定属性
|
// 根据组件类型添加特定属性
|
||||||
if (col.com === 'Select') {
|
if (col.com === 'Select') {
|
||||||
baseProps.filterable = true
|
baseProps.filterable = true
|
||||||
baseProps.clearable = true
|
baseProps.clearable = true
|
||||||
} else if (col.com === 'SelectIcon') {
|
} else if (col.com === 'SelectIcon') {
|
||||||
baseProps.icon = 'ios-search'
|
baseProps.filterable = true
|
||||||
|
baseProps.clearable = true
|
||||||
|
baseProps.placeholder = col.placeholder || '搜索或选择图标'
|
||||||
} else if (col.com === 'TextArea') {
|
} else if (col.com === 'TextArea') {
|
||||||
baseProps.rows = 4
|
baseProps.rows = 4
|
||||||
baseProps.placeholder = '请输入内容'
|
baseProps.placeholder = '请输入内容'
|
||||||
@@ -237,7 +240,10 @@ export default {
|
|||||||
'Input': 'width:100%;',
|
'Input': 'width:100%;',
|
||||||
'TextArea': 'width:100%;',
|
'TextArea': 'width:100%;',
|
||||||
'DatePicker': 'width:100%;',
|
'DatePicker': 'width:100%;',
|
||||||
'TimePicker': 'width:100%;'
|
'TimePicker': 'width:100%;',
|
||||||
|
'SelectIcon': 'width:100%;',
|
||||||
|
"inputNumber": "width:100%;",
|
||||||
|
|
||||||
}
|
}
|
||||||
return styleMap[componentType] || ''
|
return styleMap[componentType] || ''
|
||||||
},
|
},
|
||||||
@@ -252,14 +258,17 @@ export default {
|
|||||||
this.$emit('input', value)
|
this.$emit('input', value)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 处理变化事件
|
// 处理变化事件(i-switch 等只发 on-change,需同步 input 供 editModal 更新行)
|
||||||
handleChange(value) {
|
handleChange(value) {
|
||||||
// 如果接收到的是事件对象,提取值
|
|
||||||
if (value && typeof value === 'object' && value.target) {
|
if (value && typeof value === 'object' && value.target) {
|
||||||
value = value.target.value
|
value = value.target.value
|
||||||
}
|
}
|
||||||
// 直接传递值,不进行类型转换
|
const out =
|
||||||
this.$emit('change', value)
|
this.col.com === 'Switch'
|
||||||
|
? !!value
|
||||||
|
: value
|
||||||
|
this.$emit('input', out)
|
||||||
|
this.$emit('change', out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,24 @@
|
|||||||
<a v-if="isDown&&value&&value.length>0" class="link right-link" @click="downExecl">下载</a>
|
<a v-if="isDown&&value&&value.length>0" class="link right-link" @click="downExecl">下载</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table :class="tbClass" ref="tablesMain" border :data="insideTableData" :width='defaultWidth' :height='defaultHeight' :columns="insideColumns" v-bind="$attrs" @on-selection-change='onSelect'>
|
<div class="table-main">
|
||||||
<slot></slot>
|
<Table
|
||||||
<slot name="footer" slot="footer"></slot>
|
v-bind="$attrs"
|
||||||
<slot name="loading" slot="loading"></slot>
|
:class="tbClass"
|
||||||
</Table>
|
ref="tablesMain"
|
||||||
|
border
|
||||||
|
:data="insideTableData"
|
||||||
|
:width="defaultWidth"
|
||||||
|
:height="defaultHeight"
|
||||||
|
:max-height="effectiveTableMaxHeight"
|
||||||
|
:columns="insideColumns"
|
||||||
|
@on-selection-change="onSelect"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
<slot name="footer" slot="footer"></slot>
|
||||||
|
<slot name="loading" slot="loading"></slot>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -81,6 +94,9 @@ export default {
|
|||||||
},
|
},
|
||||||
// 动态计算的偏移量
|
// 动态计算的偏移量
|
||||||
calculatedOffset: null,
|
calculatedOffset: null,
|
||||||
|
/** 传给 iView Table 的 max-height(px),使表体在可视区内滚动,横向条始终在视口底部 */
|
||||||
|
bodyViewportMaxHeight: null,
|
||||||
|
_tableContentResizeObserver: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -145,6 +161,13 @@ export default {
|
|||||||
// 使用手动指定的偏移量
|
// 使用手动指定的偏移量
|
||||||
return `calc(100vh - ${this.maxHeightOffset}px)`
|
return `calc(100vh - ${this.maxHeightOffset}px)`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 未传 height 时由容器测量得到,避免宽表横向滚动条落在整表最底部 */
|
||||||
|
effectiveTableMaxHeight() {
|
||||||
|
if (this.height) return undefined
|
||||||
|
if (this.bodyViewportMaxHeight == null) return undefined
|
||||||
|
return this.bodyViewportMaxHeight
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -174,6 +197,7 @@ export default {
|
|||||||
// 触发表格重新渲染
|
// 触发表格重新渲染
|
||||||
this.$refs.tablesMain.$forceUpdate()
|
this.$refs.tablesMain.$forceUpdate()
|
||||||
}
|
}
|
||||||
|
this.syncTableBodyMaxHeight()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onChangePage(page) {
|
onChangePage(page) {
|
||||||
@@ -184,6 +208,43 @@ export default {
|
|||||||
this.$emit('on-select', selection, row)
|
this.$emit('on-select', selection, row)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
syncTableBodyMaxHeight() {
|
||||||
|
if (this.height) {
|
||||||
|
this.bodyViewportMaxHeight = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const wrap = this.$refs.table_content
|
||||||
|
if (!wrap) return
|
||||||
|
const titleEl = wrap.querySelector('.table-title')
|
||||||
|
const titleH = (this.title || this.isDown) && titleEl ? titleEl.offsetHeight : 0
|
||||||
|
const avail = wrap.clientHeight - titleH
|
||||||
|
const next = Math.max(160, Math.floor(avail))
|
||||||
|
if (this.bodyViewportMaxHeight !== next) {
|
||||||
|
this.bodyViewportMaxHeight = next
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const inst = this.$refs.tablesMain
|
||||||
|
if (inst && typeof inst.handleResize === 'function') {
|
||||||
|
inst.handleResize()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
setupTableContentResizeObserver() {
|
||||||
|
const el = this.$refs.table_content
|
||||||
|
if (!el || typeof ResizeObserver === 'undefined') return
|
||||||
|
if (this._tableContentResizeObserver) {
|
||||||
|
this._tableContentResizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
const ro = new ResizeObserver(() => {
|
||||||
|
this.syncTableBodyMaxHeight()
|
||||||
|
})
|
||||||
|
ro.observe(el)
|
||||||
|
this._tableContentResizeObserver = ro
|
||||||
|
},
|
||||||
|
|
||||||
// 动态计算表格容器上方所有元素的高度总和
|
// 动态计算表格容器上方所有元素的高度总和
|
||||||
calculateOffset() {
|
calculateOffset() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -203,6 +264,9 @@ export default {
|
|||||||
|
|
||||||
// 计算总偏移量
|
// 计算总偏移量
|
||||||
this.calculatedOffset = topOffset + pageBoxHeight + bottomPadding
|
this.calculatedOffset = topOffset + pageBoxHeight + bottomPadding
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.syncTableBodyMaxHeight()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -243,6 +307,7 @@ export default {
|
|||||||
|
|
||||||
// 动态计算偏移量
|
// 动态计算偏移量
|
||||||
this.calculateOffset()
|
this.calculateOffset()
|
||||||
|
this.setupTableContentResizeObserver()
|
||||||
|
|
||||||
// 如果数据或列配置为空,输出警告
|
// 如果数据或列配置为空,输出警告
|
||||||
if (!this.insideTableData || this.insideTableData.length === 0) {
|
if (!this.insideTableData || this.insideTableData.length === 0) {
|
||||||
@@ -255,21 +320,34 @@ export default {
|
|||||||
|
|
||||||
// 监听窗口大小变化,重新计算偏移量
|
// 监听窗口大小变化,重新计算偏移量
|
||||||
window.addEventListener('resize', this.calculateOffset)
|
window.addEventListener('resize', this.calculateOffset)
|
||||||
|
window.addEventListener('resize', this.syncTableBodyMaxHeight)
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
// 移除窗口大小监听
|
// 移除窗口大小监听
|
||||||
window.removeEventListener('resize', this.calculateOffset)
|
window.removeEventListener('resize', this.calculateOffset)
|
||||||
|
window.removeEventListener('resize', this.syncTableBodyMaxHeight)
|
||||||
|
if (this._tableContentResizeObserver) {
|
||||||
|
this._tableContentResizeObserver.disconnect()
|
||||||
|
this._tableContentResizeObserver = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.table-warp {
|
.table-warp {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.table-content {
|
.table-content {
|
||||||
overflow-y: auto;
|
/* 纵向由 Table 内部表体滚动;避免与外层双滚动导致横向条在整页最底 */
|
||||||
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.table-title {
|
.table-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -279,10 +357,17 @@ export default {
|
|||||||
padding: 0rem 0.15rem;
|
padding: 0rem 0.15rem;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
.right-link {
|
.right-link {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-main {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-box {
|
.page-box {
|
||||||
|
|||||||
306
src/components/tables/selectIconOptions.js
Normal file
306
src/components/tables/selectIconOptions.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
/**
|
||||||
|
* 菜单/表单「图标」下拉选项(与 View Design Icon 的 type 一致)
|
||||||
|
* 不依赖 icons.json,保证 UMD 打包后无需额外静态资源即可使用
|
||||||
|
*/
|
||||||
|
export const SELECT_ICON_OPTIONS = [
|
||||||
|
'md-add',
|
||||||
|
'md-add-circle',
|
||||||
|
'md-alarm',
|
||||||
|
'md-albums',
|
||||||
|
'md-alert',
|
||||||
|
'md-american-football',
|
||||||
|
'md-analytics',
|
||||||
|
'md-aperture',
|
||||||
|
'md-apps',
|
||||||
|
'md-appstore',
|
||||||
|
'md-archive',
|
||||||
|
'md-arrow-back',
|
||||||
|
'md-arrow-down',
|
||||||
|
'md-arrow-dropdown',
|
||||||
|
'md-arrow-dropdown-circle',
|
||||||
|
'md-arrow-dropleft',
|
||||||
|
'md-arrow-dropleft-circle',
|
||||||
|
'md-arrow-dropright',
|
||||||
|
'md-arrow-dropright-circle',
|
||||||
|
'md-arrow-dropup',
|
||||||
|
'md-arrow-dropup-circle',
|
||||||
|
'md-arrow-forward',
|
||||||
|
'md-arrow-round-back',
|
||||||
|
'md-arrow-round-down',
|
||||||
|
'md-arrow-round-forward',
|
||||||
|
'md-arrow-round-up',
|
||||||
|
'md-arrow-up',
|
||||||
|
'md-at',
|
||||||
|
'md-attach',
|
||||||
|
'md-backspace',
|
||||||
|
'md-barcode',
|
||||||
|
'md-baseball',
|
||||||
|
'md-basket',
|
||||||
|
'md-basketball',
|
||||||
|
'md-battery-charging',
|
||||||
|
'md-battery-dead',
|
||||||
|
'md-battery-full',
|
||||||
|
'md-beaker',
|
||||||
|
'md-beer',
|
||||||
|
'md-bicycle',
|
||||||
|
'md-bluetooth',
|
||||||
|
'md-boat',
|
||||||
|
'md-body',
|
||||||
|
'md-bonfire',
|
||||||
|
'md-book',
|
||||||
|
'md-bookmark',
|
||||||
|
'md-bookmarks',
|
||||||
|
'md-bowtie',
|
||||||
|
'md-briefcase',
|
||||||
|
'md-browsers',
|
||||||
|
'md-brush',
|
||||||
|
'md-bug',
|
||||||
|
'md-build',
|
||||||
|
'md-bulb',
|
||||||
|
'md-bus',
|
||||||
|
'md-cafe',
|
||||||
|
'md-calculator',
|
||||||
|
'md-calendar',
|
||||||
|
'md-call',
|
||||||
|
'md-camera',
|
||||||
|
'md-car',
|
||||||
|
'md-card',
|
||||||
|
'md-cart',
|
||||||
|
'md-cash',
|
||||||
|
'md-chatboxes',
|
||||||
|
'md-chatbubbles',
|
||||||
|
'md-checkbox',
|
||||||
|
'md-checkbox-outline',
|
||||||
|
'md-checkmark',
|
||||||
|
'md-checkmark-circle',
|
||||||
|
'md-checkmark-circle-outline',
|
||||||
|
'md-clipboard',
|
||||||
|
'md-clock',
|
||||||
|
'md-close',
|
||||||
|
'md-close-circle',
|
||||||
|
'md-closed-captioning',
|
||||||
|
'md-cloud',
|
||||||
|
'md-cloud-circle',
|
||||||
|
'md-cloud-done',
|
||||||
|
'md-cloud-download',
|
||||||
|
'md-cloud-outline',
|
||||||
|
'md-cloud-upload',
|
||||||
|
'md-cloudy',
|
||||||
|
'md-cloudy-night',
|
||||||
|
'md-code',
|
||||||
|
'md-code-download',
|
||||||
|
'md-code-working',
|
||||||
|
'md-cog',
|
||||||
|
'md-color-fill',
|
||||||
|
'md-color-filter',
|
||||||
|
'md-color-palette',
|
||||||
|
'md-color-wand',
|
||||||
|
'md-compass',
|
||||||
|
'md-construct',
|
||||||
|
'md-contact',
|
||||||
|
'md-contacts',
|
||||||
|
'md-contract',
|
||||||
|
'md-contrast',
|
||||||
|
'md-copy',
|
||||||
|
'md-create',
|
||||||
|
'md-crop',
|
||||||
|
'md-cube',
|
||||||
|
'md-cut',
|
||||||
|
'md-desktop',
|
||||||
|
'md-disc',
|
||||||
|
'md-document',
|
||||||
|
'md-done-all',
|
||||||
|
'md-download',
|
||||||
|
'md-easel',
|
||||||
|
'md-egg',
|
||||||
|
'md-exit',
|
||||||
|
'md-expand',
|
||||||
|
'md-eye',
|
||||||
|
'md-eye-off',
|
||||||
|
'md-fastforward',
|
||||||
|
'md-female',
|
||||||
|
'md-filing',
|
||||||
|
'md-film',
|
||||||
|
'md-finger-print',
|
||||||
|
'md-flag',
|
||||||
|
'md-flame',
|
||||||
|
'md-flash',
|
||||||
|
'md-flask',
|
||||||
|
'md-flower',
|
||||||
|
'md-folder',
|
||||||
|
'md-folder-open',
|
||||||
|
'md-football',
|
||||||
|
'md-funnel',
|
||||||
|
'md-game-controller-a',
|
||||||
|
'md-game-controller-b',
|
||||||
|
'md-git-branch',
|
||||||
|
'md-git-commit',
|
||||||
|
'md-git-compare',
|
||||||
|
'md-git-merge',
|
||||||
|
'md-git-network',
|
||||||
|
'md-git-pull-request',
|
||||||
|
'md-glasses',
|
||||||
|
'md-globe',
|
||||||
|
'md-grid',
|
||||||
|
'md-hammer',
|
||||||
|
'md-hand',
|
||||||
|
'md-happy',
|
||||||
|
'md-headset',
|
||||||
|
'md-heart',
|
||||||
|
'md-heart-outline',
|
||||||
|
'md-help',
|
||||||
|
'md-help-buoy',
|
||||||
|
'md-help-circle',
|
||||||
|
'md-home',
|
||||||
|
'md-ice-cream',
|
||||||
|
'md-image',
|
||||||
|
'md-images',
|
||||||
|
'md-infinite',
|
||||||
|
'md-information',
|
||||||
|
'md-information-circle',
|
||||||
|
'md-ionic',
|
||||||
|
'md-ionitron',
|
||||||
|
'md-jet',
|
||||||
|
'md-key',
|
||||||
|
'md-keypad',
|
||||||
|
'md-laptop',
|
||||||
|
'md-leaf',
|
||||||
|
'md-link',
|
||||||
|
'md-list',
|
||||||
|
'md-list-box',
|
||||||
|
'md-locate',
|
||||||
|
'md-lock',
|
||||||
|
'md-log-in',
|
||||||
|
'md-log-out',
|
||||||
|
'md-magnet',
|
||||||
|
'md-mail',
|
||||||
|
'md-mail-open',
|
||||||
|
'md-male',
|
||||||
|
'md-man',
|
||||||
|
'md-map',
|
||||||
|
'md-medal',
|
||||||
|
'md-medical',
|
||||||
|
'md-medkit',
|
||||||
|
'md-megaphone',
|
||||||
|
'md-menu',
|
||||||
|
'md-mic',
|
||||||
|
'md-mic-off',
|
||||||
|
'md-microphone',
|
||||||
|
'md-moon',
|
||||||
|
'md-more',
|
||||||
|
'md-move',
|
||||||
|
'md-musical-note',
|
||||||
|
'md-musical-notes',
|
||||||
|
'md-navigate',
|
||||||
|
'md-no-smoking',
|
||||||
|
'md-notifications',
|
||||||
|
'md-notifications-off',
|
||||||
|
'md-notifications-outline',
|
||||||
|
'md-nuclear',
|
||||||
|
'md-nutrition',
|
||||||
|
'md-open',
|
||||||
|
'md-options',
|
||||||
|
'md-outlet',
|
||||||
|
'md-paper',
|
||||||
|
'md-paper-plane',
|
||||||
|
'md-partly-sunny',
|
||||||
|
'md-pause',
|
||||||
|
'md-paw',
|
||||||
|
'md-people',
|
||||||
|
'md-person',
|
||||||
|
'md-person-add',
|
||||||
|
'md-phone-landscape',
|
||||||
|
'md-phone-portrait',
|
||||||
|
'md-photos',
|
||||||
|
'md-pie',
|
||||||
|
'md-pin',
|
||||||
|
'md-pint',
|
||||||
|
'md-pizza',
|
||||||
|
'md-plane',
|
||||||
|
'md-planet',
|
||||||
|
'md-play',
|
||||||
|
'md-podium',
|
||||||
|
'md-power',
|
||||||
|
'md-pricetag',
|
||||||
|
'md-pricetags',
|
||||||
|
'md-print',
|
||||||
|
'md-pulse',
|
||||||
|
'md-qr-scanner',
|
||||||
|
'md-quote',
|
||||||
|
'md-radio',
|
||||||
|
'md-radio-button-off',
|
||||||
|
'md-radio-button-on',
|
||||||
|
'md-rainy',
|
||||||
|
'md-recording',
|
||||||
|
'md-redo',
|
||||||
|
'md-refresh',
|
||||||
|
'md-refresh-circle',
|
||||||
|
'md-remove',
|
||||||
|
'md-remove-circle',
|
||||||
|
'md-reorder',
|
||||||
|
'md-repeat',
|
||||||
|
'md-resize',
|
||||||
|
'md-restaurant',
|
||||||
|
'md-return-left',
|
||||||
|
'md-return-right',
|
||||||
|
'md-reverse-camera',
|
||||||
|
'md-rewind',
|
||||||
|
'md-ribbon',
|
||||||
|
'md-rose',
|
||||||
|
'md-sad',
|
||||||
|
'md-school',
|
||||||
|
'md-search',
|
||||||
|
'md-send',
|
||||||
|
'md-settings',
|
||||||
|
'md-share',
|
||||||
|
'md-share-alt',
|
||||||
|
'md-shirt',
|
||||||
|
'md-shuffle',
|
||||||
|
'md-skip-backward',
|
||||||
|
'md-skip-forward',
|
||||||
|
'md-snow',
|
||||||
|
'md-speedometer',
|
||||||
|
'md-square',
|
||||||
|
'md-square-outline',
|
||||||
|
'md-star',
|
||||||
|
'md-star-half',
|
||||||
|
'md-star-outline',
|
||||||
|
'md-stats',
|
||||||
|
'md-stopwatch',
|
||||||
|
'md-subway',
|
||||||
|
'md-sunny',
|
||||||
|
'md-swap',
|
||||||
|
'md-switch',
|
||||||
|
'md-sync',
|
||||||
|
'md-tablet-landscape',
|
||||||
|
'md-tablet-portrait',
|
||||||
|
'md-tennisball',
|
||||||
|
'md-text',
|
||||||
|
'md-thermometer',
|
||||||
|
'md-thumbs-down',
|
||||||
|
'md-thumbs-up',
|
||||||
|
'md-thunderstorm',
|
||||||
|
'md-time',
|
||||||
|
'md-timer',
|
||||||
|
'md-train',
|
||||||
|
'md-transgender',
|
||||||
|
'md-trash',
|
||||||
|
'md-trending-down',
|
||||||
|
'md-trending-up',
|
||||||
|
'md-trophy',
|
||||||
|
'md-umbrella',
|
||||||
|
'md-undo',
|
||||||
|
'md-unlock',
|
||||||
|
'md-videocam',
|
||||||
|
'md-volume-down',
|
||||||
|
'md-volume-mute',
|
||||||
|
'md-volume-off',
|
||||||
|
'md-volume-up',
|
||||||
|
'md-walk',
|
||||||
|
'md-warning',
|
||||||
|
'md-watch',
|
||||||
|
'md-water',
|
||||||
|
'md-wifi',
|
||||||
|
'md-wine',
|
||||||
|
'md-woman'
|
||||||
|
]
|
||||||
@@ -72,6 +72,7 @@ export default {
|
|||||||
|
|
||||||
.table-scroll-container {
|
.table-scroll-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
/* 横向滚动条始终可见,不需要滚动到底 */
|
/* 横向滚动条始终可见,不需要滚动到底 */
|
||||||
|
|||||||
@@ -115,6 +115,17 @@ export const defaultMenus = [
|
|||||||
is_show_menu: 1,
|
is_show_menu: 1,
|
||||||
icon: 'md-text',
|
icon: 'md-text',
|
||||||
sort: 3
|
sort: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 125,
|
||||||
|
name: '租户管理',
|
||||||
|
path: '/system/tenant',
|
||||||
|
component: 'system/sys_tenant',
|
||||||
|
parent_id: 120,
|
||||||
|
type: '页面',
|
||||||
|
is_show_menu: 1,
|
||||||
|
icon: 'md-git-branch',
|
||||||
|
sort: 4
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import uiTool from '../utils/uiTool'
|
|||||||
import { defaultMenus, filterMenusByIds } from '../config/menuConfig'
|
import { defaultMenus, filterMenusByIds } from '../config/menuConfig'
|
||||||
import userServer from '../api/system/userServer'
|
import userServer from '../api/system/userServer'
|
||||||
|
|
||||||
|
function readTenantLs() {
|
||||||
|
try {
|
||||||
|
const s = localStorage.getItem('currentTenant')
|
||||||
|
if (!s || s === 'undefined') return null
|
||||||
|
return JSON.parse(s)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
@@ -10,7 +20,9 @@ export default {
|
|||||||
avatorImgPath: '',
|
avatorImgPath: '',
|
||||||
token: getToken(),
|
token: getToken(),
|
||||||
authorityMenus: [],
|
authorityMenus: [],
|
||||||
menuList: localStorage.getItem('menuList') ? JSON.parse(localStorage.getItem('menuList')) : []
|
menuList: localStorage.getItem('menuList') ? JSON.parse(localStorage.getItem('menuList')) : [],
|
||||||
|
/** 登录返回的租户信息:{ id, name, code, is_platform } */
|
||||||
|
currentTenant: readTenantLs()
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setAvator(state, avatorPath) {
|
setAvator(state, avatorPath) {
|
||||||
@@ -31,6 +43,14 @@ export default {
|
|||||||
setMenuList(state, menus) {
|
setMenuList(state, menus) {
|
||||||
state.menuList = menus
|
state.menuList = menus
|
||||||
localStorage.setItem('menuList', JSON.stringify(menus))
|
localStorage.setItem('menuList', JSON.stringify(menus))
|
||||||
|
},
|
||||||
|
setCurrentTenant(state, tenant) {
|
||||||
|
state.currentTenant = tenant && typeof tenant === 'object' ? tenant : null
|
||||||
|
if (state.currentTenant) {
|
||||||
|
localStorage.setItem('currentTenant', JSON.stringify(state.currentTenant))
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('currentTenant')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@@ -162,6 +182,11 @@ export default {
|
|||||||
|
|
||||||
commit('setUserName', name)
|
commit('setUserName', name)
|
||||||
commit('setToken', token)
|
commit('setToken', token)
|
||||||
|
if (res.data.tenant) {
|
||||||
|
commit('setCurrentTenant', res.data.tenant)
|
||||||
|
} else {
|
||||||
|
commit('setCurrentTenant', null)
|
||||||
|
}
|
||||||
|
|
||||||
// 登录接口返回的 authorityMenus 是菜单 ID 数组字符串
|
// 登录接口返回的 authorityMenus 是菜单 ID 数组字符串
|
||||||
console.log('登录返回的菜单 IDs:', authorityMenusIds)
|
console.log('登录返回的菜单 IDs:', authorityMenusIds)
|
||||||
@@ -205,6 +230,7 @@ export default {
|
|||||||
commit('setToken', '')
|
commit('setToken', '')
|
||||||
commit('setAuthorityMenus', '[]')
|
commit('setAuthorityMenus', '[]')
|
||||||
commit('setMenuList', [])
|
commit('setMenuList', [])
|
||||||
|
commit('setCurrentTenant', null)
|
||||||
localStorage.removeItem('menuList')
|
localStorage.removeItem('menuList')
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { formatDate } from './tools'
|
import { formatDate } from './tools'
|
||||||
|
|
||||||
|
/** 递归删除对象中值为空字符串的键(不修改数组内的空字符串元素) */
|
||||||
|
function stripEmptyStringKeys(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => stripEmptyStringKeys(item))
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]') {
|
||||||
|
const next = {}
|
||||||
|
for (const k of Object.keys(value)) {
|
||||||
|
const v = value[k]
|
||||||
|
if (v === '') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next[k] = stripEmptyStringKeys(v)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
class Http {
|
class Http {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -131,13 +152,18 @@ class Http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
param = JSON.parse(JSON.stringify(param))
|
param = JSON.parse(JSON.stringify(param))
|
||||||
return param
|
return stripEmptyStringKeys(param)
|
||||||
}
|
}
|
||||||
|
|
||||||
formatFormDataParam(param) {
|
formatFormDataParam(param) {
|
||||||
|
param = param || {}
|
||||||
let formData = new FormData()
|
let formData = new FormData()
|
||||||
Object.keys(param).forEach(key => {
|
Object.keys(param).forEach((key) => {
|
||||||
formData.append(key, param[key])
|
const v = param[key]
|
||||||
|
if (v === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formData.append(key, v)
|
||||||
})
|
})
|
||||||
return formData
|
return formData
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SysLog from './system/sys_log.vue'
|
|||||||
import SysParamSetup from './system/sys_param_setup.vue'
|
import SysParamSetup from './system/sys_param_setup.vue'
|
||||||
import SysRole from './system/sys_role.vue'
|
import SysRole from './system/sys_role.vue'
|
||||||
import SysUser from './system/sys_user.vue'
|
import SysUser from './system/sys_user.vue'
|
||||||
|
import SysTenant from './system/sys_tenant.vue'
|
||||||
import SysLogOperate from './system/sys_log_operate.vue'
|
import SysLogOperate from './system/sys_log_operate.vue'
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ export function setupComponentMap(customMap = {}, uiTool) {
|
|||||||
'system/sys_param_setup': SysParamSetup,
|
'system/sys_param_setup': SysParamSetup,
|
||||||
'system/sys_role': SysRole,
|
'system/sys_role': SysRole,
|
||||||
'system/sys_user': SysUser,
|
'system/sys_user': SysUser,
|
||||||
|
'system/sys_tenant': SysTenant,
|
||||||
'system/sys_control': SysControl,
|
'system/sys_control': SysControl,
|
||||||
'system/sys_menu': SysMenu,
|
'system/sys_menu': SysMenu,
|
||||||
'system/sys_title': SysTitle,
|
'system/sys_title': SysTitle,
|
||||||
@@ -54,11 +56,13 @@ export function setupComponentMap(customMap = {}, uiTool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
HomePage,
|
||||||
SysLog,
|
SysLog,
|
||||||
SysLogOperate,
|
SysLogOperate,
|
||||||
SysParamSetup,
|
SysParamSetup,
|
||||||
SysRole,
|
SysRole,
|
||||||
SysUser,
|
SysUser,
|
||||||
|
SysTenant,
|
||||||
SysControl,
|
SysControl,
|
||||||
SysMenu,
|
SysMenu,
|
||||||
SysTitle,
|
SysTitle,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="login-con">
|
<div class="login-con">
|
||||||
<Card icon="log-in" title="欢迎登录" style="width:350px;height:300px;" :bordered="false">
|
<Card icon="log-in" title="欢迎登录" style="width:350px;height:260px;" :bordered="false">
|
||||||
<div class="form-con">
|
<div class="form-con">
|
||||||
<login-form @on-success-valid="handleSubmit"></login-form>
|
<login-form @on-success-valid="handleSubmit"></login-form>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +45,10 @@ export default {
|
|||||||
...mapActions('user', ['handleLogin']),
|
...mapActions('user', ['handleLogin']),
|
||||||
async handleSubmit({ userName, password }) {
|
async handleSubmit({ userName, password }) {
|
||||||
try {
|
try {
|
||||||
let userFrom = { name: userName, password: password }
|
let userFrom = {
|
||||||
|
name: userName,
|
||||||
|
password: password
|
||||||
|
}
|
||||||
await this.handleLogin({
|
await this.handleLogin({
|
||||||
userFrom,
|
userFrom,
|
||||||
Main,
|
Main,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<Button type="primary" @click="addWarp()">新增</Button>
|
<Button type="primary" @click="addWarp()">新增</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-body">
|
<div class="table-body">
|
||||||
<TreeGrid :columns="gridOption.columns" :data="gridOption.data"></TreeGrid>
|
<TreeGrid :columns="gridOption.columns" :data="gridOption.data"></TreeGrid>
|
||||||
</div>
|
</div>
|
||||||
<editModal ref="editModal" :columns="gridOption.editColumns" :rules="gridOption.rules">
|
<editModal ref="editModal" :columns="gridOption.editColumns" :rules="gridOption.rules">
|
||||||
<div slot="bottom">
|
<div slot="bottom">
|
||||||
@@ -12,12 +12,12 @@
|
|||||||
<fieldItem name='类别'>
|
<fieldItem name='类别'>
|
||||||
<RadioGroup v-model="editRow.type">
|
<RadioGroup v-model="editRow.type">
|
||||||
<Radio :label="item.key" :key="item.key" v-for="item in typeSource">
|
<Radio :label="item.key" :key="item.key" v-for="item in typeSource">
|
||||||
{{item.value}}
|
{{ item.value }}
|
||||||
</Radio>
|
</Radio>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</fieldItem>
|
</fieldItem>
|
||||||
|
|
||||||
<div v-if="editRow.type==='页面'||editRow.type==='功能'">
|
<div v-if="editRow.type === '页面' || editRow.type === '功能'">
|
||||||
<fieldItem name='数据模型'>
|
<fieldItem name='数据模型'>
|
||||||
<Select v-model="editRow.model_id">
|
<Select v-model="editRow.model_id">
|
||||||
<Option v-for="item in modelRows" :value="item.id" :key="item.id">{{ item.value }}</Option>
|
<Option v-for="item in modelRows" :value="item.id" :key="item.id">{{ item.value }}</Option>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</fieldItem>
|
</fieldItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldItem name='地址' v-if="editRow.type==='外链'">
|
<fieldItem name='地址' v-if="editRow.type === '外链'">
|
||||||
<Input v-model="editRow.component" placeholder="请输入网址" />
|
<Input v-model="editRow.component" placeholder="请输入网址" />
|
||||||
</fieldItem>
|
</fieldItem>
|
||||||
|
|
||||||
@@ -266,16 +266,18 @@ export default {
|
|||||||
async initCol() {
|
async initCol() {
|
||||||
let res = await menuServer.modelAll()
|
let res = await menuServer.modelAll()
|
||||||
|
|
||||||
let data = res.data.map((row) => {
|
if ( res.data && typeof res.data === 'array') {
|
||||||
let { id, key, name } = row
|
let data = res.data.map((row) => {
|
||||||
let value = key
|
let { id, key, name } = row
|
||||||
if (name) {
|
let value = key
|
||||||
value = value + '-' + name
|
if (name) {
|
||||||
}
|
value = value + '-' + name
|
||||||
|
}
|
||||||
|
|
||||||
return { id, value, key }
|
return { id, value, key }
|
||||||
})
|
})
|
||||||
this.modelRows = [{ id: 0, value: '自定义模板', key: 'custom_template' }, ...data] || []
|
this.modelRows = [{ id: 0, value: '自定义模板', key: 'custom_template' }, ...data] || []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
calculate() {
|
calculate() {
|
||||||
@@ -400,6 +402,17 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
|
.content-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="content-view">
|
<div class="content-view">
|
||||||
|
<Alert type="info" show-icon closable style="margin-bottom: 12px">
|
||||||
|
角色为<strong>全库共用</strong>,不按租户隔离;各租户用户可在「用户管理」中绑定同一角色,菜单权限以角色配置为准。
|
||||||
|
</Alert>
|
||||||
<div class="table-head-tool">
|
<div class="table-head-tool">
|
||||||
<Button type="primary" @click="showAddWarp">新增</Button>
|
<Button type="primary" @click="showAddWarp">新增</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,6 +76,23 @@ export default {
|
|||||||
this.init()
|
this.init()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
parseRoleMenuIds(menus) {
|
||||||
|
if (menus == null || menus === '') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (Array.isArray(menus)) {
|
||||||
|
return menus.map((id) => Number(id)).filter((id) => !Number.isNaN(id))
|
||||||
|
}
|
||||||
|
if (typeof menus === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(menus)
|
||||||
|
return this.parseRoleMenuIds(parsed)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
async init() {
|
async init() {
|
||||||
let res = await roleServer.list()
|
let res = await roleServer.list()
|
||||||
this.gridOption.data = res.data
|
this.gridOption.data = res.data
|
||||||
@@ -120,12 +140,8 @@ export default {
|
|||||||
|
|
||||||
let res = await menuServer.list()
|
let res = await menuServer.list()
|
||||||
let tree = uiTool.transformTree(res.data)
|
let tree = uiTool.transformTree(res.data)
|
||||||
if (row.menus) {
|
this.expandTreeAll = this.parseRoleMenuIds(row.menus)
|
||||||
this.expandTreeAll = JSON.parse(row.menus)
|
this.treeData = this.mapTree(tree)
|
||||||
this.treeData = this.mapTree(tree)
|
|
||||||
} else {
|
|
||||||
this.treeData = this.mapTree(tree)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShowPermission = true
|
this.isShowPermission = true
|
||||||
},
|
},
|
||||||
@@ -136,7 +152,7 @@ export default {
|
|||||||
p.children = this.mapTree(p.children)
|
p.children = this.mapTree(p.children)
|
||||||
}
|
}
|
||||||
|
|
||||||
let row = this.expandTreeAll.find((p2) => p2 === p.id)
|
let row = this.expandTreeAll.find((p2) => Number(p2) === Number(p.id))
|
||||||
if (row) {
|
if (row) {
|
||||||
p.checked = true
|
p.checked = true
|
||||||
}
|
}
|
||||||
|
|||||||
143
src/views/system/sys_tenant.vue
Normal file
143
src/views/system/sys_tenant.vue
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-view">
|
||||||
|
<Alert type="warning" show-icon style="margin-bottom: 12px">
|
||||||
|
仅<strong>平台租户</strong>(is_platform=1)可维护租户列表;普通租户登录后本页仅能看到自身租户信息。
|
||||||
|
新增时<strong>租户编码可留空</strong>,留空则由服务端自动生成;填写则使用自定义编码(须唯一)。需在数据库执行迁移脚本创建 <code>sys_tenant</code> 表及默认数据。
|
||||||
|
</Alert>
|
||||||
|
<div class="table-head-tool">
|
||||||
|
<Button type="primary" @click="showAddWarp">新增租户</Button>
|
||||||
|
</div>
|
||||||
|
<div class="table-body">
|
||||||
|
<tables ref="tables" v-model="gridOption.data" :columns="gridOption.columns" />
|
||||||
|
</div>
|
||||||
|
<editModal ref="editModal" :columns="gridOption.columns" :data="gridOption.editRow" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import uiTool from '@/utils/uiTool'
|
||||||
|
import sysTenantServer from '@/api/system/sysTenantServer'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'sys_tenant_page',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
gridOption: {
|
||||||
|
editRow: {},
|
||||||
|
columns: [
|
||||||
|
{ title: 'id', key: 'id' },
|
||||||
|
{ title: '名称', key: 'name', name: 'name', required: true },
|
||||||
|
{
|
||||||
|
title: '租户编码',
|
||||||
|
key: 'code',
|
||||||
|
name: 'code',
|
||||||
|
placeholder: '留空则保存时自动生成;填写则使用自定义编码(须唯一)'
|
||||||
|
},
|
||||||
|
{ title: '备注', key: 'remark', name: 'remark' },
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
name: 'status',
|
||||||
|
com: 'Radio',
|
||||||
|
source: [
|
||||||
|
{ key: 1, value: '启用' },
|
||||||
|
{ key: 0, value: '停用' }
|
||||||
|
],
|
||||||
|
render(h, p) {
|
||||||
|
return h('span', p.row.status === 1 ? '启用' : '停用')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '平台租户',
|
||||||
|
key: 'is_platform',
|
||||||
|
name: 'is_platform',
|
||||||
|
com: 'Switch',
|
||||||
|
render(h, p) {
|
||||||
|
return h('span', Number(p.row.is_platform) === 1 ? '是' : '否')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
render: (h, params) => {
|
||||||
|
if (params.row.id === 1) {
|
||||||
|
return h('span', { style: { color: '#999' } }, '内置租户')
|
||||||
|
}
|
||||||
|
const btns = [
|
||||||
|
{
|
||||||
|
title: '修改',
|
||||||
|
type: 'primary',
|
||||||
|
click: () => this.showEditWarp(params.row)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '删除',
|
||||||
|
type: 'primary',
|
||||||
|
click: () => this.delConfirm(params.row)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return uiTool.getBtn(h, btns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
normalizeTenantRow(row) {
|
||||||
|
const r = { ...row }
|
||||||
|
r.is_platform = r.is_platform === true || r.is_platform === 1 || r.is_platform === '1' ? 1 : 0
|
||||||
|
if (r.code != null && typeof r.code === 'string' && !r.code.trim()) {
|
||||||
|
delete r.code
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
async init() {
|
||||||
|
const res = await sysTenantServer.list()
|
||||||
|
if (res && res.code === 0) {
|
||||||
|
this.gridOption.data = res.data || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showAddWarp() {
|
||||||
|
this.$refs.editModal.addShow(
|
||||||
|
{ status: 1, is_platform: 0 },
|
||||||
|
async (row) => {
|
||||||
|
await sysTenantServer.add(this.normalizeTenantRow(row))
|
||||||
|
this.$Message.success('新增成功')
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
showEditWarp(row) {
|
||||||
|
this.$refs.editModal.editShow(row, async (newRow) => {
|
||||||
|
await sysTenantServer.edit(this.normalizeTenantRow(newRow))
|
||||||
|
this.$Message.success('修改成功')
|
||||||
|
this.init()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delConfirm(row) {
|
||||||
|
uiTool.delConfirm(async () => {
|
||||||
|
await sysTenantServer.del(row)
|
||||||
|
this.$Message.success('删除成功')
|
||||||
|
this.init()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="content-view">
|
<div class="content-view">
|
||||||
|
<Alert v-if="currentTenant" type="info" show-icon closable style="margin-bottom: 12px">
|
||||||
|
当前租户:<strong>{{ currentTenant.name }}</strong>({{ currentTenant.code }})。用户按租户隔离;
|
||||||
|
<strong>角色全库共用</strong>,下拉中的角色对所有租户一致。
|
||||||
|
</Alert>
|
||||||
|
|
||||||
<div class="table-head-tool">
|
<div class="table-head-tool">
|
||||||
<Button type="primary" @click="showAddWarp">新增</Button>
|
<Button type="primary" @click="showAddWarp">新增</Button>
|
||||||
@@ -16,9 +20,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
import userServer from '@/api/system/userServer'
|
import userServer from '@/api/system/userServer'
|
||||||
import roleServer from '@/api/system/roleServer'
|
import roleServer from '@/api/system/roleServer'
|
||||||
|
import sysTenantServer from '@/api/system/sysTenantServer'
|
||||||
import uiTool from '@/utils/uiTool'
|
import uiTool from '@/utils/uiTool'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -29,8 +34,21 @@ export default {
|
|||||||
gridOption: {
|
gridOption: {
|
||||||
editRow: {},
|
editRow: {},
|
||||||
columns: [
|
columns: [
|
||||||
{ title: '登陆名', key: 'name' },
|
{ title: '登陆名', key: 'name', name: 'name' },
|
||||||
|
{
|
||||||
|
title: '租户',
|
||||||
|
key: 'tenant_id',
|
||||||
|
name: 'tenant_id',
|
||||||
|
com: 'Select',
|
||||||
|
required: true,
|
||||||
|
render(h, params) {
|
||||||
|
if (params.row.tenant) {
|
||||||
|
const t = params.row.tenant
|
||||||
|
return h('span', `${t.name || ''}(${t.code || ''})`)
|
||||||
|
}
|
||||||
|
return h('span', params.row.tenant_id != null ? String(params.row.tenant_id) : '')
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '密码',
|
title: '密码',
|
||||||
key: 'password',
|
key: 'password',
|
||||||
@@ -41,6 +59,7 @@ export default {
|
|||||||
{
|
{
|
||||||
title: '所属角色',
|
title: '所属角色',
|
||||||
key: 'roleId',
|
key: 'roleId',
|
||||||
|
name: 'roleId',
|
||||||
com: 'Select',
|
com: 'Select',
|
||||||
render: (h, params) => {
|
render: (h, params) => {
|
||||||
if (params.row.role) {
|
if (params.row.role) {
|
||||||
@@ -83,6 +102,12 @@ export default {
|
|||||||
...mapGetters({
|
...mapGetters({
|
||||||
shopList: 'shop/shopList',
|
shopList: 'shop/shopList',
|
||||||
}),
|
}),
|
||||||
|
...mapState('user', ['currentTenant']),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentTenant() {
|
||||||
|
this.initCol()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.init()
|
this.init()
|
||||||
@@ -94,31 +119,53 @@ export default {
|
|||||||
this.gridOption.data = res.data
|
this.gridOption.data = res.data
|
||||||
},
|
},
|
||||||
async initCol() {
|
async initCol() {
|
||||||
let res = await roleServer.list()
|
const res = await roleServer.list()
|
||||||
this.roles = res.data
|
this.roles = res.data || []
|
||||||
let roleSource = this.roles.map((p) => {
|
const roleSource = this.roles.map((p) => ({
|
||||||
return {
|
key: p.id,
|
||||||
key: p.id,
|
value: p.name,
|
||||||
value: p.name,
|
}))
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let roleRow = this.gridOption.columns.find((p) => p.key === 'roleId')
|
const roleRow = this.gridOption.columns.find((p) => p.key === 'roleId')
|
||||||
if (roleRow) {
|
if (roleRow) {
|
||||||
roleRow.source = roleSource
|
roleRow.source = roleSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tenantRes = await sysTenantServer.list()
|
||||||
|
const tenants = tenantRes && tenantRes.code === 0 && Array.isArray(tenantRes.data) ? tenantRes.data : []
|
||||||
|
const tenantSource = tenants.map((t) => ({
|
||||||
|
key: Number(t.id),
|
||||||
|
value: `${t.name || ''}(${t.code || ''})`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const platform = this.currentTenant && Number(this.currentTenant.is_platform) === 1
|
||||||
|
const tenantCol = this.gridOption.columns.find((p) => p.key === 'tenant_id')
|
||||||
|
if (tenantCol) {
|
||||||
|
tenantCol.source = tenantSource
|
||||||
|
tenantCol.disabled = !platform
|
||||||
|
tenantCol.disabledOnAdd = !platform
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showAddWarp() {
|
showAddWarp() {
|
||||||
this.$refs.editModal.addShow({}, async (newRow) => {
|
const tid = this.currentTenant && this.currentTenant.id != null ? Number(this.currentTenant.id) : undefined
|
||||||
await userServer.add(newRow)
|
this.$refs.editModal.addShow({ tenant_id: tid }, async (newRow) => {
|
||||||
|
const payload = { ...newRow }
|
||||||
|
if (payload.tenant_id != null) {
|
||||||
|
payload.tenant_id = Number(payload.tenant_id)
|
||||||
|
}
|
||||||
|
await userServer.add(payload)
|
||||||
this.$Message.success('新增成功!')
|
this.$Message.success('新增成功!')
|
||||||
this.init()
|
this.init()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
showEditWarp(row) {
|
showEditWarp(row) {
|
||||||
this.$refs.editModal.editShow(row, async (newRow) => {
|
this.$refs.editModal.editShow({ ...row }, async (newRow) => {
|
||||||
await userServer.edit(newRow)
|
const payload = { ...newRow }
|
||||||
|
if (payload.tenant_id != null) {
|
||||||
|
payload.tenant_id = Number(payload.tenant_id)
|
||||||
|
}
|
||||||
|
await userServer.edit(payload)
|
||||||
this.$Message.success('修改成功!')
|
this.$Message.success('修改成功!')
|
||||||
this.init()
|
this.init()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const webpack = require('webpack')
|
const webpack = require('webpack')
|
||||||
|
|
||||||
|
/** 构建完成后将根目录 README.md 复制为 dist/admin-framework.md */
|
||||||
|
function copyReadmeToDistPlugin() {
|
||||||
|
return {
|
||||||
|
apply(compiler) {
|
||||||
|
compiler.hooks.afterEmit.tap('CopyReadmeToDist', () => {
|
||||||
|
const src = path.join(__dirname, 'README.md')
|
||||||
|
const dest = path.join(compiler.options.output.path, 'admin-framework.md')
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(src)) {
|
||||||
|
fs.copyFileSync(src, dest)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[webpack] 复制 README.md -> dist/admin-framework.md 失败:', e.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const { VueLoaderPlugin } = require('vue-loader')
|
const { VueLoaderPlugin } = require('vue-loader')
|
||||||
const TerserPlugin = require('terser-webpack-plugin')
|
const TerserPlugin = require('terser-webpack-plugin')
|
||||||
|
|
||||||
@@ -142,6 +161,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new VueLoaderPlugin(),
|
new VueLoaderPlugin(),
|
||||||
|
copyReadmeToDistPlugin(),
|
||||||
new webpack.BannerPlugin({
|
new webpack.BannerPlugin({
|
||||||
banner: () => {
|
banner: () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|||||||
Reference in New Issue
Block a user