feat: 完成系统管理模块功能迭代

新增用户、菜单、日志、问题帮助等业务模块,优化角色权限分配功能,新增依赖包与全局组件
This commit is contained in:
sheng
2026-05-29 18:12:54 +08:00
parent a61036e5dc
commit 20a821ba32
28 changed files with 5512 additions and 39984 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,9 @@
8. [API 文件写法](#8-api-文件写法)
9. [旧代码迁移对照](#9-旧代码迁移对照)
10. [接口请求错误处理规范](#10-接口请求错误处理规范)
11. [常见问题排查](#11-常见问题排查)
11. [依赖安装规范](#11-依赖安装规范)
12. [常见问题排查](#12-常见问题排查)
13. [特殊弹出框组件规范](#13-特殊弹出框组件规范)
---
@@ -860,6 +862,125 @@ useTableButtons({
推荐使用公共 key `page.common.help`(模板中用 `$t(ckey('help'))`)。不传 `help-url` 则不显示。
### 场景 8折叠式搜索区搜索条件过多时
当搜索条件超过一行时,用 `v-show` + `searchExpanded` 控制额外条件的显隐,避免 header 区域过长。
**数据定义(`data()` 中)**
```js
data () {
return {
searchExpanded: false, // 控制展开/收起
// ...其他数据
}
}
```
**模板结构**
```vue
<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form :inline="true" ref="searchFormRef" size="mini" @submit.native.prevent>
<!-- 常用条件始终可见 -->
<el-form-item :label="$t(key('ip'))">
<el-input v-model="search.ip" :placeholder="$t(key('placeholder_ip'))"
clearable style="width:160px" @keyup.enter.native="onSearch" />
</el-form-item>
<el-form-item :label="$t(key('status'))">
<el-select v-model="search.status" :placeholder="$t(key('placeholder_status'))"
clearable style="width:120px">
<el-option :value="200" :label="$t(key('success'))" />
<el-option :value="4001" :label="$t(key('failure'))" />
</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-button
v-if="!searchExpanded"
type="text"
icon="el-icon-arrow-down"
@click="searchExpanded = true"
>
{{ $t(key('expand')) }}
</el-button>
<el-button
v-else
type="text"
icon="el-icon-arrow-up"
@click="searchExpanded = false"
>
{{ $t(key('collapse')) }}
</el-button>
</el-form-item>
<!-- 扩展条件v-show 控制显隐 -->
<div v-show="searchExpanded" class="search-bar__extra">
<el-form-item :label="$t(key('tray_number'))">
<el-input v-model="search.tray" :placeholder="$t(key('placeholder_tray_no'))"
clearable style="width:160px" @keyup.enter.native="onSearch" />
</el-form-item>
<el-form-item :label="$t(key('process_code'))">
<el-input v-model="search.process_code"
:placeholder="$t(key('placeholder_process_code'))"
clearable style="width:160px" @keyup.enter.native="onSearch" />
</el-form-item>
<el-form-item :label="$t(key('create_time'))">
<el-date-picker v-model="search.time" type="datetimerange"
:placeholder="$t(key('placeholder_create_time'))"
range-separator="-" start-placeholder="" end-placeholder=""
value-format="yyyy-MM-dd HH:mm:ss" style="width:340px" />
</el-form-item>
</div>
</el-form>
</div>
</template>
<!-- ... -->
</d2-container>
</template>
```
**样式**
```css
.search-bar {
padding: 10px 0;
}
.search-bar .el-form-item--mini.el-form-item {
margin-bottom: 4px;
}
.search-bar__extra {
display: inline-block;
width: 100%;
}
```
**i18n 附加 key**
| key | 中文 | English |
|-----|------|---------|
| `expand` | 展开更多 | Expand |
| `collapse` | 收起 | Collapse |
**设计要点**
1. 常用高频筛选条件放在第一行始终可见(如 IP、状态
2. 低频条件用 `v-show="searchExpanded"` 包裹,默认隐藏
3. 展开/收起按钮放在操作按钮组同一行,使用 `type="text"` + 箭头图标
4. 折叠时重置不展开,重置只清空 `search` 数据,不改变 `searchExpanded` 状态
5. 适用于只读类页面(如日志查看),无需选中框和批量操作
**完整参考**[接口日志页](file:///d:/code/mes/mes-ui/src/views/system-administration/system-utilities/api-logs/index.vue)
---
## 7. 路由配置
@@ -1172,7 +1293,37 @@ this.fetchData()
---
## 11. 常见问题排查
## 11. 依赖安装规范
### 包管理器
本项目使用 **pnpm** 作为包管理器,**禁止使用 npm 或 yarn**。
### 安装新依赖
当页面需要额外的第三方库时(如 `mavon-editor``vue-json-tree-view` 等),使用:
```bash
pnpm add <package-name>
```
### 常见页面依赖速查
| 依赖包 | 用途 | 安装命令 |
|--------|------|---------|
| `mavon-editor` | Markdown 编辑器(问题帮助、文档编辑) | `pnpm add mavon-editor` |
| `vue-json-tree-view` | JSON 树形展示(日志查看响应) | 已预装 |
| `marked` + `highlight.js` | Markdown 渲染d2-markdown 组件依赖) | 已预装 |
### 注意事项
1. 安装前检查 `package.json` 是否已有该依赖(`vue-json-tree-view``marked` 等项目已预装)
2. 安装后确认版本兼容性,特别是 Vue 2.x 项目不要安装仅支持 Vue 3 的包
3. 若因网络问题安装失败,检查 registry 配置(本项目使用 `npmmirror.com` 镜像)
---
## 12. 常见问题排查
### Q1弹框打开后不显示内容
@@ -1197,3 +1348,181 @@ this.fetchData()
### Q6表单验证错误提示显示为原始 i18n key
`page-dialog-form` 会通过 `translatedRules` 计算属性自动翻译验证规则的 `message` 字段。确认传入的 `rules.message` 使用了 `this.key()` 传入完整 key。
---
## 13. 特殊弹出框组件规范
### 适用范围
当页面需要**超出 `page-dialog-form` 能力的弹出框**时如权限分配树、多步骤向导、ifream 嵌入、复杂联动表单等),**禁止在 `index.vue` 中直接堆砌内联代码**,必须抽离为独立组件。
### 目录结构标准
```
src/views/{模块}/{功能}/
├── index.vue ← 主页面,只负责引入和组装
└── components/ ← 所有额外弹框统一放这里
├── PermDrawer/ ← 示例:权限分配抽屉
│ └── index.vue
├── ImportDialog/ ← 示例:批量导入
│ └── index.vue
└── DetailDrawer/ ← 示例:详情抽屉
└── index.vue
```
### 命名规范
| 规则 | 说明 | 示例 |
|------|------|------|
| 组件文件夹 | **英文 PascalCase**,描述功能 | `PermDrawer`(权限抽屉)、`ImportDialog`(导入弹框)、`DetailDrawer`(详情抽屉) |
| 组件入口文件 | 统一 `index.vue` | `PermDrawer/index.vue` |
| 组件注册名 | 英文 PascalCase 或 kebab-case 前缀 | `RolePermDrawer``role-perm-drawer` |
| 禁止 | 中文名、拼音、模糊命名 | ❌ `权限分配/`、❌ `quanxian/`、❌ `Popup/` |
### 主页面用法(`index.vue`
```vue
<template>
<d2-container>
<!-- 搜索区 -->
<template #header>...</template>
<!-- 标准 CRUD 表格 -->
<page-table ... />
<!-- 标准新增/编辑弹框 -->
<page-dialog-form ... />
<!-- 特殊弹框仅引用 + props简洁清晰 -->
<role-perm-drawer
:visible.sync="permVisible"
:role="permRole"
:title="key('assign_permissions')"
:confirm-text="key('confirm')"
:cancel-text="key('cancel')"
@saved="fetchData"
/>
</d2-container>
</template>
<script>
import RolePermDrawer from './components/PermDrawer/index.vue'
export default {
components: { ..., RolePermDrawer },
data () {
return {
permVisible: false,
permRole: {} // 只存当前操作行,其余逻辑全在子组件
}
},
methods: {
openPermDialog (row) {
this.permRole = row
this.permVisible = true
}
}
}
</script>
```
### 子组件模板(`components/PermDrawer/index.vue`
```vue
<template>
<el-drawer
:visible.sync="visibleProxy"
:title="$t(title)"
:size="width"
:close-on-click-modal="false"
direction="rtl"
@close="onClose"
>
<div v-loading="loading">
<!-- 业务内容 el-treeel-transfer -->
</div>
<div class="my-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'
export default {
name: 'RolePermDrawer',
mixins: [i18nMixin('page.xxx.xxx.xxx')],
props: {
visible: Boolean, // 用 .sync 双向绑定
title: String, // 弹框标题i18n key
confirmText: String, // 确定按钮文本i18n key
cancelText: String, // 取消按钮文本i18n key
width: { type: String, default: '360px' }
// 业务 props 按需添加,如 role、data 等
},
data () {
return {
loading: false,
submitting: false
}
},
computed: {
visibleProxy: {
get () { return this.visible },
set (val) { this.$emit('update:visible', val) }
}
},
watch: {
visible (val) {
if (val) { this.init() } // 打开时加载数据
}
},
methods: {
init () { /* 加载数据 */ },
onSubmit () {
this.submitting = true
try {
// 提交后
this.$emit('saved') // 通知父组件刷新
this.visibleProxy = false
} finally {
this.submitting = false
}
},
onCancel () { this.visibleProxy = false },
onClose () { /* 清理状态 */ }
}
}
</script>
```
### 组件通信约定
| 方向 | 方式 | 说明 |
|------|------|------|
| 父 → 子 | `props` | 传递 `visible`sync、业务数据对象、i18n key |
| 子 → 父 | `$emit` | `@saved` 通知父组件刷新表格,`@closed` 通知关闭完成 |
| 避免 | `$parent` / `$refs` | 禁止子组件通过 `$refs.parent.xxx` 访问父组件方法 |
### 判断标准:何时需要独立组件?
| 场景 | 使用方案 |
|------|---------|
| 新增/编辑表单input + select + textarea | `page-dialog-form` 即可 |
| 权限分配树 | **独立组件**`components/PermDrawer/` |
| 批量导入/导出向导 | **独立组件**`components/ImportDialog/` |
| 关联数据选择器(多选表格) | **独立组件**`components/SelectorDialog/` |
| 详情查看(非编辑) | **独立组件**`components/DetailDrawer/` |
| 复杂多步骤流程 | **独立组件**`components/FlowWizard/` |
### 实际案例
参考完整实现:
- 权限分配抽屉:[`src/views/system-administration/user-management/role/components/PermDrawer/index.vue`](file:///d:/code/mes/mes-ui/src/views/system-administration/user-management/role/components/PermDrawer/index.vue)
- 主页面引用方式:[`src/views/system-administration/user-management/role/index.vue`](file:///d:/code/mes/mes-ui/src/views/system-administration/user-management/role/index.vue#L79-L87)

View File

@@ -15,6 +15,56 @@
- 参考文档:[表格组件使用说明.md](file:///d:/code/mes/mes-ui/docs/表格组件使用说明.md)
- 参考示例:`src/views/production-master-data/factory-model/factory-area/index.vue`
#### 1.1 特殊弹出框组件迁移(重要)
当旧页面中存在**超出 `page-dialog-form` 能力的弹出框**时(权限分配树、批量导入、关联选择器、详情查看、多步骤向导等),必须遵循以下流程:
##### 第一步:分析旧代码
- **仔细阅读旧项目的弹出框代码**理解其数据结构、交互逻辑、API 调用
- 关注旧代码的数据来源路径(如 `row.menu_admin` 直接从列表获取、还是额外调 API
- 记录旧的参数名、method 名,确保迁移时不遗漏
##### 第二步:提出优化方案
迁移时**不能简单照搬旧代码**,必须结合新版能力提出优化:
| 旧写法 | 优化方向 |
|--------|---------|
| `el-dialog` 内联在页面中 | 抽离为独立组件 → `components/` |
| `sct-base-dialog` + 内联表单 | 独立组件 + `el-drawer`(抽屉式体验更佳) |
| 额外调 API 获取数据(如行数据已包含) | 直接从 `row.xxx` 取值,减少请求 |
| `setTimeout` 硬编码延迟 | 提出来讨论,结合 `v-if` + `$nextTick` 优化 |
| `$parent` / `$refs` 跨组件通信 | 改为 `props` + `$emit` |
示例:角色权限分配迁移优化方案
```
旧方案 优化方案
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
el-dialog 内联 42 行代码 → 独立 PermDrawer 组件components/PermDrawer/index.vue
getRoleMenu API 额外请求 → 直接从 row.menu_admin JSON.parse 获取(列表自带)
setTimeout 1000ms 关闭 → 保留(树渲染需要等待),但抽取到子组件内部
索引页 data 6 个 perm 字段 → 仅 2 个permVisible + permRole
```
##### 第三步:按标准实现
参考 [表格组件使用说明.md 第 13 节](file:///d:/code/mes/mes-ui/docs/表格组件使用说明.md#13-特殊弹出框组件规范),严格遵循:
| 规则 | 说明 |
|------|------|
| 目录位置 | `src/views/{模块}/{功能}/components/{英文PascalCase}/index.vue` |
| 文件夹命名 | 英文 PascalCase描述功能`PermDrawer``ImportDialog` |
| 组件通信 | 父→子 `props`(含 `.sync`),子→父 `$emit('saved')`,禁止 `$parent` / `$refs` |
| 主页面数据 | 只存 `visible` Boolean + 业务对象,其余状态在子组件内部 |
##### 第四步:完整案例
参考角色权限分配抽屉的完整迁移:
- 旧代码:`D:\code\company\SCTMES_MES_V5\vue-app\src\views\system_settings\user_management\role\components\PageMain\index.vue``dialogVisibleGive` + `el-tree`
- 新代码:[`src/views/system-administration/user-management/role/components/PermDrawer/index.vue`](file:///d:/code/mes/mes-ui/src/views/system-administration/user-management/role/components/PermDrawer/index.vue)
- 主页面引用:[`src/views/system-administration/user-management/role/index.vue`](file:///d:/code/mes/mes-ui/src/views/system-administration/user-management/role/index.vue#L79-L87)
---
#### 2. i18n 国际化(重要)
- 使用 `mixins: [i18nMixin('完整i18n前缀')]`,不要在每个页面手动定义 `T` 常量和 `tkey()` 方法
- `data()` 中用 `this.key('xxx')` 传完整 i18n key**不要用 `k()` 提前翻译**(翻译由 page-table / page-dialog-form 内部处理,切换语言自动响应)
@@ -64,6 +114,7 @@
| `page.system_settings.menu_configuration.menu` | `page.system_administration.menu_management.menu_configuration` |
| `page.system_settings.system_assistant.operate_log` | `page.system_administration.system_utilities.operation_logs` |
| `page.system_settings.system_assistant.api_log` | `page.system_administration.system_utilities.api_logs` |
| `page.system_settings.system_assistant.problem_help` | `page.system_administration.system_utilities.problem_help` |
| `page.system_settings.system_monitoring.system.login` | `page.system_administration.system_monitoring.login` |
| `page.production_configuration.matetial_model.*` | `page.production_master_data.material_model.*` |
| `page.planning_production.production_batch_management.batch` | `page.planning_production.batch_management.batch_list` |
@@ -90,6 +141,16 @@
- **Columns**`useTableColumns([...])`
- **Methods**`fetchData / onSearch / onReset / onPageChange / openAdd / openEdit / onDialogSubmit / handleDelete`
- **i18n key 模板**(每个页面至少要有这些):`search / reset / add / edit / delete / operation / add_title / edit_title / code / name / remark / enter_code / enter_name / remark_length / operation_success / confirm / cancel / tip / confirm_delete`
- **搜索条件过多时**参考 [表格组件使用说明.md - 场景 8](file:///d:/code/mes/mes-ui/docs/表格组件使用说明.md#场景-8折叠式搜索区搜索条件过多时),使用 `searchExpanded` + `v-show` 实现折叠式搜索区,需额外添加 i18n key`expand`(展开更多/Expand、`collapse`(收起/Collapse
#### 6.1 依赖安装
当迁移涉及新的第三方库时:
1. **包管理器**:本项目使用 **pnpm**,安装命令为 `pnpm add <package-name>`
2. **安装前检查**:先在 `package.json` 中确认依赖是否已存在,避免重复安装
3. **版本兼容**:确保安装的包支持 Vue 2.x本项目为 Vue 2.7
4. **参考标准**[表格组件使用说明.md - 第 11 节](file:///d:/code/mes/mes-ui/docs/表格组件使用说明.md#11-依赖安装规范)
---

39947
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,7 @@
"lodash": "^4.17.21",
"lowdb": "^1.0.0",
"marked": "^2.1.3",
"mavon-editor": "^2.10.4",
"nprogress": "^0.2.0",
"qs": "^6.11.0",
"quill": "^1.3.7",

42
pnpm-lock.yaml generated
View File

@@ -68,6 +68,9 @@ importers:
marked:
specifier: ^2.1.3
version: 2.1.3
mavon-editor:
specifier: ^2.10.4
version: 2.10.4
nprogress:
specifier: ^0.2.0
version: 0.2.0
@@ -2640,6 +2643,9 @@ packages:
engines: {node: '>=4'}
hasBin: true
cssfilter@0.0.10:
resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==}
cssnano-preset-default@4.0.8:
resolution: {integrity: sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==}
engines: {node: '>=6.9.0'}
@@ -4492,9 +4498,6 @@ packages:
json-parse-better-errors@1.0.2:
resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -4590,9 +4593,6 @@ packages:
resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==}
engines: {node: '>= 0.8.0'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
load-json-file@4.0.0:
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
engines: {node: '>=4'}
@@ -4726,6 +4726,9 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mavon-editor@2.10.4:
resolution: {integrity: sha512-CFsBLkgt/KZBDg+SJYe2fyYv4zClY149PiwpH0rDAiiP4ae1XNs0GC8nBsoTeipsHcebDLN1QMkt3bUsnMDjQw==}
md5.js@1.3.5:
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
@@ -7115,6 +7118,11 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xss@1.0.15:
resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==}
engines: {node: '>= 0.10.0'}
hasBin: true
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -10346,6 +10354,8 @@ snapshots:
cssesc@3.0.0: {}
cssfilter@0.0.10: {}
cssnano-preset-default@4.0.8:
dependencies:
css-declaration-sorter: 4.0.1
@@ -12725,8 +12735,6 @@ snapshots:
json-parse-better-errors@1.0.2: {}
json-parse-even-better-errors@2.3.1: {}
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0:
@@ -12820,8 +12828,6 @@ snapshots:
prelude-ls: 1.1.2
type-check: 0.3.2
lines-and-columns@1.2.4: {}
load-json-file@4.0.0:
dependencies:
graceful-fs: 4.2.11
@@ -12953,6 +12959,10 @@ snapshots:
math-intrinsics@1.1.0: {}
mavon-editor@2.10.4:
dependencies:
xss: 1.0.15
md5.js@1.3.5:
dependencies:
hash-base: 3.0.5
@@ -13521,12 +13531,7 @@ snapshots:
error-ex: 1.3.4
json-parse-better-errors: 1.0.2
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.29.0
error-ex: 1.3.4
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse-json@5.2.0: {}
parse-passwd@1.0.0: {}
@@ -15835,6 +15840,11 @@ snapshots:
xmlchars@2.2.0: {}
xss@1.0.15:
dependencies:
commander: 2.20.3
cssfilter: 0.0.10
xtend@4.0.2: {}
y18n@4.0.3: {}

View File

@@ -0,0 +1,15 @@
import { request } from '@/api/_service'
const BASE = 'system_settings/system_assistant/interface_log/'
export function getInterfaceLogList (data) {
return request({
url: BASE + 'list',
method: 'get',
params: {
method: 'system_settings_system_assistant_interface_log_list',
platform: 'background',
...data
}
})
}

View File

@@ -0,0 +1,67 @@
import { request } from '@/api/_service'
const BASE = 'system_settings/menu_configuration/menu/'
function apiParams (method, data = {}) {
return {
method: `system_settings_menu_configuration_menu_${method}`,
platform: 'background',
...data
}
}
export function getMenuAll (data) {
return request({
url: BASE + 'all',
method: 'get',
params: apiParams('all', data)
})
}
export function getMenuList (data) {
return request({
url: BASE + 'list',
method: 'get',
params: apiParams('list', data)
})
}
export function createMenu (data) {
return request({
url: BASE + 'create',
method: 'post',
data: apiParams('create', data)
})
}
export function editMenu (data) {
return request({
url: BASE + 'edit',
method: 'put',
data: apiParams('edit', data)
})
}
export function deleteMenu (data) {
return request({
url: BASE + 'delete',
method: 'delete',
data: apiParams('delete', data)
})
}
export function updateMenuStatus (data) {
return request({
url: BASE + 'update_status',
method: 'put',
data: apiParams('update_status', data)
})
}
export function sortMenu (data) {
return request({
url: BASE + 'sort',
method: 'put',
data: apiParams('sort', data)
})
}

View File

@@ -0,0 +1,15 @@
import { request } from '@/api/_service'
const BASE = 'system_settings/system_assistant/operate_log/'
export function getOperateLogList (data) {
return request({
url: BASE + 'list',
method: 'get',
params: {
method: 'system_settings_system_assistant_operate_log_list',
platform: 'background',
...data
}
})
}

View File

@@ -0,0 +1,149 @@
import { request } from '@/api/_service'
const CATEGORY_BASE = 'system_settings/system_assistant/problem_help/category/'
const MARKDOWN_BASE = 'system_settings/system_assistant/problem_help/markdown/'
export function getCategoryTree (data) {
return request({
url: CATEGORY_BASE + 'tree',
method: 'get',
params: {
method: 'system_settings_system_assistant_problem_category_tree',
platform: 'background',
...data
}
})
}
export function setCategoryAdd (data) {
return request({
url: CATEGORY_BASE + 'add',
method: 'post',
data: {
method: 'system_settings_system_assistant_problem_category_add',
platform: 'background',
...data
}
})
}
export function setCategoryUpdate (data) {
return request({
url: CATEGORY_BASE + 'edit',
method: 'put',
data: {
method: 'system_settings_system_assistant_problem_category_edit',
platform: 'background',
...data
}
})
}
export function delCategory (data) {
return request({
url: CATEGORY_BASE + 'delete',
method: 'post',
data: {
method: 'system_settings_system_assistant_problem_category_delete',
platform: 'background',
...data
}
})
}
export function getProblemTree (data) {
return request({
url: MARKDOWN_BASE + 'tree',
method: 'get',
params: {
method: 'system_settings_system_assistant_problem_markdown_tree',
platform: 'background',
...data
}
})
}
export function getMarkdownDetails (data) {
return request({
url: MARKDOWN_BASE + 'details',
method: 'get',
params: {
method: 'system_settings_system_assistant_problem_markdown_details',
platform: 'background',
...data
}
})
}
export function setMarkdownAdd (data) {
return request({
url: MARKDOWN_BASE + 'add',
method: 'post',
data: {
method: 'system_settings_system_assistant_problem_markdown_add',
platform: 'background',
...data
}
})
}
export function setMarkdownEdit (data) {
return request({
url: MARKDOWN_BASE + 'edit',
method: 'put',
data: {
method: 'system_settings_system_assistant_problem_markdown_edit',
platform: 'background',
...data
}
})
}
export function delMarkdownDetails (data) {
return request({
url: MARKDOWN_BASE + 'del',
method: 'delete',
data: {
method: 'system_settings_system_assistant_problem_markdown_del',
platform: 'background',
...data
}
})
}
export function deleteProblem (data) {
return request({
url: MARKDOWN_BASE + 'delete',
method: 'post',
data: {
method: 'del.problem.item',
platform: 'admin',
...data
}
})
}
export function searchMarkdown (data) {
return request({
url: MARKDOWN_BASE + 'search',
method: 'get',
params: {
method: 'system_settings_system_assistant_problem_markdown_search',
platform: 'background',
...data
}
})
}
export function getRoleAll (data) {
return request({
url: 'system_settings/user_management/role/list',
method: 'get',
params: {
method: 'system_settings_user_management_role_list',
platform: 'background',
page_size: 9999,
...data
}
})
}

View File

@@ -10,6 +10,14 @@ function apiParams (method, data = {}) {
}
}
export function getRoleAll (data) {
return request({
url: BASE + 'all',
method: 'get',
params: apiParams('all', data)
})
}
export function getRoleList (data) {
return request({
url: BASE + 'list',

View File

@@ -0,0 +1,99 @@
import { request } from '@/api/_service'
const BASE = 'system_settings/user_management/user/'
export function getUserList (data) {
return request({
url: BASE + 'list',
method: 'get',
params: {
method: 'system_settings_user_management_user_list',
platform: 'background',
...data
}
})
}
export function createUser (data) {
return request({
url: BASE + 'create',
method: 'post',
data: {
method: 'system_settings_user_management_user_create',
platform: 'background',
...data
}
})
}
export function editUser (data) {
return request({
url: BASE + 'edit',
method: 'put',
data: {
method: 'system_settings_user_management_user_edit',
platform: 'background',
...data
}
})
}
export function deleteUser (data) {
return request({
url: BASE + 'delete',
method: 'delete',
data: {
method: 'system_settings_user_management_user_delete',
platform: 'background',
...data
}
})
}
export function batchDeleteUser (data) {
return request({
url: BASE + 'batch_delete',
method: 'delete',
data: {
method: 'system_settings_user_management_user_batch_delete',
platform: 'background',
...data
}
})
}
export function enableUser (data) {
return request({
url: BASE + 'enable',
method: 'put',
data: {
method: 'system_settings_user_management_enable_user',
platform: 'background',
...data
}
})
}
export function disableUser (data) {
return request({
url: BASE + 'disable',
method: 'put',
data: {
method: 'system_settings_user_management_disable_user',
platform: 'background',
...data
}
})
}
export function resetUserPwd (data) {
return request({
url: BASE + 'reset_pwd',
method: 'put',
data: {
method: 'system_settings_user_management_user_reset_pwd',
platform: 'background',
...data
}
})
}

View File

@@ -77,6 +77,232 @@
"confirm": "Confirm",
"cancel": "Cancel",
"tip": "Tip"
},
"user": {
"search": "Search",
"reset": "Reset",
"index": "No.",
"username": "Username",
"full_name": "Full Name",
"pass_number": "Pass Number",
"status": "Status",
"enable": "Enabled",
"disable": "Disabled",
"user_group": "User Group",
"login_ip": "Last Login IP",
"last_login_time": "Last Login Time",
"actions": "Actions",
"add": "Add",
"edit": "Edit",
"delete": "Delete",
"add_title": "Add User",
"edit_title": "Edit User",
"enter_username": "Please enter username",
"enter_name": "Please enter full name",
"enter_password": "Please enter password",
"enter_confirm_password": "Please confirm password",
"enter_pass_number": "Please enter pass number",
"password": "Password",
"confirm_password": "Confirm Password",
"select_user_group": "Please select user group",
"select_rows_first": "Please select rows first",
"operation_success": "Operation succeeded",
"confirm_delete": "Are you sure to delete this user?",
"confirm_batch_delete": "Are you sure to batch delete selected users?",
"confirm_reset_pwd": "Are you sure to reset this user's password?",
"confirm_execute": "Are you sure to execute this operation?",
"password_not_match": "Passwords do not match",
"password_length": "Length should be 6 to 64 characters",
"username_length": "Length should be 3 to 20 characters",
"cannot_delete_self": "Cannot delete own account",
"cannot_operate_self": "Cannot operate own account",
"batch_delete": "Batch Delete",
"reset_password": "Reset Password",
"confirm": "Confirm",
"cancel": "Cancel",
"prompt": "Prompt"
}
},
"menu_management": {
"menu_configuration": {
"add_top_menu": "Add Top-Level Menu",
"expand": "Expand",
"collapse": "Collapse",
"add": "Add",
"enable": "Enable",
"disable": "Disable",
"delete": "Delete",
"confirm": "Confirm",
"cancel": "Cancel",
"modify": "Edit",
"filter": "Filter",
"filter_placeholder": "Enter keywords to filter",
"parent_menu": "Parent Menu",
"parent_menu_placeholder": "Leave blank for top-level menu. Try searching: Home",
"menu_name": "Menu Name",
"menu_name_placeholder": "Please enter menu name",
"menu_alias": "Alias",
"menu_alias_placeholder": "Enter menu alias (optional)",
"icon": "Icon",
"select_menu_icon": "Select Menu Icon",
"sort": "No.",
"navigation": "Navigation",
"link_type": "Link Type",
"link_type_module": "Internal Module",
"link_type_external": "External URL",
"open_type": "Open Method",
"open_self": "Same Tab",
"open_blank": "New Tab",
"url": "URL",
"url_placeholder": "Enter link address",
"params": "Parameters",
"params_placeholder": "Enter URL parameters (optional)",
"remark": "Remark",
"remark_placeholder": "Enter menu notes (optional)",
"add_menu": "Add Menu",
"edit_menu": "Edit Menu",
"name_required": "Menu name cannot be empty",
"name_max_length": "Must be 32 characters or less",
"alias_max_length": "Must be 16 characters or less",
"must_be_number": "Please enter a valid number",
"link_type_required": "Link type cannot be empty",
"max_255_length": "Must be 255 characters or less",
"operation_success": "Operation successful",
"delete_confirm": "Are you sure you want to delete this menu?",
"status_change_confirm": "Changing status will affect child/parent menus. Proceed?",
"prompt": "Notice",
"status": "Status",
"admin": "Backend",
"pda": "PDA",
"navigation_property": "Navigation Property",
"navigation_visible": "Visible",
"navigation_hidden": "Hidden",
"menu_depth": "Menu Level",
"query": "Search",
"reset": "Reset",
"please_select": "Please select"
}
},
"system_utilities": {
"api_logs": {
"id": "ID",
"ip": "IP",
"interface_name": "Interface Name",
"request_method": "Request Method",
"response_status": "Response Status",
"response_time_ms": "Response Time (ms)",
"process_code": "Process Code",
"tray_number": "Tray No.",
"battery_id": "Battery Barcode",
"batch_number": "Batch No.",
"process_id": "Process ID",
"create_date": "Create Date",
"operation": "Operation",
"status": "Status",
"batch": "Batch",
"create_time": "Create Time",
"success": "Success",
"failure": "Failure",
"view_response": "View Response",
"response": "Response",
"copy_request_content": "Copy Request",
"copy_response_content": "Copy Response",
"request_body": "Request Body",
"response_content": "Response Body",
"search": "Search",
"reset": "Reset",
"operation_success": "Operation succeeded",
"placeholder_ip": "Please enter IP",
"placeholder_interface_name": "Please enter interface name",
"placeholder_status": "Please select status",
"placeholder_batch": "Please enter batch",
"placeholder_tray_no": "Please enter tray no.",
"placeholder_process_code": "Please enter process code",
"placeholder_battery_barcode": "Please enter battery barcode",
"placeholder_create_time": "Please select time",
"expand": "Expand",
"collapse": "Collapse"
},
"operation_logs": {
"search": "Search",
"reset": "Reset",
"expand": "Expand",
"collapse": "Collapse",
"ip": "IP",
"placeholder_ip": "Please enter IP",
"operator": "Operator",
"placeholder_operator": "Select Operator",
"batch": "Batch",
"placeholder_batch": "Please enter batch",
"tray_no": "Tray No.",
"placeholder_tray_no": "Please enter tray no.",
"create_time": "Creation Time",
"placeholder_time": "Please select time",
"success": "Success",
"failure": "Failure",
"status": "Status",
"view_response": "View Response",
"operation": "Actions",
"response": "Response",
"copy_request_content": "Copy Request Content",
"copy_response_content": "Copy Response Content",
"request_body": "Request Body",
"response_content": "Response Content",
"action_name": "Action Name",
"action_code": "Action Code",
"request_path": "Request Path",
"create_date": "Creation Date",
"id": "ID",
"operation_success": "Operation Successful"
},
"problem_help": {
"search_placeholder": "Please enter content",
"add_directory": "Add Directory",
"add_document": "Add Document",
"edit_document": "Edit Document",
"edit": "Edit",
"delete": "Delete",
"download": "Download",
"dialog_edit_title": "Edit Category",
"dialog_create_title": "Add Category",
"category_name": "Category Name",
"placeholder_category_name": "Please enter a category name",
"parent_category": "Parent Menu",
"top_category": "Root Category",
"view_permission": "View Permissions",
"please_select": "Please select",
"sort": "Sort Order",
"cancel": "Cancel",
"confirm": "Confirm",
"update": "Update",
"category_name_required": "Category name cannot be empty",
"add_success": "Added successfully",
"update_success": "Updated successfully",
"title": "Title",
"describe": "Description",
"type": "Type",
"document": "Document",
"add": "Add",
"enter_title": "Please enter title",
"enter_describe": "Please enter description",
"edit_success": "Modified successfully",
"content_required": "Please enter document content",
"file_required": "Please upload a file",
"select_parent_menu": "Please select parent menu",
"select_role_group": "Please select permission role group",
"create_success": "Created successfully",
"drag_file_here": "Drag file here, or",
"click_upload": "Click to upload",
"submitter": "Submitter:",
"delete_success": "Deleted successfully!",
"operation_success": "Operation successful",
"confirm_message": "Are you sure you want to proceed?",
"prompt": "Prompt",
"confirm_close": "Confirm close?",
"select_delete_directory": "Please select a directory to delete first",
"select_edit_directory": "Please select a directory to edit first",
"upload_failed_retry": "Upload failed, please try again later",
"select_document_tip": "Please select a document from the left menu to view"
}
}
},

View File

@@ -77,6 +77,232 @@
"confirm": "确定",
"cancel": "取消",
"tip": "提示"
},
"user": {
"search": "查询",
"reset": "重置",
"index": "序号",
"username": "账号",
"full_name": "姓名",
"pass_number": "出入证编号",
"status": "状态",
"enable": "启用",
"disable": "禁用",
"user_group": "用户组",
"login_ip": "上次登录IP",
"last_login_time": "上次登录时间",
"actions": "操作",
"add": "新 增",
"edit": "编 辑",
"delete": "删 除",
"add_title": "新增用户",
"edit_title": "编辑用户",
"enter_username": "请输入账号",
"enter_name": "请输入姓名",
"enter_password": "请输入密码",
"enter_confirm_password": "请再次输入密码",
"enter_pass_number": "请输入出入证编号",
"password": "密码",
"confirm_password": "确认密码",
"select_user_group": "请选择用户组",
"select_rows_first": "请先勾选要操作的数据",
"operation_success": "操作成功",
"confirm_delete": "确定要删除该用户吗?",
"confirm_batch_delete": "确定要批量删除勾选的用户吗?",
"confirm_reset_pwd": "确定要重置该用户密码吗?",
"confirm_execute": "确定要执行该操作吗?",
"password_not_match": "两次输入的密码不一致",
"password_length": "长度在 6 到 64 个字符",
"username_length": "长度在 3 到 20 个字符",
"cannot_delete_self": "不能删除自己的账号",
"cannot_operate_self": "不能操作自己的账号",
"batch_delete": "批量删除",
"reset_password": "重置密码",
"confirm": "确定",
"cancel": "取消",
"prompt": "提示"
}
},
"menu_management": {
"menu_configuration": {
"add_top_menu": "新增顶层菜单",
"expand": "展开",
"collapse": "收起",
"add": "新增",
"enable": "启用",
"disable": "禁用",
"delete": "删除",
"confirm": "确定",
"cancel": "取消",
"modify": "修改",
"filter": "过滤",
"filter_placeholder": "输入关键词进行过滤",
"parent_menu": "上级菜单",
"parent_menu_placeholder": "不选择表示顶层菜单 试试搜索:首页",
"menu_name": "名称",
"menu_name_placeholder": "请输入菜单名称",
"menu_alias": "别名",
"menu_alias_placeholder": "可输入菜单别名",
"icon": "图标",
"select_menu_icon": "可选择菜单图标",
"sort": "排序",
"navigation": "导航",
"link_type": "链接类型",
"link_type_module": "模块",
"link_type_external": "外链",
"open_type": "打开方式",
"open_self": "当前窗口",
"open_blank": "新窗口",
"url": "URL",
"url_placeholder": "可输入链接地址",
"params": "参数",
"params_placeholder": "可输入链接参数",
"remark": "备注",
"remark_placeholder": "可输入菜单备注",
"add_menu": "新增菜单",
"edit_menu": "编辑菜单",
"name_required": "名称不能为空",
"name_max_length": "长度不能大于 32 个字符",
"alias_max_length": "长度不能大于 16 个字符",
"must_be_number": "必须为数字值",
"link_type_required": "链接类型不能为空",
"max_255_length": "长度不能大于 255 个字符",
"operation_success": "操作成功",
"delete_confirm": "确定要执行该操作吗?",
"status_change_confirm": "状态的切换会影响上下级菜单,是否确认操作?",
"prompt": "提示",
"status": "状态",
"admin": "后台",
"pda": "PDA",
"navigation_property": "导航属性",
"navigation_visible": "可见",
"navigation_hidden": "隐藏",
"menu_depth": "菜单深度",
"query": "查询",
"reset": "重置",
"please_select": "请选择"
}
},
"system_utilities": {
"api_logs": {
"id": "ID",
"ip": "IP",
"interface_name": "接口名称",
"request_method": "请求方法",
"response_status": "响应状态",
"response_time_ms": "响应时长(毫秒)",
"process_code": "工序编码",
"tray_number": "托盘号",
"battery_id": "电池条码",
"batch_number": "批次号",
"process_id": "进程ID",
"create_date": "创建日期",
"operation": "操作",
"status": "状态",
"batch": "批次",
"create_time": "创建时间",
"success": "成功",
"failure": "失败",
"view_response": "查看响应",
"response": "响应",
"copy_request_content": "复制请求内容",
"copy_response_content": "复制响应内容",
"request_body": "请求体",
"response_content": "响应内容",
"search": "查询",
"reset": "重置",
"operation_success": "操作成功",
"placeholder_ip": "请输入IP",
"placeholder_interface_name": "请输入接口名称",
"placeholder_status": "请选择状态",
"placeholder_batch": "请输入批次",
"placeholder_tray_no": "请输入托盘号",
"placeholder_process_code": "请输入工序编码",
"placeholder_battery_barcode": "请输入电池条码",
"placeholder_create_time": "请选择时间",
"expand": "展开更多",
"collapse": "收起"
},
"operation_logs": {
"search": "查询",
"reset": "重置",
"expand": "展开更多",
"collapse": "收起",
"ip": "IP",
"placeholder_ip": "请输入IP",
"operator": "操作人",
"placeholder_operator": "选择操作人",
"batch": "批次",
"placeholder_batch": "请输入批次",
"tray_no": "托盘号",
"placeholder_tray_no": "请输入托盘号",
"create_time": "创建时间",
"placeholder_time": "请选择时间",
"success": "成功",
"failure": "失败",
"status": "状态",
"view_response": "查看响应",
"operation": "操作",
"response": "响应",
"copy_request_content": "复制请求内容",
"copy_response_content": "复制响应内容",
"request_body": "请求体",
"response_content": "响应内容",
"action_name": "操作动作名称",
"action_code": "操作动作编码",
"request_path": "请求路径",
"create_date": "创建日期",
"id": "ID",
"operation_success": "操作成功"
},
"problem_help": {
"search_placeholder": "请输入内容",
"add_directory": "新增目录",
"add_document": "新增文档",
"edit_document": "编辑文档",
"edit": "编辑",
"delete": "删除",
"download": "下载",
"dialog_edit_title": "编辑类型",
"dialog_create_title": "新增类型",
"category_name": "类型名称",
"placeholder_category_name": "请输入类型名称",
"parent_category": "上级菜单",
"top_category": "顶层目录",
"view_permission": "查看权限",
"please_select": "请选择",
"sort": "序号",
"cancel": "取消",
"confirm": "确定",
"update": "修改",
"category_name_required": "分类名称不能为空",
"add_success": "添加成功",
"update_success": "修改成功",
"title": "标题",
"describe": "描述",
"type": "类型",
"document": "文档",
"add": "新增",
"enter_title": "请输入标题",
"enter_describe": "请输入描述",
"edit_success": "修改成功",
"content_required": "请输入文档编辑内容",
"file_required": "请上传文件",
"select_parent_menu": "请选择对应上级菜单",
"select_role_group": "请选择权限用户组",
"create_success": "新增成功",
"drag_file_here": "将文件拖到此处,或",
"click_upload": "点击上传",
"submitter": "提交人:",
"delete_success": "删除成功!",
"operation_success": "操作成功",
"confirm_message": "确定要执行该操作吗?",
"prompt": "提示",
"confirm_close": "确认关闭?",
"select_delete_directory": "请先选择需要删除的目录",
"select_edit_directory": "请先选择需要编辑的目录",
"upload_failed_retry": "上传失败,请稍后重试",
"select_document_tip": "请从左侧菜单选择文档查看"
}
}
},

View File

@@ -7,6 +7,8 @@ import 'flex.css'
import '@/components'
// svg 图标
import '@/assets/svg-icons'
// JSON 树形视图
import vueJsonTreeView from 'vue-json-tree-view'
// 国际化
import i18n from '@/i18n.js'
@@ -57,5 +59,7 @@ export default {
Vue.use(pluginError)
Vue.use(pluginLog)
Vue.use(pluginOpen)
// JSON 树形视图组件tree-view
Vue.use(vueJsonTreeView)
}
}

View File

@@ -19,6 +19,36 @@ export default {
name: `${pre}user_management-role`,
meta: { ...meta, cache: true, title: '角色' },
component: _import('system-administration/user-management/role')
},
{
path: 'user_management/user',
name: `${pre}user_management-user`,
meta: { ...meta, cache: true, title: '用户' },
component: _import('system-administration/user-management/user')
},
{
path: 'menu_configuration/menu',
name: `${pre}menu_configuration-menu`,
meta: { ...meta, cache: true, title: '菜单配置' },
component: _import('system-administration/menu-management/menu-configuration')
},
{
path: 'system_assistant/interface_log',
name: `${pre}system_assistant-interface_log`,
meta: { ...meta, cache: true, title: '接口日志' },
component: _import('system-administration/system-utilities/api-logs')
},
{
path: 'system_assistant/operate_log',
name: `${pre}system_assistant-operate_log`,
meta: { ...meta, cache: true, title: '操作日志' },
component: _import('system-administration/system-utilities/operation-logs')
},
{
path: 'system_assistant/problem_help',
name: `${pre}system_assistant-problem_help`,
meta: { ...meta, cache: true, title: '问题帮助' },
component: _import('system-administration/system-utilities/problem-help')
}
])('system_settings-')
}

View File

@@ -0,0 +1,682 @@
<template>
<d2-container>
<template #header>
<el-form :inline="true" :model="searchForm" ref="searchFormRef" size="mini">
<el-form-item :label="$t(key('link_type_module'))" prop="module">
<el-radio-group v-model="searchForm.module" @change="handleSearch" size="small">
<el-radio-button :label="$t(key('admin'))"></el-radio-button>
<el-radio-button :label="$t(key('pda'))"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t(key('status'))" prop="status">
<el-select
v-model="searchForm.status"
:placeholder="$t(key('please_select'))"
style="width: 120px;"
clearable
>
<el-option :label="$t(key('enable'))" :value="1" />
<el-option :label="$t(key('disable'))" :value="0" />
</el-select>
</el-form-item>
<el-form-item :label="$t(key('navigation_property'))" prop="is_navi">
<el-select
v-model="searchForm.is_navi"
:placeholder="$t(key('please_select'))"
style="width: 120px;"
clearable
>
<el-option :label="$t(key('navigation_visible'))" :value="1" />
<el-option :label="$t(key('navigation_hidden'))" :value="0" />
</el-select>
</el-form-item>
<el-form-item :label="$t(key('menu_depth'))" prop="level">
<el-input-number
v-model="searchForm.level"
controls-position="right"
:min="0"
style="width: 100px;"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="el-icon-search"
:disabled="loading"
@click="handleSearch"
>
{{ $t(key('query')) }}
</el-button>
</el-form-item>
<el-form-item>
<el-button icon="el-icon-refresh" @click="handleReset">
{{ $t(key('reset')) }}
</el-button>
</el-form-item>
</el-form>
</template>
<div class="menu-config">
<el-form :inline="true" size="small" @submit.native.prevent>
<el-form-item v-if="auth.create">
<el-button
icon="el-icon-plus"
:disabled="loading"
@click="handleCreateTop"
>
{{ $t(key('add_top_menu')) }}
</el-button>
</el-form-item>
<el-form-item>
<el-button-group>
<el-button icon="el-icon-circle-plus-outline" :disabled="loading" @click="toggleExpand(true)">
{{ $t(key('expand')) }}
</el-button>
<el-button icon="el-icon-remove-outline" :disabled="loading" @click="toggleExpand(false)">
{{ $t(key('collapse')) }}
</el-button>
</el-button-group>
</el-form-item>
<el-form-item :label="$t(key('filter'))">
<el-input
v-model="filterText"
:disabled="loading"
:placeholder="$t(key('filter_placeholder'))"
prefix-icon="el-icon-search"
style="width: 240px;"
clearable
/>
</el-form-item>
</el-form>
<el-row :gutter="20">
<el-col :span="10">
<el-tree
v-if="hackReset"
ref="tree"
class="tree-scroll"
node-key="menu_id"
:data="treeData"
:props="treeProps"
:filter-node-method="filterNode"
highlight-current
:default-expand-all="isExpandAll"
:default-expanded-keys="expanded"
draggable
:allow-drag="allowDrag"
@node-click="handleNodeClick"
@node-drop="handleDrop"
@node-expand="handleNodeExpand"
@node-collapse="handleNodeCollapse"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span class="tree-label" :class="{ 'status-disabled': !data.status }">
<i v-if="auth.move" class="el-icon-sort move-tree cs-mr-5" />
<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" />
{{ node.label }}
</span>
<span class="node-actions">
<el-button
v-if="auth.create"
type="text"
size="mini"
@click.stop="handleAppend(data)"
>
{{ $t(key('add')) }}
</el-button>
<el-button
v-if="auth.disabled_enable"
type="text"
size="mini"
@click.stop="handleToggleStatus(data)"
>
{{ data.status ? $t(key('disable')) : $t(key('enable')) }}
</el-button>
<el-button
v-if="auth.delete"
type="text"
size="mini"
@click.stop="handleRemove(data.menu_id)"
>
{{ $t(key('delete')) }}
</el-button>
</span>
</span>
</el-tree>
</el-col>
<el-col :span="14">
<el-card v-show="auth.create || auth.edit" class="box-card" shadow="never">
<div slot="header">
<span>{{ $t(key(formStatus === 'create' ? 'add_menu' : 'edit_menu')) }}</span>
<el-button
v-if="formStatus === 'create' && auth.create"
type="text"
:loading="formLoading"
style="float: right; padding: 3px 0;"
@click="handleCreateSubmit"
>
{{ $t(key('confirm')) }}
</el-button>
<el-button
v-else-if="formStatus === 'update' && auth.edit"
type="text"
:loading="formLoading"
style="float: right; padding: 3px 0;"
@click="handleUpdateSubmit"
>
{{ $t(key('modify')) }}
</el-button>
</div>
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item :label="$t(key('parent_menu'))" prop="parent_id">
<el-cascader
v-model="form.parent_id"
:placeholder="$t(key('parent_menu_placeholder'))"
:key="form.menu_id"
:options="treeData"
:props="cascaderProps"
style="width: 100%;"
filterable
clearable
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t(key('menu_name'))" prop="name">
<el-input
v-model="form.name"
:placeholder="$t(key('menu_name_placeholder'))"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t(key('menu_alias'))" prop="alias">
<el-input
v-model="form.alias"
:placeholder="$t(key('menu_alias_placeholder'))"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t(key('icon'))" prop="icon">
<d2-icon-select
v-model="form.icon"
:user-input="true"
:placeholder="$t(key('select_menu_icon'))"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t(key('sort'))" prop="sort">
<el-input-number
v-model="form.sort"
:min="0"
:max="255"
style="width: 120px;"
controls-position="right"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t(key('navigation'))" prop="is_navi">
<el-switch v-model="form.is_navi" active-value="1" inactive-value="0" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t(key('link_type'))" prop="type">
<el-radio-group v-model="form.type">
<el-radio :label="0">{{ $t(key('link_type_module')) }}</el-radio>
<el-radio :label="1">{{ $t(key('link_type_external')) }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t(key('open_type'))" prop="target">
<el-radio-group v-model="form.target">
<el-radio label="_self">{{ $t(key('open_self')) }}</el-radio>
<el-radio label="_blank">{{ $t(key('open_blank')) }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="URL" prop="url">
<el-input
v-model="form.url"
:placeholder="$t(key('url_placeholder'))"
clearable
/>
</el-form-item>
<el-form-item :label="$t(key('params'))" prop="params">
<el-input
v-model="form.params"
:placeholder="$t(key('params_placeholder'))"
clearable
/>
</el-form-item>
<el-form-item :label="$t(key('remark'))" prop="remark">
<el-input
v-model="form.remark"
:placeholder="$t(key('remark_placeholder'))"
type="textarea"
:rows="3"
/>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</div>
</d2-container>
</template>
<script>
import util from '@/libs/util'
import { i18nMixin } from '@/composables/useI18n'
import { confirmMixin } from '@/composables/useConfirmHandle'
import {
getMenuList,
createMenu,
editMenu,
deleteMenu,
updateMenuStatus,
sortMenu
} from '@/api/system-administration/menu-configuration'
export default {
name: 'system-administration-menu-configuration',
mixins: [i18nMixin('page.system_administration.menu_management.menu_configuration'), confirmMixin],
data () {
const $t = this.$t.bind(this)
return {
loading: false,
formLoading: false,
treeData: [],
module: 'admin',
hackReset: true,
isExpandAll: false,
expanded: [],
oldExpanded: [],
filterText: '',
formStatus: 'create',
appendData: {},
appendType: '',
updateNode: {},
auth: {
create: true,
delete: true,
edit: true,
disabled_enable: true,
move: true
},
treeProps: {
label: 'name',
children: 'children'
},
cascaderProps: {
value: 'menu_id',
label: 'name',
children: 'children',
checkStrictly: true,
emitPath: false
},
searchForm: {
module: 'admin',
status: undefined,
is_navi: undefined,
level: 0
},
form: this.getDefaultForm(),
rules: {
name: [
{ required: true, message: $t(this.key('name_required')), trigger: 'blur' },
{ max: 32, message: $t(this.key('name_max_length')), trigger: 'blur' }
],
alias: [
{ max: 16, message: $t(this.key('alias_max_length')), trigger: 'blur' }
],
sort: [
{ type: 'number', message: $t(this.key('must_be_number')), trigger: 'blur' }
],
url: [
{ max: 255, message: $t(this.key('max_255_length')), trigger: 'blur' }
],
params: [
{ max: 255, message: $t(this.key('max_255_length')), trigger: 'blur' }
],
remark: [
{ max: 255, message: $t(this.key('max_255_length')), trigger: 'blur' }
]
}
}
},
watch: {
filterText (val) {
this.$refs.tree && this.$refs.tree.filter(val)
this.expanded = this.oldExpanded
}
},
mounted () {
this._validationAuth()
this.handleSearch()
},
methods: {
getDefaultForm () {
return {
parent_id: 0,
name: '',
alias: '',
icon: '',
remark: '',
type: 0,
url: '',
params: '',
target: '_self',
is_navi: '0',
sort: 50
}
},
_validationAuth () {
this.auth.create = this.$permission('/system_settings/menu_configuration/menu/create')
this.auth.edit = this.$permission('/system_settings/menu_configuration/menu/edit')
this.auth.delete = this.$permission('/system_settings/menu_configuration/menu/delete')
this.auth.disabled_enable = this.$permission('/system_settings/menu_configuration/menu/disabled_enable')
this.auth.move = this.$permission('/system_settings/menu_configuration/menu/sort')
},
filterNode (value, data) {
if (!value) return true
return data.name.indexOf(value) !== -1
},
toggleExpand (isExpand) {
this.filterText = ''
this.expanded = []
this.hackReset = false
this.$nextTick(() => {
this.isExpandAll = isExpand
this.hackReset = true
})
},
resetForm () {
this.form = this.getDefaultForm()
},
resetElements (status = 'create') {
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.clearValidate()
})
this.formStatus = status
this.formLoading = false
},
handleSearch () {
const form = { ...this.searchForm }
form.level = form.level <= 0 ? undefined : form.level - 1
this.loading = true
getMenuList(form)
.then(res => {
this.module = form.module
const menuKey = 'menu_' + form.module
const flatList = Array.isArray(res)
? res
: (res[menuKey] || (res.data && res.data[menuKey]) || [])
this.treeData = util.formatDataToTree(flatList)
this.filterText = ''
this.resetForm()
this.resetElements('create')
if (this.$refs.tree && this.$refs.tree.getCurrentKey()) {
this.$refs.tree.setCurrentKey(null)
}
})
.finally(() => {
this.loading = false
})
},
handleReset () {
this.$refs.searchFormRef.resetFields()
this.searchForm.module = 'admin'
this.searchForm.level = 0
this.handleSearch()
},
handleNodeClick (data) {
if (!this.auth.create && !this.auth.edit) return
this.updateNode = data
this.resetForm()
this.resetElements('update')
this.form = { ...data, is_navi: String(data.is_navi) }
},
handleCreateTop () {
this.appendType = 'newTopMenu'
this.resetForm()
this.resetElements('create')
if (this.$refs.tree && this.$refs.tree.getCurrentKey()) {
this.$refs.tree.setCurrentKey(null)
}
},
handleAppend (data) {
const key = data.menu_id
this.handleCreateTop()
this.$refs.tree.setCurrentKey(key)
this.form.parent_id = key
this.appendType = 'newChildMenu'
this.appendData = data
},
appendChildToTree (newNode) {
if (!this.appendData.children) {
this.$set(this.appendData, 'children', [])
}
this.appendData.children.push(newNode)
},
appendTopToTree (newNode) {
if (!newNode.children) {
this.$set(newNode, 'children', [])
}
this.treeData.push(newNode)
},
updateNodeInTree (data) {
if (!this.updateNode) return
Object.keys(data).forEach(key => {
if (Object.prototype.hasOwnProperty.call(this.updateNode, key)) {
this.updateNode[key] = data[key]
}
})
this.updateNode = {}
},
handleCreateSubmit () {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.oldExpanded = this.expanded
this.formLoading = true
createMenu({ ...this.form, module: this.module })
.then(res => {
let data = res
if (data && data.data) data = data.data
if (Array.isArray(data) && data.length > 0) data = data[0]
if (!this.isExpandAll) {
this.expanded = [data.parent_id || data.menu_id]
}
if (this.appendType === 'newChildMenu') {
this.appendChildToTree(data)
} else if (this.appendType === 'newTopMenu') {
this.appendTopToTree(data)
}
this.appendType = ''
this.$message.success(this.$t(this.key('operation_success')))
})
.finally(() => {
this.formLoading = false
})
})
},
handleUpdateSubmit () {
this.$refs.formRef.validate(valid => {
if (!valid) return
this.oldExpanded = this.expanded
this.formLoading = true
editMenu(this.form)
.then(() => {
this.updateNodeInTree(this.form)
this.$message.success(this.$t(this.key('operation_success')))
})
.finally(() => {
this.formLoading = false
})
})
},
async handleRemove (key) {
const cancelled = await this.$confirmAction(
{
message: this.key('delete_confirm'),
title: this.key('prompt'),
confirmButtonText: this.key('confirm'),
cancelButtonText: this.key('cancel')
},
() => deleteMenu({ menu_id: key })
)
if (cancelled) return
this.$refs.tree.remove(this.$refs.tree.getNode(key))
this.$message.success(this.$t(this.key('operation_success')))
},
async handleToggleStatus (data) {
const cancelled = await this.$confirmAction(
{
message: this.key('status_change_confirm'),
title: this.key('prompt'),
confirmButtonText: this.key('confirm'),
cancelButtonText: this.key('cancel')
},
() => updateMenuStatus({ menu_id: data.menu_id, status: data.status ? 0 : 1 })
)
if (cancelled) return
if (!this.isExpandAll) {
const node = this.$refs.tree.getNode(data.menu_id)
this.expanded = [node && node.data ? (node.data.parent_id || data.menu_id) : data.menu_id]
}
this.handleSearch()
},
handleDrop (draggingNode, dropNode, dropType) {
const setMenu = {
menu_id: draggingNode.data.menu_id,
parent_id: draggingNode.data.parent_id
}
const indexMenu = []
if (dropType === 'inner') {
setMenu.parent_id = dropNode.key
} else {
setMenu.parent_id = dropNode.data.parent_id
dropNode.parent.childNodes.forEach((value, index) => {
indexMenu.push(value.key)
value.data.sort = index + 1
})
}
if (indexMenu.length > 0) {
sortMenu({ menu_sort: indexMenu, ...setMenu })
.finally(() => { this.handleSearch() })
.catch(() => { this.handleSearch() })
}
},
allowDrag () {
return true
},
handleNodeExpand (data) {
const exists = this.expanded.some(item => item === data.menu_id)
if (!exists) {
this.expanded.push(data.menu_id)
}
},
handleNodeCollapse (data) {
this.expanded = this.expanded.filter(item => item !== data.menu_id)
this._removeChildrenIds(data)
},
_removeChildrenIds (data) {
if (!data.children) return
data.children.forEach(item => {
const index = this.expanded.indexOf(item.menu_id)
if (index >= 0) {
this.expanded.splice(index, 1)
}
this._removeChildrenIds(item)
})
}
}
}
</script>
<style lang="scss" scoped>
.tree-scroll {
max-height: 615px;
overflow: auto;
padding-bottom: 1px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.tree-label {
i {
width: 16px;
}
.fa {
font-size: 16px;
vertical-align: baseline;
}
}
.node-actions {
display: none;
}
.custom-tree-node:hover .node-actions {
display: block;
}
.move-tree {
color: #c0c4cc;
cursor: move;
}
.status-disabled {
color: #c0c4cc;
text-decoration: line-through;
}
.box-card {
border-radius: 0;
border: 1px solid #dcdfe6;
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<el-dialog
:title="$t(`${prefix}.response`)"
:visible.sync="visibleProxy"
width="700px"
:close-on-click-modal="false"
@close="onClose"
>
<div>
<el-row :gutter="20">
<el-col :span="4">
<el-button type="primary" plain size="mini" @click="copyParam">
{{ $t(`${prefix}.copy_request_content`) }}
</el-button>
</el-col>
<el-col :span="4">
<el-button type="primary" plain size="mini" @click="copyResult">
{{ $t(`${prefix}.copy_response_content`) }}
</el-button>
</el-col>
</el-row>
<el-divider content-position="left">
{{ $t(`${prefix}.request_body`) }}
</el-divider>
<tree-view
v-if="paramData"
:data="paramData"
:options="{ maxDepth: 2 }"
style="line-height: 20px; text-align: left;"
/>
<el-divider content-position="left">
{{ $t(`${prefix}.response_content`) }}
</el-divider>
<tree-view
v-if="resultData"
:data="resultData"
:options="{ maxDepth: 2 }"
style="line-height: 20px; text-align: left;"
/>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'ResponseDialog',
props: {
visible: { type: Boolean, default: false },
paramData: { type: Object, default: null },
resultData: { type: Object, default: null },
prefix: { type: String, required: true }
},
computed: {
visibleProxy: {
get () { return this.visible },
set (val) { this.$emit('update:visible', val) }
}
},
methods: {
onClose () {
this.$emit('update:visible', false)
},
copyToClipboard (data) {
const input = document.createElement('input')
input.value = JSON.stringify(data)
document.body.appendChild(input)
input.select()
document.execCommand('Copy')
input.style.display = 'none'
this.$message.success(this.$t(`${this.prefix}.operation_success`))
},
copyParam () {
this.copyToClipboard(this.paramData)
},
copyResult () {
this.copyToClipboard(this.resultData)
}
}
}
</script>

View File

@@ -0,0 +1,277 @@
<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form :inline="true" ref="searchFormRef" size="mini" @submit.native.prevent>
<el-form-item :label="$t(key('ip'))">
<el-input
v-model="search.ip"
:placeholder="$t(key('placeholder_ip'))"
clearable
style="width:160px"
@keyup.enter.native="onSearch"
/>
</el-form-item>
<el-form-item :label="$t(key('interface_name'))">
<el-input
v-model="search.unit"
:placeholder="$t(key('placeholder_interface_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('placeholder_status'))"
clearable
style="width:120px"
>
<el-option :value="200" :label="$t(key('success'))" />
<el-option :value="4001" :label="$t(key('failure'))" />
</el-select>
</el-form-item>
<el-form-item :label="$t(key('batch'))">
<el-input
v-model="search.batch"
:placeholder="$t(key('placeholder_batch'))"
clearable
style="width:160px"
@keyup.enter.native="onSearch"
/>
</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-button
v-if="!searchExpanded"
type="text"
icon="el-icon-arrow-down"
@click="searchExpanded = true"
>
{{ $t(key('expand')) }}
</el-button>
<el-button
v-else
type="text"
icon="el-icon-arrow-up"
@click="searchExpanded = false"
>
{{ $t(key('collapse')) }}
</el-button>
</el-form-item>
<div v-show="searchExpanded" class="search-bar__extra">
<el-form-item :label="$t(key('tray_number'))">
<el-input
v-model="search.tray"
:placeholder="$t(key('placeholder_tray_no'))"
clearable
style="width:160px"
@keyup.enter.native="onSearch"
/>
</el-form-item>
<el-form-item :label="$t(key('process_code'))">
<el-input
v-model="search.process_code"
:placeholder="$t(key('placeholder_process_code'))"
clearable
style="width:160px"
@keyup.enter.native="onSearch"
/>
</el-form-item>
<el-form-item :label="$t(key('battery_id'))">
<el-input
v-model="search.battery_id"
:placeholder="$t(key('placeholder_battery_barcode'))"
clearable
style="width:220px"
@keyup.enter.native="onSearch"
/>
</el-form-item>
<el-form-item :label="$t(key('create_time'))">
<el-date-picker
v-model="search.time"
type="datetimerange"
:placeholder="$t(key('placeholder_create_time'))"
range-separator="-"
start-placeholder=""
end-placeholder=""
value-format="yyyy-MM-dd HH:mm:ss"
style="width:340px"
/>
</el-form-item>
</div>
</el-form>
</div>
</template>
<page-table
ref="pageTable"
:columns="columns"
:data="tableData"
:loading="loading"
:row-buttons="rowButtons"
:pagination="pagination"
auto-height
@page-change="onPageChange"
>
<template #col-status="{ row }">
<span v-if="row.status === 0" style="color: #67c23a;">
<i class="el-icon-circle-check" />
{{ $t(key('success')) }}
</span>
<span v-else style="color: #F56C6C;">
<i class="el-icon-circle-close" />
{{ $t(key('failure')) }}
</span>
</template>
</page-table>
<response-dialog
:visible.sync="respVisible"
:param-data="respParam"
:result-data="respResult"
:prefix="prefix"
/>
</d2-container>
</template>
<script>
import { useTableColumns } from '@/composables/useTableColumns'
import { useTableButtons } from '@/composables/useTableButtons'
import { i18nMixin } from '@/composables/useI18n'
import { getInterfaceLogList } from '@/api/system-administration/api-logs'
import PageTable from '@/components/page-table'
import ResponseDialog from './components/ResponseDialog/index.vue'
export default {
name: 'system-administration-api-logs',
components: { PageTable, ResponseDialog },
mixins: [i18nMixin('page.system_administration.system_utilities.api_logs')],
data () {
return {
loading: false,
tableData: [],
columns: [],
rowButtons: [],
pagination: { current: 1, size: 10, total: 0 },
search: {
ip: '',
unit: '',
status: undefined,
batch: '',
tray: '',
process_code: '',
battery_id: '',
time: undefined
},
respVisible: false,
respParam: null,
respResult: null,
searchExpanded: false
}
},
computed: {
prefix () {
return 'page.system_administration.system_utilities.api_logs'
}
},
created () {
this.columns = useTableColumns([
{ prop: 'id', label: this.key('id'), width: 65 },
{ prop: 'client_ip', label: this.key('ip'), width: 140 },
{ prop: 'unit', label: this.key('request_method'), minWidth: 150 },
{ prop: 'status', label: this.key('response_status'), slot: 'status', width: 100 },
{ prop: 'insterface_time', label: this.key('response_time_ms'), width: 130 },
{ prop: 'data1', label: this.key('process_code'), showOverflowTooltip: true, minWidth: 120 },
{ prop: 'data2', label: this.key('tray_number'), showOverflowTooltip: true, minWidth: 120 },
{ prop: 'data3', label: this.key('battery_id'), showOverflowTooltip: true, minWidth: 130 },
{ prop: 'data4', label: this.key('batch_number'), showOverflowTooltip: true, minWidth: 120 },
{ prop: 'data5', label: this.key('process_id'), showOverflowTooltip: true, minWidth: 120 },
{ prop: 'create_time', label: this.key('create_date'), width: 170 },
{ prop: '_actions', label: this.key('operation'), width: 100, fixed: 'right' }
], { selectionWidth: 0 })
const btns = useTableButtons({
row: [
{
key: 'view',
label: this.key('view_response'),
icon: 'el-icon-view',
auth: '/system_settings/system_assistant/interface_log/view',
onClick: this.handleViewResponse
}
]
}, this.$permission)
this.rowButtons = btns.rowButtons
this.fetchData()
},
methods: {
async fetchData () {
this.loading = true
try {
const params = {
...this.search,
page_no: this.pagination.current,
page_size: this.pagination.size
}
const res = await getInterfaceLogList(params)
const data = Array.isArray(res) ? res : (res.data || [])
this.tableData = data
this.pagination.total = res.count || data.length
} finally {
this.loading = false
}
},
onSearch () {
this.pagination.current = 1
this.fetchData()
},
onReset () {
this.$refs.searchFormRef.resetFields()
this.search.ip = ''
this.search.unit = ''
this.search.status = undefined
this.search.batch = ''
this.search.tray = ''
this.search.process_code = ''
this.search.battery_id = ''
this.search.time = undefined
this.pagination.current = 1
this.fetchData()
},
onPageChange (page) {
this.pagination.current = page.current
this.pagination.size = page.size
this.fetchData()
},
handleViewResponse (row) {
try {
this.respParam = JSON.parse(row.params || '{}')
this.respResult = JSON.parse(row.result || '{}')
} catch {
this.respParam = row.params || {}
this.respResult = row.result || {}
}
this.respVisible = true
}
}
}
</script>
<style scoped>
.search-bar {
padding: 10px 0;
}
.search-bar .el-form-item--mini.el-form-item {
margin-bottom: 4px;
}
.search-bar__extra {
display: inline-block;
width: 100%;
}
</style>

View File

@@ -0,0 +1,259 @@
<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form :inline="true" ref="searchFormRef" size="mini" @submit.native.prevent>
<el-form-item :label="$t(key('ip'))">
<el-input
v-model="search.ip"
:placeholder="$t(key('placeholder_ip'))"
clearable
style="width:160px"
@keyup.enter.native="onSearch"
/>
</el-form-item>
<el-form-item :label="$t(key('operator'))">
<el-select
v-model="search.user_id"
:placeholder="$t(key('placeholder_operator'))"
clearable
filterable
style="width:200px"
>
<el-option
v-for="u in userList"
:key="u.user_id"
:value="u.user_id"
:label="u.username"
/>
</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-button
v-if="!searchExpanded"
type="text"
icon="el-icon-arrow-down"
@click="searchExpanded = true"
>
{{ $t(key('expand')) }}
</el-button>
<el-button
v-else
type="text"
icon="el-icon-arrow-up"
@click="searchExpanded = false"
>
{{ $t(key('collapse')) }}
</el-button>
</el-form-item>
<div v-show="searchExpanded" class="search-bar__extra">
<el-form-item :label="$t(key('batch'))">
<el-input
v-model="search.batch"
:placeholder="$t(key('placeholder_batch'))"
clearable
style="width:160px"
@keyup.enter.native="onSearch"
/>
</el-form-item>
<el-form-item :label="$t(key('tray_no'))">
<el-input
v-model="search.tray"
:placeholder="$t(key('placeholder_tray_no'))"
clearable
style="width:160px"
@keyup.enter.native="onSearch"
/>
</el-form-item>
<el-form-item :label="$t(key('create_time'))">
<el-date-picker
v-model="search.time"
type="datetimerange"
:placeholder="$t(key('placeholder_time'))"
range-separator="-"
start-placeholder=""
end-placeholder=""
value-format="yyyy-MM-dd HH:mm:ss"
style="width:340px"
/>
</el-form-item>
</div>
</el-form>
</div>
</template>
<page-table
ref="pageTable"
:columns="columns"
:data="tableData"
:loading="loading"
:row-buttons="rowButtons"
:pagination="pagination"
auto-height
@page-change="onPageChange"
>
<template #col-status="{ row }">
<span v-if="row.status === 0" style="color: #67c23a;">
<i class="el-icon-circle-check" />
{{ $t(key('success')) }}
</span>
<span v-else style="color: #F56C6C;">
<i class="el-icon-circle-close" />
{{ $t(key('failure')) }}
</span>
</template>
</page-table>
<response-dialog
:visible.sync="respVisible"
:param-data="respParam"
:result-data="respResult"
:prefix="prefix"
/>
</d2-container>
</template>
<script>
import { useTableColumns } from '@/composables/useTableColumns'
import { useTableButtons } from '@/composables/useTableButtons'
import { i18nMixin } from '@/composables/useI18n'
import { getOperateLogList } from '@/api/system-administration/operation-logs'
import { getUserList } from '@/api/system-administration/user'
import PageTable from '@/components/page-table'
import ResponseDialog from '../api-logs/components/ResponseDialog/index.vue'
export default {
name: 'system-administration-operation-logs',
components: { PageTable, ResponseDialog },
mixins: [i18nMixin('page.system_administration.system_utilities.operation_logs')],
data () {
return {
loading: false,
tableData: [],
columns: [],
rowButtons: [],
userList: [],
pagination: { current: 1, size: 10, total: 0 },
search: {
ip: '',
user_id: undefined,
batch: '',
tray: '',
time: undefined
},
respVisible: false,
respParam: null,
respResult: null,
searchExpanded: false
}
},
computed: {
prefix () {
return 'page.system_administration.system_utilities.operation_logs'
}
},
created () {
this.columns = useTableColumns([
{ prop: 'id', label: this.key('id'), width: 65 },
{ prop: 'username', label: this.key('operator'), width: 120 },
{ prop: 'ip', label: this.key('ip'), width: 140 },
{ prop: 'status', label: this.key('status'), slot: 'status', width: 90 },
{ prop: 'action_name', label: this.key('action_name'), showOverflowTooltip: true, minWidth: 130 },
{ prop: 'action', label: this.key('action_code'), showOverflowTooltip: true, minWidth: 130 },
{ prop: 'path', label: this.key('request_path'), showOverflowTooltip: true, minWidth: 180 },
{ prop: 'batch', label: this.key('batch'), showOverflowTooltip: true, minWidth: 120 },
{ prop: 'tray', label: this.key('tray_no'), showOverflowTooltip: true, minWidth: 120 },
{ prop: 'create_time', label: this.key('create_date'), width: 170 },
{ prop: '_actions', label: this.key('operation'), width: 100, fixed: 'right' }
], { selectionWidth: 0 })
const btns = useTableButtons({
row: [
{
key: 'view',
label: this.key('view_response'),
icon: 'el-icon-view',
auth: '/system_settings/system_assistant/operate_log/view',
onClick: this.handleViewResponse
}
]
}, this.$permission)
this.rowButtons = btns.rowButtons
this.fetchUsers()
this.fetchData()
},
methods: {
async fetchUsers () {
try {
const res = await getUserList({ page_no: 1, page_size: 9999 })
this.userList = Array.isArray(res) ? res : (res.data || [])
} catch { /* 用户列表加载失败不影响主流程 */ }
},
async fetchData () {
this.loading = true
try {
const params = {
...this.search,
page_no: this.pagination.current,
page_size: this.pagination.size
}
const res = await getOperateLogList(params)
const data = Array.isArray(res) ? res : (res.data || [])
this.tableData = data
this.pagination.total = res.count || data.length
} finally {
this.loading = false
}
},
onSearch () {
this.pagination.current = 1
this.fetchData()
},
onReset () {
this.$refs.searchFormRef.resetFields()
this.search = {
ip: '',
user_id: undefined,
batch: '',
tray: '',
time: undefined
}
this.pagination.current = 1
this.fetchData()
},
onPageChange (page) {
this.pagination.current = page.current
this.pagination.size = page.size
this.fetchData()
},
handleViewResponse (row) {
try {
this.respParam = JSON.parse(row.params || '{}')
this.respResult = JSON.parse(row.result || '{}')
} catch {
this.respParam = row.params || {}
this.respResult = row.result || {}
}
this.respVisible = true
}
}
}
</script>
<style scoped>
.search-bar {
padding: 10px 0;
}
.search-bar .el-form-item--mini.el-form-item {
margin-bottom: 4px;
}
.search-bar__extra {
display: inline-block;
width: 100%;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<el-dialog
:title="textMap[dialogStatus]"
:visible.sync="visibleProxy"
:append-to-body="true"
:close-on-click-modal="false"
width="600px"
@closed="onClosed"
>
<el-form :model="form" :rules="rules" ref="form" label-width="110px">
<el-form-item :label="$t(`${pre}.category_name`)" prop="name">
<el-input
v-model="form.name"
:placeholder="$t(`${pre}.placeholder_category_name`)"
clearable
/>
</el-form-item>
<el-form-item :label="$t(`${pre}.parent_category`)" prop="parent_id">
<el-cascader
v-model="form.parent_id"
:options="cascaderOptions"
:props="{
value: 'id',
label: 'name',
expandTrigger: 'hover',
checkStrictly: true
}"
/>
</el-form-item>
<el-form-item :label="$t(`${pre}.view_permission`)" prop="role_ids">
<el-select v-model="form.role_ids" multiple :placeholder="$t(`${pre}.please_select`)">
<el-option
v-for="item in roleList"
:key="item.id"
:label="item.name"
:value="String(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t(`${pre}.sort`)" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="10" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="close">{{ $t(`${pre}.cancel`) }}</el-button>
<el-button type="primary" size="small" :loading="loading" @click="handleSubmit">
{{ dialogStatus === 'create' ? $t(`${pre}.confirm`) : $t(`${pre}.update`) }}
</el-button>
</div>
</el-dialog>
</template>
<script>
const TOP_CATEGORY = { id: 0, name: '' }
export default {
name: 'CategoryDialog',
props: {
visible: { type: Boolean, default: false },
categoryTree: { type: Array, default: () => [] },
roleList: { type: Array, default: () => [] },
pre: { type: String, required: true }
},
data () {
return {
dialogStatus: 'create',
loading: false,
editId: null,
form: this.getDefaultForm(),
rules: {
name: [{ required: true, message: this.$t(`${this.pre}.category_name_required`), trigger: 'blur' }]
}
}
},
computed: {
visibleProxy: {
get () { return this.visible },
set (val) { this.$emit('update:visible', val) }
},
textMap () {
return {
create: this.$t(`${this.pre}.dialog_create_title`),
update: this.$t(`${this.pre}.dialog_edit_title`)
}
},
cascaderOptions () {
const top = { ...TOP_CATEGORY, name: this.$t(`${this.pre}.top_category`) }
return [top].concat(this.categoryTree)
}
},
methods: {
getDefaultForm () {
return { name: undefined, parent_id: undefined, sort: 0, role_ids: [] }
},
openCreate () {
this.dialogStatus = 'create'
this.form = this.getDefaultForm()
this.$nextTick(() => {
if (this.$refs.form) this.$refs.form.clearValidate()
})
},
openEdit (row) {
this.dialogStatus = 'update'
this.editId = row.id
this.form = {
name: row.name,
parent_id: row.parent_id != null ? [row.parent_id] : [],
role_ids: row.role_ids || [],
sort: row.sort || 0
}
this.$nextTick(() => {
if (this.$refs.form) this.$refs.form.clearValidate()
})
},
handleSubmit () {
this.$refs.form.validate(valid => {
if (!valid) return
this.loading = true
this.$emit('submit', {
status: this.dialogStatus,
id: this.editId,
form: { ...this.form }
})
this.loading = false
})
},
onClosed () {
this.form = this.getDefaultForm()
},
close () {
this.$emit('update:visible', false)
}
}
}
</script>

View File

@@ -0,0 +1,202 @@
<template>
<el-drawer
:title="drawerTitle"
:visible.sync="visibleProxy"
direction="rtl"
size="80%"
:before-close="handleBeforeClose"
>
<div v-loading="loading" class="editor-body">
<el-form
ref="form"
label-position="right"
:model="form"
:rules="rules"
label-width="80px"
>
<el-form-item :label="$t(`${pre}.title`)" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item :label="$t(`${pre}.describe`)" prop="describe">
<el-input type="textarea" :maxlength="30" show-word-limit v-model="form.describe" />
</el-form-item>
<el-form-item :label="$t(`${pre}.parent_category`)" prop="category_id">
<el-cascader
v-model="form.category_id"
:options="categoryTreeData"
:props="{
value: 'id',
label: 'name',
expandTrigger: 'hover',
checkStrictly: true
}"
/>
</el-form-item>
<el-form-item :label="$t(`${pre}.view_permission`)" prop="role_ids">
<el-select v-model="form.role_ids" multiple :placeholder="$t(`${pre}.please_select`)">
<el-option
v-for="item in roleList"
:key="item.id"
:label="item.name"
:value="String(item.id)"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t(`${pre}.sort`)" prop="sort">
<el-input-number v-model="form.sort" :min="0" />
</el-form-item>
<el-form-item :label="$t(`${pre}.type`)">
<el-radio-group v-model="form.type">
<el-radio label="md">{{ $t(`${pre}.document`) }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-show="form.type === 'md'" style="width:100%">
<mavon-editor
ref="mavon"
style="height:500px"
v-model="form.markdown"
@imgAdd="handleUploadImages"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="submitting" @click="handleAdd">
{{ $t(`${pre}.add`) }}
</el-button>
<el-button @click="closeDrawer">{{ $t(`${pre}.cancel`) }}</el-button>
</el-form-item>
</el-form>
</div>
</el-drawer>
</template>
<script>
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
import axios from 'axios'
export default {
name: 'MarkdownEditor',
components: { mavonEditor },
props: {
visible: { type: Boolean, default: false },
editData: { type: Object, default: null },
categoryTreeData: { type: Array, default: () => [] },
roleList: { type: Array, default: () => [] },
pre: { type: String, required: true }
},
data () {
return {
loading: false,
submitting: false,
isEdit: false,
editId: null,
form: this.getDefaultForm(),
rules: {
title: [{ required: true, message: this.$t(`${this.pre}.enter_title`), trigger: 'blur' }],
describe: [{ required: true, message: this.$t(`${this.pre}.enter_describe`), trigger: 'blur' }],
category_id: [{ required: true, message: this.$t(`${this.pre}.select_parent_menu`), trigger: 'blur' }],
role_ids: [{ required: true, message: this.$t(`${this.pre}.select_role_group`), trigger: 'blur' }]
}
}
},
computed: {
visibleProxy: {
get () { return this.visible },
set (val) { this.$emit('update:visible', val) }
},
drawerTitle () {
return this.isEdit ? this.$t(`${this.pre}.edit_document`) : this.$t(`${this.pre}.add_document`)
}
},
watch: {
visible (val) {
if (val && this.editData) {
this.isEdit = true
this.editId = this.editData.id
this.form = {
title: this.editData.title,
category_id: this.editData.category_id,
describe: this.editData.describe,
markdown: this.editData.markdown || '',
type: this.editData.type || 'md',
role_ids: this.editData.role_ids ? this.editData.role_ids.map(String) : [],
url: this.editData.url || '',
sort: this.editData.sort || 0
}
this.$nextTick(() => {
if (this.$refs.form) this.$refs.form.clearValidate()
})
} else if (val && !this.editData) {
this.isEdit = false
this.editId = null
this.resetForm()
}
}
},
methods: {
getDefaultForm () {
return {
title: undefined,
category_id: undefined,
describe: undefined,
markdown: '',
type: 'md',
role_ids: [],
url: '',
sort: 0
}
},
resetForm () {
this.form = this.getDefaultForm()
this.submitting = false
this.isEdit = false
this.editId = null
},
handleAdd () {
this.$refs.form.validate(valid => {
if (!valid) return
if (this.form.type === 'md' && !this.form.markdown) {
this.$message.warning(this.$t(`${this.pre}.content_required`))
return
}
this.submitting = true
this.$emit('submit', this.isEdit
? { id: this.editId, ...this.form }
: { ...this.form }
)
})
},
handleBeforeClose (done) {
this.$confirm(this.$t(`${this.pre}.confirm_close`))
.then(() => {
this.resetForm()
done()
})
.catch(() => {})
},
closeDrawer () {
this.resetForm()
this.$emit('update:visible', false)
},
handleUploadImages (pos, file) {
const formdata = new FormData()
formdata.append('file', file)
formdata.append('type', 'image')
axios({
url: process.env.VUE_APP_UPLOAD_PATH,
method: 'post',
data: formdata,
headers: { 'Content-Type': 'multipart/form-data' }
}).then(url => {
this.$refs.mavon.$img2Url(pos, process.env.VUE_APP_PRO_PUBLIC_URL + url.data.url)
})
}
}
}
</script>
<style scoped>
.editor-body {
padding: 10px 20px;
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<div class="custom-menu-tree">
<template v-for="item in dataList">
<el-menu-item v-if="!item.children" :key="item.id" :index="String(item.id)">
<span class="tree-icon">
<i v-if="item.type === 'md'" class="el-icon-folder-opened" style="color: rgba(144, 198, 252, 1)" />
<i v-else class="el-icon-files" style="color: rgba(144, 198, 252, 1)" />
</span>
<span class="tree-name" :title="item.name">{{ item.name }}</span>
<slot name="menu" :menu-id="item.id" :type="item.type" :file-list="{ url: item.url, name: item.url, file_type: item.file_type }" />
</el-menu-item>
<el-submenu v-else :key="item.id" :index="String(item.id)">
<template slot="title">
<i :class="[item.icon]" />
<span>{{ item.name }}</span>
<slot name="submenu" :menu-data="item" :menu-id="item.id" />
</template>
<MenuTree :data-list="item.children">
<template slot="menu" scope="ctx">
<slot name="menu" :menu-id="ctx.menuId" :type="ctx.type" :file-list="ctx.fileList" />
</template>
<template slot="submenu" scope="ctx">
<slot name="submenu" :menu-data="ctx.menuData" :menu-id="ctx.menuId" />
</template>
</MenuTree>
</el-submenu>
</template>
</div>
</template>
<script>
export default {
name: 'MenuTree',
props: {
dataList: { type: Array, default: () => [] }
}
}
</script>
<style scoped>
::v-deep .is-opened > .el-submenu__title {
border-left: 3px solid #409EFF;
}
.tree-name {
display: inline-block;
width: 58%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-left: 5px;
}
.tree-icon {
background: rgba(232, 239, 248, 1);
width: 14px;
height: 14px;
padding: 0 0 2px 2px;
border-radius: 2px;
}
</style>

View File

@@ -0,0 +1,370 @@
<template>
<d2-container>
<template #header>
<div class="problem-header">
<el-input
:placeholder="$t(key('search_placeholder'))"
v-model="searchTitle"
size="mini"
clearable
style="width:220px"
@clear="onClearSearch"
@keyup.enter.native="onSearch"
>
<el-button slot="append" icon="el-icon-search" @click="onSearch" />
</el-input>
<el-button
type="primary"
size="mini"
icon="el-icon-plus"
style="margin-left:10px"
@click="openCategoryCreate"
>
{{ $t(key('add_directory')) }}
</el-button>
</div>
</template>
<el-container class="problem-content">
<el-aside width="300px" class="problem-aside">
<div v-if="searchShow" class="search-result">
<el-card
v-for="item in searchData"
:key="item.id"
:body-style="{ padding: '10px' }"
shadow="never"
>
<div class="search-item-title" @click="handleSelect(item.id)">
<span class="tree-icon">
<i
v-if="item.type === 'md'"
class="el-icon-folder-opened"
style="color: rgba(144, 198, 252, 1)"
/>
<i v-else class="el-icon-files" style="color: rgba(144, 198, 252, 1)" />
</span>
{{ item.title }}
</div>
<div class="search-item-desc">{{ item.describe }}</div>
</el-card>
</div>
<div class="aside-toolbar">
<el-button type="primary" size="mini" icon="el-icon-plus" @click="openEditor">
{{ $t(key('add_document')) }}
</el-button>
<el-button type="success" size="mini" icon="el-icon-edit-outline" @click="openCategoryEdit" v-permission="'/setting/aide/problem/set'">
{{ $t(key('edit')) }}
</el-button>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="handleDeleteCategory" v-permission="'/setting/aide/problem/del'">
{{ $t(key('delete')) }}
</el-button>
</div>
<el-menu
class="problem-menu"
:unique-opened="true"
@select="handleSelect"
@open="handleOpen"
>
<MenuTree :data-list="problemTree">
<template slot="menu" scope="ctx">
<div class="menu-actions">
<el-button
v-if="ctx.type === 'file'"
type="text"
size="mini"
@click.stop="handleDownload(ctx.fileList.url, ctx.fileList.file_type)"
>
{{ $t(key('download')) }}
</el-button>
<el-button v-if="ctx.type === 'md'" type="text" size="mini" @click.stop="handleEditMarkdown(ctx.menuId)">
{{ $t(key('edit')) }}
</el-button>
<el-button type="text" size="mini" style="margin-left:0" @click.stop="handleDeleteMarkdown(ctx.menuId)">
{{ $t(key('delete')) }}
</el-button>
</div>
</template>
</MenuTree>
</el-menu>
</el-aside>
<el-main class="problem-main">
<el-card v-if="docVisible" class="doc-card" shadow="never">
<div slot="header" class="doc-header">
<h1 style="margin:0">{{ docDetail.title }}</h1>
<h6 style="margin:0">
{{ docDetail.create_time }}
{{ $t(key('submitter')) }}{{ docDetail.admin && docDetail.admin.name }}
</h6>
</div>
<d2-markdown :key="markdownKey" :source="docDetail.markdown" />
</el-card>
<div v-else class="doc-empty">
<i class="el-icon-document" style="font-size:64px;color:#dcdfe6" />
<p style="color:#909399">{{ $t(key('select_document_tip')) }}</p>
</div>
</el-main>
</el-container>
<category-dialog
ref="categoryDialog"
:visible.sync="categoryVisible"
:category-tree="categoryTree"
:role-list="roleList"
:pre="i18nPrefix"
@submit="onCategorySubmit"
/>
<markdown-editor
ref="editorDrawer"
:visible.sync="editorVisible"
:edit-data="editDocData"
:category-tree-data="categoryTree"
:role-list="roleList"
:pre="i18nPrefix"
@submit="onEditorSubmit"
/>
</d2-container>
</template>
<script>
import { i18nMixin } from '@/composables/useI18n'
import {
getCategoryTree,
setCategoryAdd,
setCategoryUpdate,
delCategory,
getProblemTree,
getMarkdownDetails,
setMarkdownAdd,
setMarkdownEdit,
delMarkdownDetails,
searchMarkdown,
getRoleAll
} from '@/api/system-administration/problem-help'
import MenuTree from './components/MenuTree/index.vue'
import CategoryDialog from './components/CategoryDialog/index.vue'
import MarkdownEditor from './components/MarkdownEditor/index.vue'
export default {
name: 'system-administration-problem-help',
components: { MenuTree, CategoryDialog, MarkdownEditor },
mixins: [i18nMixin('page.system_administration.system_utilities.problem_help')],
data () {
return {
loading: false,
searchTitle: '',
searchData: [],
searchShow: false,
categoryTree: [],
problemTree: [],
roleList: [],
selectedMenuId: null,
docVisible: false,
docDetail: { title: '', create_time: '', admin: {}, markdown: '' },
markdownKey: false,
categoryVisible: false,
editorVisible: false,
editDocData: null
}
},
computed: {
i18nPrefix () {
return 'page.system_administration.system_utilities.problem_help'
}
},
mounted () {
this.fetchTree()
},
methods: {
async fetchTree () {
try {
const [catRes, probRes, roleRes] = await Promise.all([
getCategoryTree(),
getProblemTree(),
getRoleAll()
])
this.categoryTree = catRes.data || []
this.problemTree = probRes.data || []
this.roleList = Array.isArray(roleRes) ? roleRes : (roleRes.data || [])
} catch { /* ignore */ }
},
onSearch () {
if (!this.searchTitle) { this.onClearSearch(); return }
searchMarkdown({ title: this.searchTitle }).then(res => {
this.searchData = res.data || []
this.searchShow = this.searchData.length > 0
})
},
onClearSearch () {
this.searchShow = false
this.searchData = []
},
handleOpen (key) {
this.selectedMenuId = key
},
handleSelect (id) {
getMarkdownDetails({ id }).then(res => {
if (res.data && res.data.type !== 'file') {
this.docDetail = res.data
this.markdownKey = !this.markdownKey
this.docVisible = true
}
})
},
handleDeleteCategory () {
if (!this.selectedMenuId) {
this.$message.error(this.$t(this.key('select_delete_directory')))
return
}
this.$confirm(this.$t(this.key('confirm_message')), this.$t(this.key('prompt')), { type: 'warning' })
.then(() => delCategory({ id: this.selectedMenuId }))
.then(() => {
this.$message.success(this.$t(this.key('delete_success')))
this.fetchTree()
})
.catch(() => {})
},
handleEditMarkdown (menuId) {
getMarkdownDetails({ id: menuId }).then(res => {
if (res.data && res.data.type !== 'file') {
this.editDocData = res.data
this.editorVisible = true
}
})
},
handleDeleteMarkdown (menuId) {
this.$confirm(this.$t(this.key('confirm_message')), this.$t(this.key('prompt')), { type: 'warning' })
.then(() => delMarkdownDetails({ id: menuId }))
.then(() => {
this.$message.success(this.$t(this.key('delete_success')))
this.fetchTree()
})
.catch(() => {})
},
handleDownload (url, fileType) {
window.open(url, '_blank')
},
openCategoryCreate () {
this.$refs.categoryDialog && this.$refs.categoryDialog.openCreate()
this.categoryVisible = true
},
openCategoryEdit () {
if (!this.selectedMenuId) {
this.$message.warning(this.$t(this.key('select_edit_directory')))
return
}
const findNode = (nodes) => {
for (const n of nodes) {
if (n.id === this.selectedMenuId) return n
if (n.children) {
const found = findNode(n.children)
if (found) return found
}
}
return null
}
const node = findNode(this.categoryTree)
if (node) {
this.$refs.categoryDialog && this.$refs.categoryDialog.openEdit(node)
this.categoryVisible = true
}
},
onCategorySubmit ({ status, id, form }) {
const api = status === 'create' ? setCategoryAdd : setCategoryUpdate
const data = status === 'create' ? form : { id, ...form }
api(data).then(() => {
this.$message.success(status === 'create'
? this.$t(this.key('add_success'))
: this.$t(this.key('update_success')))
this.categoryVisible = false
this.fetchTree()
})
},
openEditor () {
this.editDocData = null
this.editorVisible = true
},
onEditorSubmit (form) {
const promise = form.id
? setMarkdownEdit({ id: form.id, ...form })
: setMarkdownAdd(form)
promise.then(() => {
this.$message.success(this.$t(this.key(form.id ? 'edit_success' : 'create_success')))
this.editorVisible = false
this.editDocData = null
this.fetchTree()
})
}
}
}
</script>
<style scoped>
.problem-header {
padding: 10px 0;
}
.problem-content {
height: calc(100vh - 160px);
}
.problem-aside {
border: solid 1px rgba(240, 240, 240, 1);
border-radius: 4px 0 0 4px;
overflow-y: auto;
}
.problem-menu {
border-top: solid 1px rgba(240, 240, 240, 1);
}
.aside-toolbar {
padding: 10px;
margin-bottom: 10px;
}
.aside-toolbar .el-button {
margin-bottom: 4px;
}
.search-result {
max-height: 400px;
overflow: auto;
margin-bottom: 10px;
}
.search-item-title {
font-size: 14px;
padding: 3px 0;
cursor: pointer;
}
.search-item-desc {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.menu-actions {
position: absolute;
right: 40px;
top: 0;
}
.tree-icon {
background: rgba(232, 239, 248, 1);
width: 14px;
height: 14px;
padding: 0 0 2px 2px;
border-radius: 2px;
display: inline-block;
}
.problem-main {
border: solid 1px rgba(240, 240, 240, 1);
border-left: none;
border-radius: 0 4px 4px 0;
}
.doc-card {
min-height: 100%;
}
.doc-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
}
</style>

View File

@@ -14,10 +14,11 @@
node-key="menu_id"
:data="treeData"
:props="{ label: 'name', children: 'children' }"
:default-expand-all="false"
:expand-on-click-node="false"
:check-strictly="true"
:default-expand-all="true"
:expand-on-click-node="true"
:default-checked-keys="checkedMenuIds"
show-checkbox
:draggable="true"
@check="onTreeCheck"
>
<span slot-scope="{ node, data }" class="perm-drawer__tree-node">
@@ -46,14 +47,14 @@
import { i18nMixin } from '@/composables/useI18n'
import util from '@/libs/util'
import { getMenuAll } from '@/api/menu'
import { giveRoleMenu, getRoleMenu } from '@/api/system-administration/role'
import { giveRoleMenu } 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 },
role: { type: Object, default: () => ({}) },
title: { type: String, default: '' },
confirmText: { type: String, default: '' },
cancelText: { type: String, default: '' },
@@ -84,18 +85,15 @@ export default {
this.treeReady = false
this.treeLoading = true
try {
const [menuRes, roleMenuRes] = await Promise.all([
getMenuAll()
])
const menuRes = await 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.checkedMenuIds = JSON.parse(this.role.menu_admin || '[]')
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 {
setTimeout(() => {
this.treeLoading = false
}, 1000)
} catch {
this.treeLoading = false
}
},
@@ -112,7 +110,7 @@ export default {
this.submitting = true
try {
const menuIds = this.$refs.permTree.getCheckedKeys()
await giveRoleMenu({ role_id: this.roleId, role_menu: menuIds })
await giveRoleMenu({ role_id: this.role.id, role_menu: menuIds })
this.$message.success(this.$t(this.key('operation_success')))
this.visibleProxy = false
this.$emit('saved')

View File

@@ -78,7 +78,7 @@
<role-perm-drawer
:visible.sync="permVisible"
:role-id="permRole.id"
:role="permRole"
:title="key('assign_permissions')"
:confirm-text="key('confirm')"
:cancel-text="key('cancel')"
@@ -322,7 +322,7 @@ export default {
}
try {
await updateRoleStatus({
ids: rows.map(row => row.id),
id: rows.map(row => row.id),
status
})
this.$message.success(this.$t(this.key('operation_success')))

View File

@@ -0,0 +1,443 @@
<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form :inline="true" size="mini">
<el-form-item :label="$t(key('username'))">
<el-input
v-model="search.username"
:placeholder="$t(key('enter_username'))"
clearable
style="width:200px"
@keyup.enter.native="onSearch"
/>
</el-form-item>
<el-form-item :label="$t(key('full_name'))">
<el-input
v-model="search.nickname"
:placeholder="$t(key('enter_name'))"
clearable
style="width:200px"
@keyup.enter.native="onSearch"
/>
</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"
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('enable')) }}
</span>
<span v-else style="color: #909399;">
<i class="el-icon-circle-close" />
{{ $t(key('disable')) }}
</span>
</template>
</page-table>
<page-dialog-form
ref="dialogForm"
:visible.sync="dialogVisible"
:title="dialogTitle"
width="35%"
:form-cols="dialogFormCols"
:form-data="formData"
:rules="dialogRules"
label-width="100px"
:submitting="submitting"
:confirm-text="key('confirm')"
:cancel-text="key('cancel')"
@submit="onDialogSubmit"
@close="onDialogClose"
/>
</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 { getRoleAll } from '@/api/system-administration/role'
import {
getUserList,
createUser,
editUser,
deleteUser,
batchDeleteUser,
enableUser,
disableUser,
resetUserPwd
} from '@/api/system-administration/user'
import PageTable from '@/components/page-table'
import PageDialogForm from '@/components/page-dialog-form'
const ownUserId = () => localStorage.getItem('user_id')
export default {
name: 'system-administration-user',
components: { PageTable, PageDialogForm },
mixins: [i18nMixin('page.system_administration.user_management.user'), confirmMixin],
data () {
const key = this.key.bind(this)
const $t = this.$t.bind(this)
return {
loading: false,
submitting: false,
tableData: [],
selectedRows: [],
dialogVisible: false,
dialogTitle: '',
editId: '',
handleType: 'create',
search: { username: '', nickname: '' },
pagination: { current: 1, size: 10, total: 0 },
roleOptions: [],
columns: [],
toolbarButtons: [],
rowButtons: [],
baseFormCols: {
create: [
[{ type: 'input', prop: 'username', label: key('username'), placeholder: key('enter_username'), clearable: true, style: { width: '90%' } }],
[{ type: 'input', prop: 'password', inputType: 'password', label: key('password'), placeholder: key('enter_password'), clearable: true, showPassword: true, style: { width: '90%' } }],
[{ type: 'input', prop: 'password_confirm', inputType: 'password', label: key('confirm_password'), placeholder: key('enter_confirm_password'), clearable: true, showPassword: true, style: { width: '90%' } }],
[{ type: 'select', prop: 'role_id', label: key('user_group'), placeholder: key('select_user_group'), clearable: true, style: { width: '90%' }, options: [] }],
[{ type: 'input', prop: 'nickname', label: key('full_name'), placeholder: key('enter_name'), clearable: true, style: { width: '90%' } }],
[{ type: 'input', prop: 'pass_number', label: key('pass_number'), placeholder: key('enter_pass_number'), clearable: true, style: { width: '90%' } }],
[{ type: 'select', prop: 'status', label: key('status'), clearable: false, style: { width: '90%' }, options: [{ value: '1', label: $t(key('enable')) }, { value: '0', label: $t(key('disable')) }] }]
],
edit: [
[{ type: 'input', prop: 'username', label: key('username'), placeholder: key('enter_username'), clearable: true, style: { width: '90%' } }],
[{ type: 'select', prop: 'role_id', label: key('user_group'), placeholder: key('select_user_group'), clearable: true, style: { width: '90%' }, options: [] }],
[{ type: 'input', prop: 'nickname', label: key('full_name'), placeholder: key('enter_name'), clearable: true, style: { width: '90%' } }],
[{ type: 'input', prop: 'pass_number', label: key('pass_number'), placeholder: key('enter_pass_number'), clearable: true, style: { width: '90%' } }],
[{ type: 'select', prop: 'status', label: key('status'), clearable: false, style: { width: '90%' }, options: [{ value: '1', label: $t(key('enable')) }, { value: '0', label: $t(key('disable')) }] }]
]
},
baseRules: {
username: [
{ required: true, message: key('enter_username'), trigger: 'blur' },
{ min: 3, max: 20, message: key('username_length'), trigger: 'blur' }
],
password: [
{ required: true, message: key('enter_password'), trigger: 'blur' },
{ min: 6, max: 64, message: key('password_length'), trigger: 'blur' }
],
password_confirm: [
{ required: true, message: key('enter_confirm_password'), trigger: 'blur' },
{ min: 6, max: 64, message: key('password_length'), trigger: 'blur' }
],
role_id: [
{ required: true, message: key('select_user_group'), trigger: 'change' }
]
}
}
},
computed: {
dialogFormCols () {
const cols = this.baseFormCols[this.handleType] || this.baseFormCols.create
const roleIdx = this.handleType === 'create' ? 3 : 1
if (cols[roleIdx] && cols[roleIdx][0]) {
cols[roleIdx][0].options = this.roleOptions
}
return cols
},
dialogRules () {
if (this.handleType === 'edit') {
return {
username: this.baseRules.username,
role_id: this.baseRules.role_id
}
}
return this.baseRules
}
},
created () {
this.initRoleOptions()
this.columns = useTableColumns([
{ prop: 'sort', label: this.key('index'), width: 80 },
{ prop: 'username', label: this.key('username'), minWidth: 120 },
{ prop: 'nickname', label: this.key('full_name'), minWidth: 100 },
{ prop: 'pass_number', label: this.key('pass_number'), minWidth: 120 },
{ prop: 'status', label: this.key('status'), slot: 'status', width: 100 },
{ prop: 'role_name', label: this.key('user_group'), minWidth: 100 },
{ prop: 'last_ip', label: this.key('login_ip'), width: 130 },
{ prop: 'create_time', label: this.key('last_login_time'), width: 160 },
{ prop: '_actions', label: this.key('actions'), width: 240, fixed: 'right' }
])
const btns = useTableButtons({
toolbar: [
{
key: 'add',
label: this.key('add'),
icon: 'el-icon-plus',
type: 'primary',
auth: '/system_settings/user_management/member/create',
onClick: this.openAdd
},
{
key: 'enable',
label: this.key('enable'),
icon: 'el-icon-check',
type: 'success',
auth: '/system_settings/user_management/member/enable',
onClick: () => this.batchUpdateStatus(1)
},
{
key: 'disable',
label: this.key('disable'),
icon: 'el-icon-close',
type: 'warning',
auth: '/system_settings/user_management/member/disable',
onClick: () => this.batchUpdateStatus(0)
},
{
key: 'batch_delete',
label: this.key('batch_delete'),
icon: 'el-icon-delete',
type: 'danger',
auth: '/system_settings/user_management/member/batch-delete',
onClick: this.handleBatchDelete
}
],
row: [
{
key: 'edit',
label: this.key('edit'),
icon: 'el-icon-edit',
auth: '/system_settings/user_management/member/edit',
onClick: this.openEdit
},
{
key: 'reset_pwd',
label: this.key('reset_password'),
icon: 'el-icon-refresh',
auth: '/system_settings/user_management/member/reset-pwd',
onClick: this.handleResetPwd
},
{
key: 'delete',
label: this.key('delete'),
icon: 'el-icon-delete',
color: 'danger',
auth: '/system_settings/user_management/member/delete',
onClick: this.handleDelete
}
]
}, this.$permission)
this.toolbarButtons = btns.toolbarButtons
this.rowButtons = btns.rowButtons
this.fetchData()
},
methods: {
async initRoleOptions () {
try {
const res = await getRoleAll()
const data = Array.isArray(res) ? res : (res.data || [])
this.roleOptions = data.map(item => ({ value: item.id, label: item.name }))
} catch { /* 忽略 */ }
},
async fetchData () {
this.loading = true
try {
const res = await getUserList({
...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 = { username: '', nickname: '' }
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 = {
username: '',
password: '',
password_confirm: '',
role_id: '',
nickname: '',
pass_number: '',
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.user_id
this.formData = {
username: row.username,
role_id: row.role_id,
nickname: row.nickname || '',
pass_number: row.pass_number || '',
status: String(row.status)
}
this.dialogVisible = true
},
async onDialogSubmit () {
this.submitting = true
try {
if (this.handleType === 'create') {
if (this.formData.password !== this.formData.password_confirm) {
this.$message.error(this.$t(this.key('password_not_match')))
return
}
await createUser(this.formData)
} else {
await editUser({ ...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()
},
batchUpdateStatus (status) {
const uid = ownUserId()
const rows = this.selectedRows.filter(row => String(row.user_id) !== uid)
if (rows.length === 0 && this.selectedRows.length > 0) {
this.$message.warning(this.$t(this.key('cannot_operate_self')))
return
}
if (rows.length === 0) {
this.$message.warning(this.$t(this.key('select_rows_first')))
return
}
const ids = rows.map(row => row.user_id)
this.$confirm(
this.$t(this.key('confirm_execute')),
this.$t(this.key('prompt')),
{
confirmButtonText: this.$t(this.key('confirm')),
cancelButtonText: this.$t(this.key('cancel')),
type: 'warning',
closeOnClickModal: false
}
).then(async () => {
try {
await (status === 1 ? enableUser({ id: ids, status }) : disableUser({ id: ids, status: 0 }))
this.$message.success(this.$t(this.key('operation_success')))
this.fetchData()
} catch { /* 拦截器已处理 */ }
}).catch(() => {})
},
async handleDelete (row) {
if (String(row.user_id) === ownUserId()) {
this.$message.warning(this.$t(this.key('cannot_delete_self')))
return
}
const cancelled = await this.$confirmAction(
{ message: this.key('confirm_delete'), title: this.key('prompt') },
() => deleteUser({ id: [row.user_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 handleBatchDelete () {
const uid = ownUserId()
const rows = this.selectedRows.filter(row => String(row.user_id) !== uid)
if (rows.length === 0 && this.selectedRows.length > 0) {
this.$message.warning(this.$t(this.key('cannot_delete_self')))
return
}
if (rows.length === 0) {
this.$message.warning(this.$t(this.key('select_rows_first')))
return
}
const cancelled = await this.$confirmAction(
{ message: this.key('confirm_batch_delete'), title: this.key('prompt') },
() => batchDeleteUser({ id: rows.map(row => row.user_id) })
)
if (cancelled) return
this.$message.success(this.$t(this.key('operation_success')))
this.fetchData()
},
async handleResetPwd (row) {
try {
await this.$confirm(
this.$t(this.key('confirm_reset_pwd')),
this.$t(this.key('prompt')),
{
confirmButtonText: this.$t(this.key('confirm')),
cancelButtonText: this.$t(this.key('cancel')),
type: 'warning',
closeOnClickModal: false
}
)
await resetUserPwd({ id: row.user_id })
this.$message.success(this.$t(this.key('operation_success')))
} catch { /* 取消或失败 */ }
}
}
}
</script>
<style scoped>
.search-bar {
padding: 10px 0;
}
/deep/ .el-form-item--mini.el-form-item {
margin-bottom: 4px;
}
</style>

View File

@@ -73,8 +73,8 @@ export default {
return this.aside[key].children.filter(item => {
const title = item.title || ''
if (title.indexOf('首页') !== -1) return false
if (item.icon === 'home') return false
if (this.$route.path === item.path) return false
// if (item.icon === 'home') return false
// if (this.$route.path === item.path) return false
return true
})
}