feat: 新增角色管理模块,优化API与交互体验
1. 新增角色管理后台页面、路由与国际化文案 2. 重构API请求错误处理逻辑,统一拦截业务与HTTP错误 3. 新增确认弹窗组合式函数,区分取消与请求错误场景 4. 完善表格按钮权限与显示控制逻辑 5. 更新API参数规范与文档说明 6. 修复部分页面分页数据解析问题
This commit is contained in:
213
docs/表格组件使用说明.md
213
docs/表格组件使用说明.md
@@ -17,7 +17,8 @@
|
||||
7. [路由配置](#7-路由配置)
|
||||
8. [API 文件写法](#8-api-文件写法)
|
||||
9. [旧代码迁移对照](#9-旧代码迁移对照)
|
||||
10. [常见问题排查](#10-常见问题排查)
|
||||
10. [接口请求错误处理规范](#10-接口请求错误处理规范)
|
||||
11. [常见问题排查](#11-常见问题排查)
|
||||
|
||||
---
|
||||
|
||||
@@ -963,7 +964,215 @@ export function deleteFactoryArea (data) {
|
||||
|
||||
---
|
||||
|
||||
## 10. 常见问题排查
|
||||
## 10. 接口请求错误处理规范
|
||||
|
||||
> 错误处理采用 **三层架构**:拦截器全局兜底 → confirmMixin 区分取消/错误 → 页面标准模式。
|
||||
|
||||
### 10.1 错误处理总流程
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ 页面发起 API 请求 │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ 是否需 confirm ? │
|
||||
└─────┬────────┬──────┘
|
||||
│ 否 │ 是
|
||||
│ │
|
||||
│ ┌────▼───────────┐
|
||||
│ │ $confirmAction() │
|
||||
│ │ (confirmMixin) │
|
||||
│ └────┬──────┬─────┘
|
||||
│ 取消 │ │ 确认
|
||||
│ ┌────▼──┐ │
|
||||
│ │return │ │
|
||||
│ │ true │ │
|
||||
│ └───────┘ │
|
||||
│ │
|
||||
┌──────────▼────────────────▼──┐
|
||||
│ axios 请求 │
|
||||
└──────────┬───────────────────┘
|
||||
│
|
||||
┌───────────▼────────────┐
|
||||
│ HTTP 状态码是什么? │
|
||||
└──┬──────────────┬──────┘
|
||||
200 │ │ 非 200
|
||||
│ │
|
||||
┌───────────▼──────┐ ┌───▼─────────────┐
|
||||
│ response.data. │ │ HTTP 拦截器 │
|
||||
│ code 是什么? │ │ error.message = │
|
||||
└──┬───────────┬───┘ │ '未授权/超时/...' │
|
||||
0 │ │≠0 └───┬──────────────┘
|
||||
│ │ │
|
||||
┌────▼──┐ ┌─────▼──────┐ │
|
||||
│resolve│ │handleError()│◄─┘
|
||||
│ data │ │Message.error│
|
||||
└───────┘ └─────┬──────┘
|
||||
│
|
||||
┌────▼────┐
|
||||
│ throw │
|
||||
│ error │
|
||||
└────┬────┘
|
||||
│
|
||||
┌─────────▼─────────┐
|
||||
│ 页面层 catch 到了吗?│
|
||||
└────┬──────────┬───┘
|
||||
没写 │ │ 写了
|
||||
│ │
|
||||
┌─────────▼──┐ ┌────▼─────────────┐
|
||||
│ 什么都不发生 │ │ finally 关锁 │
|
||||
│(依赖拦截器 │ │ loading/submitting│
|
||||
│ Message) │ │ = false │
|
||||
└────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
### 10.2 三层架构详图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 第一层:全局拦截器 │
|
||||
│ src/api/_service.js │
|
||||
│ │
|
||||
│ 拦截所有 axios 响应,统一处理两类错误: │
|
||||
│ │
|
||||
│ HTTP 错误 (4xx/5xx) │
|
||||
│ ├─ 400 → error.message = '请求错误' │
|
||||
│ ├─ 401 → error.message = '未授权,请登录' │
|
||||
│ ├─ 403 → error.message = '拒绝访问' │
|
||||
│ ├─ 404 → error.message = '请求地址出错' │
|
||||
│ ├─ 408 → error.message = '请求超时' │
|
||||
│ ├─ 500 → error.message = '服务器内部错误' │
|
||||
│ ├─ 502 → error.message = '网关错误' │
|
||||
│ ├─ ... 其它状态码 │
|
||||
│ │ │
|
||||
│ └─ handleError(error) │
|
||||
│ ├─ store.dispatch('d2admin/log/push', ...) │
|
||||
│ ├─ console.log(error) (dev only) │
|
||||
│ └─ Message.error(error.message) ← 弹红色提示 │
|
||||
│ │
|
||||
│ 业务错误 (code ≠ 0, 如 code=500 "参数错误") │
|
||||
│ ├─ 取出 response.data.msg 或 '请求失败' │
|
||||
│ ├─ const err = new Error(`${msg}: ${url}`) │
|
||||
│ ├─ handleError(err) │
|
||||
│ └─ throw err ← 继续向上抛给页面层 │
|
||||
└──────────────────────┬──────────────────────────┘
|
||||
│
|
||||
┌──────────────────────▼──────────────────────────┐
|
||||
│ 第二层:confirmMixin │
|
||||
│ src/composables/useConfirmHandle.js │
|
||||
│ │
|
||||
│ $confirmAction(confirmOpts, action) │
|
||||
│ ├─ 第一层 try/catch │
|
||||
│ │ await this.$confirm(...) │
|
||||
│ │ ├─ 用户点"确定" → 继续 │
|
||||
│ │ └─ 用户点"取消" → catch → return true │
|
||||
│ │ │
|
||||
│ └─ 第二层 try/catch │
|
||||
│ await action() ← 执行 API 调用 │
|
||||
│ ├─ 成功 → return false │
|
||||
│ └─ 失败 → catch → return false │
|
||||
│ (拦截器已弹出 Message,此处静默吞掉) │
|
||||
└──────────────────────┬──────────────────────────┘
|
||||
│
|
||||
┌──────────────────────▼──────────────────────────┐
|
||||
│ 第三层:页面标准模式 │
|
||||
│ │
|
||||
│ 场景A — 查询 (fetchData) │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ async fetchData () { │ │
|
||||
│ │ this.loading = true │ │
|
||||
│ │ try { │ │
|
||||
│ │ const res = await getList(...) │ │
|
||||
│ │ this.tableData = res.data │ │
|
||||
│ │ } finally { this.loading = false } │ │
|
||||
│ │ } │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ • try 内无 catch:拦截器已弹错误提示 │
|
||||
│ • finally 始终执行:loading 无论成败都关闭 │
|
||||
│ │
|
||||
│ 场景B — 提交 (onDialogSubmit) │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ async onDialogSubmit () { │ │
|
||||
│ │ this.submitting = true │ │
|
||||
│ │ try { │ │
|
||||
│ │ await createApi(this.formData) │ │
|
||||
│ │ this.$message.success(...) ← 成功才执行 │ │
|
||||
│ │ this.dialogVisible = false │ │
|
||||
│ │ this.fetchData() │ │
|
||||
│ │ } finally { this.submitting = false } │ │
|
||||
│ │ } │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ • 出错时弹窗不关,用户可修改后重试 │
|
||||
│ │
|
||||
│ 场景C — 删除 (handleDelete) │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ async handleDelete (row) { │ │
|
||||
│ │ const cancelled = await this.$confirmAction(│ │
|
||||
│ │ { message: this.key('confirm_delete'), │ │
|
||||
│ │ title: this.key('tip') }, │ │
|
||||
│ │ () => deleteApi({ id: row.id }) │ │
|
||||
│ │ ) │ │
|
||||
│ │ if (cancelled) return ← 用户取消,静默 │ │
|
||||
│ │ this.$message.success(...) ← 成功才执行 │ │
|
||||
│ │ this.fetchData() │ │
|
||||
│ │ } │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ • $confirmAction 内已处理错误 Message,不额外 catch│
|
||||
│ • 没有 try/catch 包裹 → 代码更简洁直观 │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 10.3 关键原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **拦截器兜底** | 所有 API 错误(HTTP + 业务)统一在 `_service.js` 中弹出 `Message.error` |
|
||||
| **finally 关锁** | `loading` / `submitting` 用 `try { ... } finally { lock = false }`,不论成败都关闭 |
|
||||
| **只用 catch 不动** | 成功后的 `$message.success` / `dialogVisible = false` / `fetchData` 不放在 `finally`,只有成功才执行 |
|
||||
| **$confirmAction 包装** | 删除/批量操作类的 confirm + API 用 `$confirmAction()` 区分取消和错误 |
|
||||
| **不吞业务错误** | `fetchData` / `onDialogSubmit` 的 try 只写 `finally`,不写 `catch`,错误由拦截器处理 |
|
||||
|
||||
### 10.4 confirmMixin 使用
|
||||
|
||||
页面中引入:
|
||||
|
||||
```js
|
||||
import { confirmMixin } from '@/composables/useConfirmHandle'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
i18nMixin('page.xxx.xxx'),
|
||||
confirmMixin // ← 新增
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
```js
|
||||
// $confirmAction(confirmOpts, actionFn) → Promise<boolean>
|
||||
// 返回 true = 用户点了取消
|
||||
// 返回 false = action 已执行完成(无论成功失败)
|
||||
|
||||
const cancelled = await this.$confirmAction(
|
||||
{
|
||||
message: this.key('confirm_delete'), // confirm 正文
|
||||
title: this.key('tip'), // confirm 标题
|
||||
// type: 'warning', // 可选,默认 'warning'
|
||||
// confirmButtonText / cancelButtonText 可选,默认用 page.common 的
|
||||
},
|
||||
() => deleteApi({ id: row.id }) // 确认后执行的异步函数
|
||||
)
|
||||
if (cancelled) return
|
||||
// 以下只在成功时执行
|
||||
this.$message.success(...)
|
||||
this.fetchData()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 常见问题排查
|
||||
|
||||
### Q1:弹框打开后不显示内容?
|
||||
|
||||
|
||||
@@ -41,6 +41,18 @@
|
||||
```
|
||||
- 后续统一修改路由后再批量替换
|
||||
|
||||
#### 4.1 API 参数名必须对齐旧项目(重要)
|
||||
- **API 函数入参的 key 名称必须与旧项目保持一致**,不能自行发明参数名
|
||||
- 例如:旧项目角色菜单接口参数是 `role_id`,不能写成 `id`
|
||||
```js
|
||||
// ❌ 错误 — 自行发明参数名
|
||||
getRoleMenu({ id: this.roleId })
|
||||
// ✅ 正确 — 与旧项目参数名一致
|
||||
getRoleMenu({ role_id: this.roleId })
|
||||
```
|
||||
- **迁移 API 时必须仔细阅读旧项目接口代码**,逐个核对每个接口的入参 key 名称
|
||||
- 如果后端报「xxx 不能为空」,首先检查参数 key 是否与旧项目一致
|
||||
|
||||
#### 5. 旧 key → 新 key 映射
|
||||
如果用户提供的是旧页面代码,需要根据对照表做 key 映射。已知映射如下(持续补充):
|
||||
|
||||
|
||||
@@ -80,14 +80,19 @@ function createService () {
|
||||
}
|
||||
|
||||
// 有 code 判断为项目接口请求
|
||||
let errorMessage = ''
|
||||
switch (response.data.code) {
|
||||
// 返回响应内容
|
||||
case 0: return response.data.data
|
||||
// 例如在 code 401 情况下退回到登录页面
|
||||
case 401: throw new Error('请重新登录')
|
||||
// 根据需要添加其它判断
|
||||
default: throw new Error(`${response.data.msg}: ${response.config.url}`)
|
||||
case 401:
|
||||
errorMessage = '请重新登录'
|
||||
break
|
||||
default:
|
||||
errorMessage = response.data.msg || '请求失败'
|
||||
break
|
||||
}
|
||||
const businessError = new Error(`${errorMessage}: ${response.config.url}`)
|
||||
handleError(businessError)
|
||||
throw businessError
|
||||
},
|
||||
error => {
|
||||
const status = get(error, 'response.status')
|
||||
|
||||
@@ -7,6 +7,8 @@ export function getMenuAll (data) {
|
||||
url: urls + 'all',
|
||||
method: 'get',
|
||||
params: {
|
||||
method: 'system_settings_menu_configuration_menu_all',
|
||||
platform: 'background',
|
||||
...data
|
||||
}
|
||||
})
|
||||
|
||||
75
src/api/system-administration/role.js
Normal file
75
src/api/system-administration/role.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { request } from '@/api/_service'
|
||||
|
||||
const BASE = 'system_settings/user_management/role/'
|
||||
|
||||
function apiParams (method, data = {}) {
|
||||
return {
|
||||
method: `system_settings_user_management_role_${method}`,
|
||||
platform: 'background',
|
||||
...data
|
||||
}
|
||||
}
|
||||
|
||||
export function getRoleList (data) {
|
||||
return request({
|
||||
url: BASE + 'list',
|
||||
method: 'get',
|
||||
params: apiParams('list', data)
|
||||
})
|
||||
}
|
||||
|
||||
export function createRole (data) {
|
||||
return request({
|
||||
url: BASE + 'create',
|
||||
method: 'post',
|
||||
data: apiParams('create', data)
|
||||
})
|
||||
}
|
||||
|
||||
export function editRole (data) {
|
||||
return request({
|
||||
url: BASE + 'edit',
|
||||
method: 'put',
|
||||
data: apiParams('edit', data)
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRole (data) {
|
||||
return request({
|
||||
url: BASE + 'delete',
|
||||
method: 'delete',
|
||||
data: apiParams('delete', data)
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRoleStatus (data) {
|
||||
return request({
|
||||
url: BASE + 'update_status',
|
||||
method: 'put',
|
||||
data: apiParams('update_status', data)
|
||||
})
|
||||
}
|
||||
|
||||
export function giveRoleMenu (data) {
|
||||
return request({
|
||||
url: BASE + 'give',
|
||||
method: 'put',
|
||||
data: {
|
||||
method: 'system_settings_user_management_give_role_menu',
|
||||
platform: 'background',
|
||||
...data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getRoleMenu (data) {
|
||||
return request({
|
||||
url: BASE + 'menu',
|
||||
method: 'get',
|
||||
params: {
|
||||
method: 'system_settings_user_management_role_menu',
|
||||
platform: 'background',
|
||||
...data
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -105,17 +105,19 @@
|
||||
v-bind="colAttrs(col)"
|
||||
>
|
||||
<template #default="{ row, $index }">
|
||||
<span
|
||||
v-for="btn in visibleRowButtons"
|
||||
:key="btn.key"
|
||||
:style="btn.cssStyle"
|
||||
:class="{ 'action-btn--danger': btn.color === 'danger' }"
|
||||
class="action-btn"
|
||||
@click="btn.onClick(row, $index)"
|
||||
>
|
||||
<i v-if="btn.icon" :class="btn.icon" />
|
||||
{{ $t(btn.label) }}
|
||||
</span>
|
||||
<template v-for="btn in visibleRowButtons">
|
||||
<span
|
||||
v-if="btn.visible(row)"
|
||||
:key="btn.key"
|
||||
:style="btn.cssStyle"
|
||||
:class="{ 'action-btn--danger': btn.color === 'danger' }"
|
||||
class="action-btn"
|
||||
@click="btn.onClick(row, $index)"
|
||||
>
|
||||
<i v-if="btn.icon" :class="btn.icon" />
|
||||
{{ $t(btn.label) }}
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 4. 自定义插槽列:列内容或表头可由父组件自定义 -->
|
||||
|
||||
60
src/composables/useConfirmHandle.js
Normal file
60
src/composables/useConfirmHandle.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* $confirm + API 调用的标准包装 mixin
|
||||
*
|
||||
* 解决问题:$confirm 取消也会 reject,和 API 失败混在同一个 catch 里无法区分
|
||||
*
|
||||
* 调用方无需 try/catch,只需判断返回值:
|
||||
* - 返回 true → 用户取消,静默返回
|
||||
* - 返回 false → action 已执行(成功或失败),拦截器已处理 Message
|
||||
*
|
||||
* @example
|
||||
* import { confirmMixin } from '@/composables/useConfirmHandle'
|
||||
* export default {
|
||||
* mixins: [confirmMixin, i18nMixin('page.xxx')],
|
||||
* methods: {
|
||||
* async handleDelete (row) {
|
||||
* const cancelled = await this.$confirmAction(
|
||||
* { message: this.key('confirm_delete'), title: this.key('tip') },
|
||||
* () => deleteApi({ id: row.id })
|
||||
* )
|
||||
* if (cancelled) return
|
||||
* this.$message.success(this.$t(this.key('operation_success')))
|
||||
* this.fetchData()
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const confirmMixin = {
|
||||
methods: {
|
||||
/**
|
||||
* @param {Object} confirmOpts - { message, title, ...$confirm 其余参数 }
|
||||
* @param {Function} action - 确认后执行的 async 函数
|
||||
* @returns {Promise<boolean>} true = 已取消, false = 已执行
|
||||
*/
|
||||
async $confirmAction (confirmOpts, action) {
|
||||
try {
|
||||
await this.$confirm(
|
||||
confirmOpts.message ? this.$t(confirmOpts.message) : '',
|
||||
confirmOpts.title ? this.$t(confirmOpts.title) : '',
|
||||
{
|
||||
confirmButtonText: this.$t(confirmOpts.confirmButtonText || 'page.common.confirm'),
|
||||
cancelButtonText: this.$t(confirmOpts.cancelButtonText || 'page.common.cancel'),
|
||||
type: confirmOpts.type || 'warning',
|
||||
closeOnClickModal: confirmOpts.closeOnClickModal !== false
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await action()
|
||||
} catch {
|
||||
// 拦截器已弹出 Message
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default confirmMixin
|
||||
@@ -39,7 +39,8 @@ export function useTableButtons (options = {}, permissionCheck) {
|
||||
auth: btn.auth,
|
||||
confirm: btn.confirm || false,
|
||||
onClick: btn.onClick,
|
||||
hasPermission: btn.auth ? check(btn.auth) : true
|
||||
hasPermission: btn.auth ? check(btn.auth) : true,
|
||||
visible: btn.visible || (() => true)
|
||||
}))
|
||||
|
||||
return { toolbarButtons, rowButtons }
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"page": {
|
||||
"common": {
|
||||
"help": "Help",
|
||||
"use": "Apply"
|
||||
"use": "Apply",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"tip": "Tip"
|
||||
},
|
||||
"demo": {
|
||||
"playground": {
|
||||
@@ -45,6 +48,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_administration": {
|
||||
"user_management": {
|
||||
"role": {
|
||||
"search": "Search",
|
||||
"reset": "Reset",
|
||||
"index": "No.",
|
||||
"name": "Role Name",
|
||||
"status": "Status",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"description": "Description",
|
||||
"actions": "Actions",
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"add_title": "Add Role",
|
||||
"edit_title": "Edit Role",
|
||||
"enter_name": "Please enter role name",
|
||||
"enter_description": "Please enter description",
|
||||
"select_status": "Please select status",
|
||||
"select_rows_first": "Please select rows first",
|
||||
"operation_success": "Operation succeeded",
|
||||
"confirm_delete": "Are you sure to delete this role?",
|
||||
"length_limit": "Length should be 1 to 100 characters",
|
||||
"assign_permissions": "Assign Permissions",
|
||||
"permission_assignment": "Permission Assignment",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"tip": "Tip"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"login": {
|
||||
"time_is_most_precious": "Time is the most precious of all wealth",
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"page": {
|
||||
"common": {
|
||||
"help": "帮 助",
|
||||
"use": "使用"
|
||||
"use": "使用",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消",
|
||||
"tip": "提示"
|
||||
},
|
||||
"demo": {
|
||||
"playground": {
|
||||
@@ -45,6 +48,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_administration": {
|
||||
"user_management": {
|
||||
"role": {
|
||||
"search": "查询",
|
||||
"reset": "重置",
|
||||
"index": "序号",
|
||||
"name": "角色名称",
|
||||
"status": "状态",
|
||||
"enabled": "启用",
|
||||
"disabled": "禁用",
|
||||
"description": "描述",
|
||||
"actions": "操作",
|
||||
"add": "新 增",
|
||||
"edit": "编 辑",
|
||||
"delete": "删 除",
|
||||
"add_title": "新增角色",
|
||||
"edit_title": "编辑角色",
|
||||
"enter_name": "请输入角色名称",
|
||||
"enter_description": "请输入描述",
|
||||
"select_status": "请选择状态",
|
||||
"select_rows_first": "请先勾选要操作的数据",
|
||||
"operation_success": "操作成功",
|
||||
"confirm_delete": "确定要删除该角色吗?",
|
||||
"length_limit": "长度在 1 到 100 个字符",
|
||||
"assign_permissions": "分配权限",
|
||||
"permission_assignment": "权限分配",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消",
|
||||
"tip": "提示"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"login": {
|
||||
"time_is_most_precious": "时间是一切财富中最宝贵的财富",
|
||||
|
||||
@@ -13,6 +13,12 @@ export default {
|
||||
name: `${pre}index`,
|
||||
meta: { ...meta, title: '系统设置', root: '/system_settings' },
|
||||
component: _import('system/function/module-index')
|
||||
},
|
||||
{
|
||||
path: 'user_management/role',
|
||||
name: `${pre}user_management-role`,
|
||||
meta: { ...meta, cache: true, title: '角色' },
|
||||
component: _import('system-administration/user-management/role')
|
||||
}
|
||||
])('system_settings-')
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
import { useTableColumns } from '@/composables/useTableColumns'
|
||||
import { useTableButtons } from '@/composables/useTableButtons'
|
||||
import { i18nMixin } from '@/composables/useI18n'
|
||||
import { confirmMixin } from '@/composables/useConfirmHandle'
|
||||
import {
|
||||
getFactoryAreaList,
|
||||
createFactoryArea,
|
||||
@@ -82,7 +83,7 @@ import PageDialogForm from '@/components/page-dialog-form'
|
||||
export default {
|
||||
name: 'production-master-data-factory-area',
|
||||
components: { PageTable, PageDialogForm },
|
||||
mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],
|
||||
mixins: [i18nMixin('page.production_master_data.factory_model.factory_area'), confirmMixin],
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
@@ -195,8 +196,10 @@ export default {
|
||||
page_no: this.pagination.current,
|
||||
page_size: this.pagination.size
|
||||
})
|
||||
this.tableData = res.data || []
|
||||
this.pagination.total = res.count || 0
|
||||
const list = Array.isArray(res) ? res : (res.data || [])
|
||||
const total = Array.isArray(res) ? res.length : (res.count || 0)
|
||||
this.tableData = list
|
||||
this.pagination.total = total
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -261,27 +264,20 @@ export default {
|
||||
this.resetForm()
|
||||
},
|
||||
async handleDelete (row) {
|
||||
try {
|
||||
await this.$confirm(
|
||||
this.$t(this.key('confirm_delete')),
|
||||
this.$t(this.key('tip')),
|
||||
{
|
||||
confirmButtonText: this.$t(this.key('confirm')),
|
||||
cancelButtonText: this.$t(this.key('cancel')),
|
||||
type: 'warning',
|
||||
closeOnClickModal: false
|
||||
}
|
||||
)
|
||||
await deleteFactoryArea({ id: [row.id] })
|
||||
this.$message.success(this.$t(this.key('operation_success')))
|
||||
this.pagination.current = Math.min(
|
||||
this.pagination.current,
|
||||
Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
|
||||
)
|
||||
this.fetchData()
|
||||
} catch (e) {
|
||||
// 取消删除 / 请求失败时不处理
|
||||
}
|
||||
const cancelled = await this.$confirmAction(
|
||||
{
|
||||
message: this.key('confirm_delete'),
|
||||
title: this.key('tip')
|
||||
},
|
||||
() => deleteFactoryArea({ id: [row.id] })
|
||||
)
|
||||
if (cancelled) return
|
||||
this.$message.success(this.$t(this.key('operation_success')))
|
||||
this.pagination.current = Math.min(
|
||||
this.pagination.current,
|
||||
Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
|
||||
)
|
||||
this.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:visible.sync="visibleProxy"
|
||||
:title="$t(title)"
|
||||
:size="width"
|
||||
:close-on-click-modal="false"
|
||||
direction="rtl"
|
||||
@close="onClose"
|
||||
>
|
||||
<div class="perm-drawer__body" v-loading="treeLoading">
|
||||
<el-tree
|
||||
v-if="treeReady"
|
||||
ref="permTree"
|
||||
node-key="menu_id"
|
||||
:data="treeData"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
:default-expand-all="false"
|
||||
:expand-on-click-node="false"
|
||||
:check-strictly="true"
|
||||
show-checkbox
|
||||
@check="onTreeCheck"
|
||||
>
|
||||
<span slot-scope="{ node, data }" class="perm-drawer__tree-node">
|
||||
<span :class="{ 'perm-drawer__tree-node--disabled': !data.status }">
|
||||
<i v-if="data.icon" :class="`fa fa-${data.icon}`" />
|
||||
<i v-else-if="data.children" :class="`el-icon-${node.expanded ? 'folder-opened' : 'folder'}`" />
|
||||
<i v-else class="el-icon-document" />
|
||||
{{ $t(data.name) }}
|
||||
</span>
|
||||
</span>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<div class="perm-drawer__footer">
|
||||
<el-button size="mini" @click="onCancel">
|
||||
{{ $t(cancelText) }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" :loading="submitting" @click="onSubmit">
|
||||
{{ $t(confirmText) }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { i18nMixin } from '@/composables/useI18n'
|
||||
import util from '@/libs/util'
|
||||
import { getMenuAll } from '@/api/menu'
|
||||
import { giveRoleMenu, getRoleMenu } from '@/api/system-administration/role'
|
||||
|
||||
export default {
|
||||
name: 'RolePermDrawer',
|
||||
mixins: [i18nMixin('page.system_administration.user_management.role')],
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
roleId: { type: Number, default: 0 },
|
||||
title: { type: String, default: '' },
|
||||
confirmText: { type: String, default: '' },
|
||||
cancelText: { type: String, default: '' },
|
||||
width: { type: String, default: '360px' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
treeLoading: false,
|
||||
submitting: false,
|
||||
treeReady: false,
|
||||
treeData: [],
|
||||
checkedMenuIds: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
visibleProxy: {
|
||||
get () { return this.visible },
|
||||
set (val) { this.$emit('update:visible', val) }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible (val) {
|
||||
if (val) { this.loadData() }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData () {
|
||||
this.treeReady = false
|
||||
this.treeLoading = true
|
||||
try {
|
||||
const [menuRes, roleMenuRes] = await Promise.all([
|
||||
getMenuAll()
|
||||
])
|
||||
const menuData = Array.isArray(menuRes) ? menuRes : (menuRes.data || [])
|
||||
const roleData = Array.isArray(roleMenuRes) ? roleMenuRes : (roleMenuRes.data || [])
|
||||
this.checkedMenuIds = roleData.map(item => item.menu_id)
|
||||
this.treeData = util.formatDataToTree(menuData)
|
||||
this.treeReady = true
|
||||
await this.$nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
this.$refs.permTree.setCheckedKeys(this.checkedMenuIds)
|
||||
} finally {
|
||||
this.treeLoading = false
|
||||
}
|
||||
},
|
||||
onTreeCheck () {},
|
||||
onClose () {
|
||||
this.treeReady = false
|
||||
this.treeData = []
|
||||
this.checkedMenuIds = []
|
||||
},
|
||||
onCancel () {
|
||||
this.visibleProxy = false
|
||||
},
|
||||
async onSubmit () {
|
||||
this.submitting = true
|
||||
try {
|
||||
const menuIds = this.$refs.permTree.getCheckedKeys()
|
||||
await giveRoleMenu({ role_id: this.roleId, role_menu: menuIds })
|
||||
this.$message.success(this.$t(this.key('operation_success')))
|
||||
this.visibleProxy = false
|
||||
this.$emit('saved')
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.perm-drawer__body {
|
||||
height: calc(100vh - 140px);
|
||||
overflow-y: auto;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.perm-drawer__footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
}
|
||||
.perm-drawer__tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.perm-drawer__tree-node--disabled {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
365
src/views/system-administration/user-management/role/index.vue
Normal file
365
src/views/system-administration/user-management/role/index.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<d2-container>
|
||||
<template #header>
|
||||
<div class="search-bar">
|
||||
<el-form :inline="true" size="mini">
|
||||
<el-form-item :label="$t(key('name'))">
|
||||
<el-input
|
||||
v-model="search.name"
|
||||
:placeholder="$t(key('enter_name'))"
|
||||
clearable
|
||||
style="width:200px"
|
||||
@keyup.enter.native="onSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t(key('status'))">
|
||||
<el-select
|
||||
v-model="search.status"
|
||||
:placeholder="$t(key('select_status'))"
|
||||
clearable
|
||||
style="width:150px"
|
||||
>
|
||||
<el-option :value="'1'" :label="$t(key('enabled'))" />
|
||||
<el-option :value="'0'" :label="$t(key('disabled'))" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="onSearch">
|
||||
{{ $t(key('search')) }}
|
||||
</el-button>
|
||||
<el-button icon="el-icon-refresh" @click="onReset">
|
||||
{{ $t(key('reset')) }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<page-table
|
||||
ref="pageTable"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:toolbar-buttons="toolbarButtons"
|
||||
:row-buttons="rowButtons"
|
||||
:pagination="pagination"
|
||||
:table-attrs="{ selectable: row => row.system === 0 }"
|
||||
auto-height
|
||||
@page-change="onPageChange"
|
||||
@selection-change="onSelect"
|
||||
>
|
||||
<template #col-status="{ row }">
|
||||
<span v-if="row.status === 1" style="color: #67c23a;">
|
||||
<i class="el-icon-circle-check" />
|
||||
{{ $t(key('enabled')) }}
|
||||
</span>
|
||||
<span v-else style="color: #909399;">
|
||||
<i class="el-icon-circle-close" />
|
||||
{{ $t(key('disabled')) }}
|
||||
</span>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<page-dialog-form
|
||||
ref="dialogForm"
|
||||
:visible.sync="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="35%"
|
||||
:form-cols="formCols"
|
||||
:form-data="formData"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
:submitting="submitting"
|
||||
:confirm-text="key('confirm')"
|
||||
:cancel-text="key('cancel')"
|
||||
@submit="onDialogSubmit"
|
||||
@close="onDialogClose"
|
||||
/>
|
||||
|
||||
<role-perm-drawer
|
||||
:visible.sync="permVisible"
|
||||
:role-id="permRole.id"
|
||||
:title="key('assign_permissions')"
|
||||
:confirm-text="key('confirm')"
|
||||
:cancel-text="key('cancel')"
|
||||
width="360px"
|
||||
@saved="fetchData"
|
||||
/>
|
||||
</d2-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useTableColumns } from '@/composables/useTableColumns'
|
||||
import { useTableButtons } from '@/composables/useTableButtons'
|
||||
import { i18nMixin } from '@/composables/useI18n'
|
||||
import { confirmMixin } from '@/composables/useConfirmHandle'
|
||||
import {
|
||||
getRoleList,
|
||||
createRole,
|
||||
editRole,
|
||||
deleteRole,
|
||||
updateRoleStatus
|
||||
} from '@/api/system-administration/role'
|
||||
import PageTable from '@/components/page-table'
|
||||
import PageDialogForm from '@/components/page-dialog-form'
|
||||
import RolePermDrawer from './components/PermDrawer/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'system-administration-role',
|
||||
components: { PageTable, PageDialogForm, RolePermDrawer },
|
||||
mixins: [i18nMixin('page.system_administration.user_management.role'), confirmMixin],
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
submitting: false,
|
||||
tableData: [],
|
||||
selectedRows: [],
|
||||
dialogVisible: false,
|
||||
dialogTitle: '',
|
||||
editId: '',
|
||||
handleType: 'create',
|
||||
search: { name: '', status: '' },
|
||||
pagination: { current: 1, size: 10, total: 0 },
|
||||
formData: { name: '', description: '', status: '1' },
|
||||
rules: {
|
||||
name: [
|
||||
{ required: true, message: this.key('enter_name'), trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: this.key('length_limit'), trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
columns: [],
|
||||
toolbarButtons: [],
|
||||
rowButtons: [],
|
||||
formCols: [
|
||||
[
|
||||
{
|
||||
type: 'input',
|
||||
prop: 'name',
|
||||
label: this.key('name'),
|
||||
placeholder: this.key('enter_name'),
|
||||
clearable: true,
|
||||
style: { width: '90%' }
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'input',
|
||||
inputType: 'textarea',
|
||||
prop: 'description',
|
||||
autosize: { minRows: 2, maxRows: 6 },
|
||||
label: this.key('description'),
|
||||
placeholder: this.key('enter_description'),
|
||||
clearable: true,
|
||||
style: { width: '90%' }
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'select',
|
||||
prop: 'status',
|
||||
label: this.key('status'),
|
||||
clearable: false,
|
||||
style: { width: '90%' },
|
||||
options: [
|
||||
{ value: '1', label: this.$t(this.key('enabled')) },
|
||||
{ value: '0', label: this.$t(this.key('disabled')) }
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
permVisible: false,
|
||||
permRole: {}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.columns = useTableColumns([
|
||||
{ prop: 'sort', label: this.key('index'), width: 80 },
|
||||
{ prop: 'name', label: this.key('name'), minWidth: 120 },
|
||||
{ prop: 'status', label: this.key('status'), slot: 'status', width: 100 },
|
||||
{ prop: 'description', label: this.key('description') },
|
||||
{ prop: '_actions', label: this.key('actions'), width: 220, fixed: 'right' }
|
||||
])
|
||||
const btns = useTableButtons({
|
||||
toolbar: [
|
||||
{
|
||||
key: 'add',
|
||||
label: this.key('add'),
|
||||
icon: 'el-icon-plus',
|
||||
type: 'primary',
|
||||
auth: '/system_settings/user_management/role/create',
|
||||
onClick: this.openAdd
|
||||
},
|
||||
{
|
||||
key: 'enable',
|
||||
label: this.key('enabled'),
|
||||
icon: 'el-icon-check',
|
||||
type: 'success',
|
||||
auth: '/system_settings/user_management/role/enable',
|
||||
onClick: () => this.batchUpdateStatus(1)
|
||||
},
|
||||
{
|
||||
key: 'disable',
|
||||
label: this.key('disabled'),
|
||||
icon: 'el-icon-close',
|
||||
type: 'warning',
|
||||
auth: '/system_settings/user_management/role/disable',
|
||||
onClick: () => this.batchUpdateStatus(0)
|
||||
}
|
||||
],
|
||||
row: [
|
||||
{
|
||||
key: 'edit',
|
||||
label: this.key('edit'),
|
||||
icon: 'el-icon-edit',
|
||||
auth: '/system_settings/user_management/role/edit',
|
||||
visible: row => row.system !== 1,
|
||||
onClick: this.openEdit
|
||||
},
|
||||
{
|
||||
key: 'assign',
|
||||
label: this.key('assign_permissions'),
|
||||
icon: 'el-icon-edit-outline',
|
||||
auth: '/system_settings/user_management/role/role_give',
|
||||
visible: row => row.system !== 1,
|
||||
onClick: this.openPermDialog
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: this.key('delete'),
|
||||
icon: 'el-icon-delete',
|
||||
color: 'danger',
|
||||
auth: '/system_settings/user_management/role/delete',
|
||||
visible: row => row.system !== 1,
|
||||
onClick: this.handleDelete
|
||||
}
|
||||
]
|
||||
}, this.$permission)
|
||||
this.toolbarButtons = btns.toolbarButtons
|
||||
this.rowButtons = btns.rowButtons
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData () {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await getRoleList({
|
||||
...this.search,
|
||||
page_no: this.pagination.current,
|
||||
page_size: this.pagination.size
|
||||
})
|
||||
const list = Array.isArray(res) ? res : (res.data || [])
|
||||
const total = Array.isArray(res) ? res.length : (res.count || 0)
|
||||
this.tableData = list
|
||||
this.pagination.total = total
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
onSearch () {
|
||||
this.pagination.current = 1
|
||||
this.fetchData()
|
||||
},
|
||||
onReset () {
|
||||
this.search = { name: '', status: '' }
|
||||
this.pagination.current = 1
|
||||
this.fetchData()
|
||||
},
|
||||
onPageChange (page) {
|
||||
this.pagination.current = page.current
|
||||
this.pagination.size = page.size
|
||||
this.fetchData()
|
||||
},
|
||||
onSelect (rows) {
|
||||
this.selectedRows = rows
|
||||
},
|
||||
resetForm () {
|
||||
this.formData = { name: '', description: '', status: '1' }
|
||||
this.editId = ''
|
||||
},
|
||||
openAdd () {
|
||||
this.handleType = 'create'
|
||||
this.dialogTitle = this.key('add_title')
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dialogForm && this.$refs.dialogForm.reset()
|
||||
this.resetForm()
|
||||
this.dialogVisible = true
|
||||
})
|
||||
},
|
||||
openEdit (row) {
|
||||
this.handleType = 'edit'
|
||||
this.dialogTitle = this.key('edit_title')
|
||||
this.editId = row.id
|
||||
this.formData = {
|
||||
name: row.name,
|
||||
description: row.description || '',
|
||||
status: String(row.status)
|
||||
}
|
||||
this.dialogVisible = true
|
||||
},
|
||||
async onDialogSubmit () {
|
||||
this.submitting = true
|
||||
try {
|
||||
if (this.handleType === 'create') {
|
||||
await createRole(this.formData)
|
||||
} else {
|
||||
await editRole({ ...this.formData, id: this.editId })
|
||||
}
|
||||
this.$message.success(this.$t(this.key('operation_success')))
|
||||
this.dialogVisible = false
|
||||
this.fetchData()
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
},
|
||||
onDialogClose () {
|
||||
this.resetForm()
|
||||
},
|
||||
async batchUpdateStatus (status) {
|
||||
const rows = this.selectedRows.filter(row => row.system === 0)
|
||||
if (rows.length === 0) {
|
||||
this.$message.warning(this.$t(this.key('select_rows_first')))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await updateRoleStatus({
|
||||
ids: rows.map(row => row.id),
|
||||
status
|
||||
})
|
||||
this.$message.success(this.$t(this.key('operation_success')))
|
||||
this.fetchData()
|
||||
} catch {
|
||||
// 拦截器已处理
|
||||
}
|
||||
},
|
||||
async handleDelete (row) {
|
||||
const cancelled = await this.$confirmAction(
|
||||
{
|
||||
message: this.key('confirm_delete'),
|
||||
title: this.key('tip')
|
||||
},
|
||||
() => deleteRole({ id: [row.id] })
|
||||
)
|
||||
if (cancelled) return
|
||||
this.$message.success(this.$t(this.key('operation_success')))
|
||||
this.pagination.current = Math.min(
|
||||
this.pagination.current,
|
||||
Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
|
||||
)
|
||||
this.fetchData()
|
||||
},
|
||||
async openPermDialog (row) {
|
||||
this.permRole = row
|
||||
this.permVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-bar {
|
||||
padding: 10px 0;
|
||||
}
|
||||
/deep/ .el-form-item--mini.el-form-item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user