mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow
This commit is contained in:
73
.drone.yml
Normal file
73
.drone.yml
Normal file
@ -0,0 +1,73 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: Build and Deploy
|
||||
|
||||
clone:
|
||||
depth: 10
|
||||
|
||||
volumes:
|
||||
- name: go_cache
|
||||
host:
|
||||
path: /data/drone_cache/go_cache
|
||||
|
||||
steps:
|
||||
- name: restore-cache
|
||||
image: drillster/drone-volume-cache
|
||||
volumes:
|
||||
- name: go_cache
|
||||
path: /cache
|
||||
settings:
|
||||
restore: true
|
||||
mount:
|
||||
- ./.npm-cache
|
||||
- ./node_modules
|
||||
|
||||
- name: build
|
||||
image: node:alpine
|
||||
pull: if-not-exists
|
||||
commands:
|
||||
- export NODE_OPTIONS=--max_old_space_size=6144
|
||||
- echo ${DRONE_BRANCH}
|
||||
- echo ${DRONE_TAG}
|
||||
- echo ${DRONE_COMMIT}
|
||||
- echo ${DRONE_COMMIT:0-7}
|
||||
- npm config set registry https://registry.npmmirror.com
|
||||
- npm install -g pnpm
|
||||
- pnpm config set registry https://registry.npmmirror.com
|
||||
- pnpm i
|
||||
- pnpm build
|
||||
|
||||
- name: rebuild-cache
|
||||
image: drillster/drone-volume-cache
|
||||
volumes:
|
||||
- name: go_cache
|
||||
path: /cache
|
||||
settings:
|
||||
rebuild: true
|
||||
mount:
|
||||
- ./.npm-cache
|
||||
- ./node_modules
|
||||
|
||||
- name: scp files
|
||||
image: appleboy/drone-scp
|
||||
pull: if-not-exists
|
||||
settings:
|
||||
host:
|
||||
from_secret: HOST
|
||||
username:
|
||||
from_secret: USERNAME
|
||||
password:
|
||||
from_secret: PASSWORD
|
||||
port:
|
||||
from_secret: PORT
|
||||
target:
|
||||
from_secret: TARGET_PATH
|
||||
source: dist/*
|
||||
overwrite: true
|
||||
rm: true
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
event:
|
||||
- push
|
@ -1,30 +0,0 @@
|
||||
---
|
||||
name: Lint Code
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint All Code
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Lint Code Base
|
||||
uses: github/super-linter@v4
|
||||
env:
|
||||
VALIDATE_ALL_CODEBASE: false
|
||||
DEFAULT_BRANCH: main
|
||||
# To change branch master or main
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FILTER_REGEX_EXCLUDE: (docs|.github)
|
||||
VALIDATE_MARKDOWN: false
|
@ -1,25 +0,0 @@
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- run: npx githublogen
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
51
.github/workflows/deploy.yml
vendored
Normal file
51
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'LICENSE'
|
||||
- 'docs/**'
|
||||
- '.vscode/**'
|
||||
- '.github/**'
|
||||
- '.gitee/**'
|
||||
- '.codeif/**'
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.x
|
||||
run_install: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Upload via SCP
|
||||
uses: appleboy/scp-action@v1
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
username: ${{ secrets.SSH_USERNAME }}
|
||||
password: ${{ secrets.SSH_PASSWORD }}
|
||||
port: ${{ secrets.SSH_PORT }}
|
||||
source: "dist/*"
|
||||
target: ${{ secrets.TARGET_PATH }}
|
||||
rm: true
|
||||
overwrite: true
|
4
.github/workflows/linter.yml
vendored
4
.github/workflows/linter.yml
vendored
@ -6,7 +6,7 @@ permissions:
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@ -23,7 +23,7 @@ jobs:
|
||||
uses: github/super-linter@v4
|
||||
env:
|
||||
VALIDATE_ALL_CODEBASE: false
|
||||
DEFAULT_BRANCH: main
|
||||
DEFAULT_BRANCH: master
|
||||
# To change branch master or main
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FILTER_REGEX_EXCLUDE: (docs|.github)
|
||||
|
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@ -0,0 +1,13 @@
|
||||
# 更新日志
|
||||
|
||||
## [v1.0.0](https://gitee.com/xlsea/ruoyi-plus-soybean/releases/tag/v1.0.0) (2025-06-05)
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
1.0.0 版本正式发布,此版本不包含工作流与多语言,请期待后续版本发布。
|
||||
|
||||
### ❤️ 贡献者
|
||||
|
||||
首次发版不展示过多贡献者,敬请谅解
|
||||
|
||||
[](https://github.com/honghuangdc) [](https://gitee.com/xlsea) [](https://gitee.com/elio-an) [](https://github.com/wangqiqi95)
|
26
README.md
26
README.md
@ -7,7 +7,8 @@
|
||||
<div align="center">
|
||||
<p>一个基于 <a href="https://gitee.com/dromara/RuoYi-Vue-Plus" target="_blank">RuoYi-Vue-Plus</a> 的后端能力和 <a href="https://github.com/soybeanjs/soybean-admin" target="_blank">Soybean Admin</a> 前端特性的现代化多租户管理系统</p>
|
||||
<p>
|
||||
<img src="https://gitee.com/xlsea/ruoyi-plus-soybean/badge/star.svg?theme=blue" alt="Gitee">
|
||||
<img src="https://gitee.com/xlsea/ruoyi-plus-soybean/badge/star.svg" alt="Gitee">
|
||||
<img src="https://img.shields.io/github/stars/m-xlsea/ruoyi-plus-soybean" alt="Github">
|
||||
<img src="https://img.shields.io/badge/Vue-3.5-brightgreen" alt="vue">
|
||||
<img src="https://img.shields.io/badge/TypeScript-5.8-blue" alt="typescript">
|
||||
<img src="https://img.shields.io/badge/Vite-6.2-orange" alt="vite">
|
||||
@ -18,9 +19,17 @@
|
||||
|
||||
# 📢 重要通知
|
||||
|
||||
<p style="color: red; font-weight: bold; font-size: 24px;">该项目未首发公测版本,请谨慎在生产环境使用!!!</p>
|
||||
<p style="color: red; font-weight: bold; font-size: 24px;">该项目未首发公测版本,请谨慎在生产环境使用!!!</p>
|
||||
<p style="color: red; font-weight: bold; font-size: 24px;">该项目未首发公测版本,请谨慎在生产环境使用!!!</p>
|
||||
虽然 1.0.0 版本已经包含了完整的核心功能,但我们仍然建议:
|
||||
- 在生产环境使用前进行充分测试
|
||||
- 关注项目更新,及时获取最新版本
|
||||
- 积极反馈问题,帮助我们快速迭代
|
||||
|
||||
**后续规划**
|
||||
- 工作流引擎集成
|
||||
- 多语言国际化完善
|
||||
- 更多企业级功能模块
|
||||
- 性能优化和稳定性提升
|
||||
|
||||
|
||||
> 如果对该项目感兴趣,可以给一个 Star 支持一下,谢谢!
|
||||
> 请大家踊跃提交 PR 和 Issue,一起完善这个项目
|
||||
@ -353,7 +362,9 @@ console.log(t('common.confirm'));
|
||||
|
||||
## 💬 交流群
|
||||
|
||||
<img src="https://foruda.gitee.com/images/1748404753216665472/3d8b1a0b_5601833.png" width="300px" />
|
||||
<img src="https://foruda.gitee.com/images/1749174520085305975/ad1b54fe_5601833.png" width="300px" />
|
||||
|
||||
添加作者微信备注:加群
|
||||
|
||||
## 🧧 捐献作者
|
||||
|
||||
@ -416,3 +427,8 @@ console.log(t('common.confirm'));
|
||||
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747796468040874363/1faa75ce_5601833.jpeg" width="24px" >
|
||||
<span>莫离支🤴 10元</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1749191537723692930/0b810403_5601833.jpeg" width="24px" >
|
||||
<span>Ivan 100元</span>
|
||||
</div>
|
||||
|
42
package.json
42
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ruoyi-vue-plus",
|
||||
"type": "module",
|
||||
"version": "1.0.0-beta.1",
|
||||
"version": "1.0.0",
|
||||
"description": "RuoYi-Vue-Plus多租户管理系统",
|
||||
"author": {
|
||||
"name": "xlsea",
|
||||
@ -57,7 +57,7 @@
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/tinymce": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@vueuse/core": "13.1.0",
|
||||
"@vueuse/core": "13.3.0",
|
||||
"clipboard": "2.0.11",
|
||||
"dayjs": "1.11.13",
|
||||
"defu": "6.1.4",
|
||||
@ -66,42 +66,42 @@
|
||||
"jsencrypt": "^3.3.2",
|
||||
"json5": "2.2.3",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"naive-ui": "2.41.0",
|
||||
"naive-ui": "2.41.1",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "3.0.2",
|
||||
"tailwind-merge": "3.2.0",
|
||||
"vue": "3.5.13",
|
||||
"pinia": "3.0.3",
|
||||
"tailwind-merge": "3.3.0",
|
||||
"vue": "3.5.16",
|
||||
"vue-advanced-cropper": "^2.8.9",
|
||||
"vue-draggable-plus": "0.6.0",
|
||||
"vue-i18n": "11.1.3",
|
||||
"vue-i18n": "11.1.5",
|
||||
"vue-router": "4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify/json": "2.2.337",
|
||||
"@iconify/json": "2.2.347",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.6.0",
|
||||
"@types/node": "22.15.17",
|
||||
"@soybeanjs/eslint-config": "1.6.1",
|
||||
"@types/node": "22.15.30",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "66.1.1",
|
||||
"@unocss/preset-icons": "66.1.1",
|
||||
"@unocss/preset-uno": "66.1.1",
|
||||
"@unocss/transformer-directives": "66.1.1",
|
||||
"@unocss/transformer-variant-group": "66.1.1",
|
||||
"@unocss/vite": "66.1.1",
|
||||
"@unocss/eslint-config": "66.1.4",
|
||||
"@unocss/preset-icons": "66.1.4",
|
||||
"@unocss/preset-uno": "66.1.4",
|
||||
"@unocss/transformer-directives": "66.1.4",
|
||||
"@unocss/transformer-variant-group": "66.1.4",
|
||||
"@unocss/vite": "66.1.4",
|
||||
"@vitejs/plugin-vue": "5.2.4",
|
||||
"@vitejs/plugin-vue-jsx": "4.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "4.2.0",
|
||||
"consola": "3.4.2",
|
||||
"eslint": "9.26.0",
|
||||
"eslint-plugin-vue": "10.1.0",
|
||||
"eslint": "9.28.0",
|
||||
"eslint-plugin-vue": "10.2.0",
|
||||
"kolorist": "1.8.0",
|
||||
"sass": "1.88.0",
|
||||
"sass": "1.89.1",
|
||||
"simple-git-hooks": "2.13.0",
|
||||
"tsx": "4.19.4",
|
||||
"typescript": "5.8.3",
|
||||
"unplugin-icons": "22.1.0",
|
||||
"unplugin-vue-components": "28.5.0",
|
||||
"unplugin-vue-components": "28.7.0",
|
||||
"vite": "6.3.5",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/alova",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./fetch": "./src/fetch.ts",
|
||||
@ -13,8 +13,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@alova/mock": "2.0.14",
|
||||
"@alova/mock": "2.0.16",
|
||||
"@sa/utils": "workspace:*",
|
||||
"alova": "3.2.10"
|
||||
"alova": "3.3.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/axios",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
@ -16,6 +16,6 @@
|
||||
"qs": "6.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qs": "6.9.18"
|
||||
"@types/qs": "6.14.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/color",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/hooks",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/materials",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/fetch",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/scripts",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"bin": {
|
||||
"sa": "./bin.ts"
|
||||
},
|
||||
@ -14,12 +14,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@soybeanjs/changelog": "0.3.24",
|
||||
"bumpp": "10.1.0",
|
||||
"c12": "3.0.3",
|
||||
"bumpp": "10.1.1",
|
||||
"c12": "3.0.4",
|
||||
"cac": "6.7.14",
|
||||
"consola": "3.4.2",
|
||||
"enquirer": "2.4.1",
|
||||
"execa": "9.5.3",
|
||||
"execa": "9.6.0",
|
||||
"kolorist": "1.8.0",
|
||||
"npm-check-updates": "18.0.1",
|
||||
"rimraf": "6.0.1"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/uno-preset",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/utils",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
2217
pnpm-lock.yaml
generated
2217
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1
src/assets/svg-icon/bell.svg
Normal file
1
src/assets/svg-icon/bell.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg data-v-0a0a4a97="" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell-icon size-4"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"></path><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"></path></svg>
|
After Width: | Height: | Size: 353 B |
35
src/components/common/data-table.vue
Normal file
35
src/components/common/data-table.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { useAttrs } from 'vue';
|
||||
import type { DataTableProps } from 'naive-ui';
|
||||
import type { CreateRowKey } from 'naive-ui/es/data-table/src/interface';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
defineOptions({
|
||||
name: 'DataTable',
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
interface Props {
|
||||
rowKey?: CreateRowKey<any>;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const { table } = useThemeStore();
|
||||
const attrs: DataTableProps = useAttrs();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDataTable
|
||||
:bordered="table.bordered"
|
||||
:bottom-bordered="table.bottomBordered"
|
||||
:single-column="table.singleColumn"
|
||||
:single-line="table.singleLine"
|
||||
:size="table.size"
|
||||
:striped="table.striped"
|
||||
:row-key="rowKey"
|
||||
v-bind="attrs"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -2,7 +2,7 @@
|
||||
import { useAttrs } from 'vue';
|
||||
import type { TreeSelectProps } from 'naive-ui';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { fetchGetDeptSelect } from '@/service/api/system';
|
||||
import { fetchGetDeptTree } from '@/service/api/system';
|
||||
|
||||
defineOptions({ name: 'DeptTreeSelect' });
|
||||
|
||||
@ -13,16 +13,21 @@ interface Props {
|
||||
defineProps<Props>();
|
||||
|
||||
const value = defineModel<CommonType.IdType | null>('value', { required: false });
|
||||
const options = defineModel<Api.System.Dept[]>('options', { required: false, default: [] });
|
||||
const options = defineModel<Api.Common.CommonTreeRecord>('options', { required: false, default: [] });
|
||||
const expandedKeys = defineModel<CommonType.IdType[]>('expandedKeys', { required: false, default: [] });
|
||||
|
||||
const attrs: TreeSelectProps = useAttrs();
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
async function getDeptList() {
|
||||
startLoading();
|
||||
const { error, data } = await fetchGetDeptSelect();
|
||||
const { error, data } = await fetchGetDeptTree();
|
||||
if (error) return;
|
||||
options.value = data;
|
||||
// 设置默认展开的节点
|
||||
if (data?.length && !expandedKeys.value.length) {
|
||||
expandedKeys.value = [data[0].id];
|
||||
}
|
||||
endLoading();
|
||||
}
|
||||
|
||||
@ -32,13 +37,13 @@ getDeptList();
|
||||
<template>
|
||||
<NTreeSelect
|
||||
v-model:value="value"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
filterable
|
||||
class="h-full"
|
||||
:loading="loading"
|
||||
key-field="deptId"
|
||||
label-field="deptName"
|
||||
:options="options"
|
||||
:default-expanded-keys="[0]"
|
||||
key-field="id"
|
||||
label-field="label"
|
||||
:options="options as []"
|
||||
v-bind="attrs"
|
||||
/>
|
||||
</template>
|
||||
|
@ -3,6 +3,7 @@ import { computed, useAttrs } from 'vue';
|
||||
import type { TagProps } from 'naive-ui';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { isNotNull } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'DictTag' });
|
||||
|
||||
@ -23,13 +24,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const attrs = useAttrs() as TagProps;
|
||||
|
||||
const { transformDictData } = useDict(props.dictCode, props.immediate);
|
||||
|
||||
const dictTagData = computed<Api.System.DictData[]>(() => {
|
||||
if (props.dictData) {
|
||||
return [props.dictData];
|
||||
const dictData = props.dictData;
|
||||
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
|
||||
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
|
||||
}
|
||||
return [dictData];
|
||||
}
|
||||
// 避免 props.value 为 0 时,无法触发
|
||||
if (props.dictCode && isNotNull(props.value)) {
|
||||
const { transformDictData } = useDict(props.dictCode, props.immediate);
|
||||
return transformDictData(props.value) || [];
|
||||
}
|
||||
|
||||
@ -44,8 +50,8 @@ const dictTagData = computed<Api.System.DictData[]>(() => {
|
||||
:key="item.dictValue"
|
||||
class="m-1"
|
||||
:class="[item.cssClass]"
|
||||
:type="item.listClass"
|
||||
v-bind="attrs"
|
||||
:type="item.listClass || 'default'"
|
||||
>
|
||||
{{ item.dictLabel }}
|
||||
</NTag>
|
||||
|
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs, watch } from 'vue';
|
||||
import { computed, useAttrs } from 'vue';
|
||||
import type { UploadFileInfo, UploadProps } from 'naive-ui';
|
||||
import { fetchBatchDeleteOss } from '@/service/api/system/oss';
|
||||
import { getToken } from '@/store/modules/auth/shared';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { AcceptType } from '@/enum/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'FileUpload'
|
||||
@ -26,30 +27,24 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
defaultUpload: true,
|
||||
showTip: true,
|
||||
max: 5,
|
||||
accept: '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf',
|
||||
accept: undefined,
|
||||
fileSize: 5,
|
||||
uploadType: 'file'
|
||||
});
|
||||
|
||||
const accept = computed(() => {
|
||||
if (props.accept) {
|
||||
return props.accept;
|
||||
}
|
||||
return props.uploadType === 'file' ? AcceptType.File : AcceptType.Image;
|
||||
});
|
||||
|
||||
const attrs: UploadProps = useAttrs();
|
||||
|
||||
const value = defineModel<CommonType.IdType[]>('value', { required: false, default: [] });
|
||||
|
||||
let fileNum = 0;
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
const needRelaodData = ref<boolean>(false);
|
||||
defineExpose({
|
||||
refreshList: needRelaodData,
|
||||
fileList
|
||||
const fileList = defineModel<UploadFileInfo[]>('fileList', {
|
||||
default: () => []
|
||||
});
|
||||
watch(
|
||||
() => fileList.value,
|
||||
newValue => {
|
||||
needRelaodData.value = newValue.length > 0;
|
||||
value.value = newValue.map(item => item.id);
|
||||
}
|
||||
);
|
||||
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||
@ -64,12 +59,12 @@ function beforeUpload(options: { file: UploadFileInfo; fileList: UploadFileInfo[
|
||||
const { file } = options;
|
||||
|
||||
// 校检文件类型
|
||||
if (props.accept) {
|
||||
if (accept.value) {
|
||||
const fileName = file.name.split('.');
|
||||
const fileExt = `.${fileName[fileName.length - 1]}`;
|
||||
const isTypeOk = props.accept.split(',')?.includes(fileExt);
|
||||
const isTypeOk = accept.value.split(',')?.includes(fileExt);
|
||||
if (!isTypeOk) {
|
||||
window.$message?.error(`文件格式不正确, 请上传 ${props.accept} 格式文件!`);
|
||||
window.$message?.error(`文件格式不正确, 请上传 ${accept.value} 格式文件!`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -122,11 +117,12 @@ function handleError(options: { file: UploadFileInfo; event?: ProgressEvent }) {
|
||||
|
||||
async function handleRemove(file: UploadFileInfo) {
|
||||
if (file.status !== 'finished') {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const { error } = await fetchBatchDeleteOss([file.id]);
|
||||
if (error) return;
|
||||
if (error) return false;
|
||||
window.$message?.success('删除成功');
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -9,11 +9,13 @@ defineOptions({ name: 'MenuTree' });
|
||||
|
||||
interface Props {
|
||||
immediate?: boolean;
|
||||
showHeader?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
showHeader: true
|
||||
});
|
||||
|
||||
const { bool: expandAll } = useBoolean();
|
||||
@ -49,7 +51,6 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// 添加 watch 监听 expandAll 的变化,options有值后,计算expandedKeys
|
||||
watch([expandAll, options], ([newVal]) => {
|
||||
if (newVal) {
|
||||
// 展开所有节点
|
||||
@ -80,6 +81,21 @@ function getAllMenuIds(menu: Api.System.MenuList) {
|
||||
return menuIds;
|
||||
}
|
||||
|
||||
/** 获取所有叶子节点的 ID(没有子节点的节点) */
|
||||
function getLeafMenuIds(menu: Api.System.MenuList): CommonType.IdType[] {
|
||||
const leafIds: CommonType.IdType[] = [];
|
||||
menu.forEach(item => {
|
||||
if (!item.children || item.children.length === 0) {
|
||||
// 是叶子节点
|
||||
leafIds.push(item.id!);
|
||||
} else {
|
||||
// 有子节点,递归获取子节点中的叶子节点
|
||||
leafIds.push(...getLeafMenuIds(item.children));
|
||||
}
|
||||
});
|
||||
return leafIds;
|
||||
}
|
||||
|
||||
function handleCheckedTreeNodeAll(checked: boolean) {
|
||||
if (checked) {
|
||||
checkedKeys.value = getAllMenuIds(options.value);
|
||||
@ -88,16 +104,30 @@ function handleCheckedTreeNodeAll(checked: boolean) {
|
||||
checkedKeys.value = [];
|
||||
}
|
||||
|
||||
function getCheckedMenuIds() {
|
||||
function getCheckedMenuIds(isCascade: boolean = false) {
|
||||
const menuIds = menuTreeRef.value?.getCheckedData()?.keys as string[];
|
||||
const indeterminateData = menuTreeRef.value?.getIndeterminateData();
|
||||
if (cascade.value) {
|
||||
if (cascade.value || isCascade) {
|
||||
const parentIds: string[] = indeterminateData?.keys.filter(item => !menuIds?.includes(String(item))) as string[];
|
||||
menuIds?.push(...parentIds);
|
||||
}
|
||||
return menuIds;
|
||||
}
|
||||
|
||||
watch(cascade, () => {
|
||||
if (cascade.value) {
|
||||
// 获取当前菜单树中的所有叶子节点ID
|
||||
const allLeafIds = getLeafMenuIds(options.value);
|
||||
// 筛选出当前选中项中的叶子节点
|
||||
const selectedLeafIds = checkedKeys.value.filter(id => allLeafIds.includes(id));
|
||||
// 重新设置选中状态为只包含叶子节点,让组件基于父子联动规则重新计算父节点状态
|
||||
checkedKeys.value = selectedLeafIds;
|
||||
return;
|
||||
}
|
||||
// 禁用父子联动时,将半选中的父节点也加入到选中列表
|
||||
checkedKeys.value = getCheckedMenuIds(true);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
getCheckedMenuIds,
|
||||
refresh: getMenuList
|
||||
@ -106,7 +136,7 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div class="w-full flex-col gap-12px">
|
||||
<div class="w-full flex-center">
|
||||
<div v-if="showHeader" class="w-full flex-center">
|
||||
<NCheckbox v-model:checked="expandAll" :checked-value="true" :unchecked-value="false">展开/折叠</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="checkAll"
|
||||
|
68
src/components/custom/oss-upload.vue
Normal file
68
src/components/custom/oss-upload.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs, watch } from 'vue';
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { fetchGetOssListByIds } from '@/service/api/system/oss';
|
||||
import { isNotNull } from '@/utils/common';
|
||||
import FileUpload from '@/components/custom/file-upload.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'OssUpload'
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
const value = defineModel<string>('value', { default: '' });
|
||||
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
async function handleFetchOssList(ossIds: string[]) {
|
||||
startLoading();
|
||||
const { error, data } = await fetchGetOssListByIds(ossIds);
|
||||
if (error) return;
|
||||
fileList.value = data.map(item => ({
|
||||
id: String(item.ossId),
|
||||
url: item.url,
|
||||
name: item.originalName,
|
||||
status: 'finished'
|
||||
}));
|
||||
endLoading();
|
||||
}
|
||||
|
||||
watch(
|
||||
value,
|
||||
async val => {
|
||||
const ossIds = val.split(',')?.filter(item => isNotNull(item));
|
||||
const fileIds = new Set(fileList.value.filter(item => item.status === 'finished').map(item => item.id));
|
||||
if (ossIds.every(item => fileIds.has(item))) {
|
||||
return;
|
||||
}
|
||||
if (ossIds.length === 0) {
|
||||
fileList.value = [];
|
||||
return;
|
||||
}
|
||||
await handleFetchOssList(ossIds);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
fileList,
|
||||
val => {
|
||||
value.value = val
|
||||
.filter(item => item.status === 'finished')
|
||||
.map(item => item.id)
|
||||
.join(',');
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpin v-if="loading" />
|
||||
<FileUpload v-else v-bind="attrs" v-model:file-list="fileList" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -63,3 +63,11 @@ export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I
|
||||
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
|
||||
|
||||
export const DARK_CLASS = 'dark';
|
||||
|
||||
export const themeTableSizeRecord: Record<UnionKey.ThemeTableSize, App.I18n.I18nKey> = {
|
||||
small: 'theme.table.size.small',
|
||||
medium: 'theme.table.size.medium',
|
||||
large: 'theme.table.size.large'
|
||||
};
|
||||
|
||||
export const themeTableSizeOptions = transformRecordToOption(themeTableSizeRecord);
|
||||
|
4
src/enum/business.ts
Normal file
4
src/enum/business.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum AcceptType {
|
||||
Image = '.jpg,.jpeg,.png,.gif,.bmp,.webp',
|
||||
File = '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf,.zip,.rar,.7z'
|
||||
}
|
@ -3,5 +3,6 @@ export enum SetupStoreId {
|
||||
Theme = 'theme-store',
|
||||
Auth = 'auth-store',
|
||||
Route = 'route-store',
|
||||
Tab = 'tab-store'
|
||||
Tab = 'tab-store',
|
||||
Notice = 'notice-store'
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ import { storeToRefs } from 'pinia';
|
||||
import { fetchGetDictDataByType } from '@/service/api/system';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { isNull } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export function useDict(dictType: string, immediate: boolean = true) {
|
||||
const dictStore = useDictStore();
|
||||
const { dictData: dictList } = storeToRefs(dictStore);
|
||||
@ -19,6 +21,11 @@ export function useDict(dictType: string, immediate: boolean = true) {
|
||||
}
|
||||
const { data: dictData, error } = await fetchGetDictDataByType(dictType);
|
||||
if (error) return;
|
||||
dictData.forEach(dict => {
|
||||
if (dict.dictLabel?.startsWith(`dict.${dictType}.`)) {
|
||||
dict.dictLabel = $t(dict.dictLabel as App.I18n.I18nKey);
|
||||
}
|
||||
});
|
||||
dictStore.setDict(dictType, dictData);
|
||||
data.value = dictData;
|
||||
}
|
||||
|
152
src/layouts/modules/global-header/components/message-button.vue
Normal file
152
src/layouts/modules/global-header/components/message-button.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useNoticeStore } from '@/store/modules/notice';
|
||||
|
||||
defineOptions({
|
||||
name: 'MessgaeButton'
|
||||
});
|
||||
|
||||
const show = ref(false);
|
||||
const noticeStore = useNoticeStore();
|
||||
const { state } = storeToRefs(noticeStore);
|
||||
|
||||
const noticeNum = computed(() => {
|
||||
return state.value.notices.filter(notice => !notice.read).length || 0;
|
||||
});
|
||||
|
||||
const toGitee = () => {
|
||||
window.open('https://gitee.com/xlsea/ruoyi-plus-soybean', '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NPopover v-model:show="show" trigger="click" arrow-point-to-center raw class="border-rounded-6px">
|
||||
<template #trigger>
|
||||
<NTooltip :disabled="show">
|
||||
<template #trigger>
|
||||
<NButton quaternary class="bell-button h-36px text-icon" :focusable="false">
|
||||
<NBadge :value="noticeNum" :max="99" :offset="[2, -2]">
|
||||
<div class="bell-icon flex-center gap-8px">
|
||||
<SvgIcon local-icon="bell" />
|
||||
</div>
|
||||
</NBadge>
|
||||
</NButton>
|
||||
</template>
|
||||
消息
|
||||
</NTooltip>
|
||||
</template>
|
||||
<NCard
|
||||
size="small"
|
||||
:bordered="false"
|
||||
class="w-345px"
|
||||
header-class="p-0"
|
||||
:segmented="{ content: true, footer: 'soft' }"
|
||||
>
|
||||
<template #header>
|
||||
<span>通知公告</span>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NTooltip placement="left" :z-index="98">
|
||||
<template #trigger>
|
||||
<NPopconfirm @positive-click="() => noticeStore.readAll()">
|
||||
<template #trigger>
|
||||
<NButton quaternary>
|
||||
<div class="flex-center gap-8px">
|
||||
<SvgIcon icon="lucide:mail-check" class="text-16px" />
|
||||
</div>
|
||||
</NButton>
|
||||
</template>
|
||||
确定全部已读吗?
|
||||
</NPopconfirm>
|
||||
</template>
|
||||
一键已读
|
||||
</NTooltip>
|
||||
</template>
|
||||
<NScrollbar class="h-260px">
|
||||
<template v-if="state?.notices?.length">
|
||||
<template v-for="(message, index) in state?.notices" :key="index">
|
||||
<NDivider v-show="index !== 0" />
|
||||
<div class="flex cursor-pointer" @click="() => noticeStore.readNotice(message)">
|
||||
<div class="flex-col justify-between gap-3px">
|
||||
<NEllipsis class="w-260px">{{ message.message }}</NEllipsis>
|
||||
<span class="text-#898989">
|
||||
{{ message.time }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<NTag :type="message.read ? 'success' : 'error'">{{ message.read ? '已读' : '未读' }}</NTag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<NEmpty v-else class="h-180px flex-center" />
|
||||
</NScrollbar>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-end">
|
||||
<NButton type="primary" size="small" @click="toGitee">前往 Gitee</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NCard>
|
||||
</NPopover>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.n-divider) {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
:deep(.n-thing-header) {
|
||||
margin-bottom: 1px !important;
|
||||
}
|
||||
|
||||
:deep(.n-thing-main__content) {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.messgae-popover) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.n-badge-sup) {
|
||||
padding: 0 5px !important;
|
||||
font-size: 10px !important;
|
||||
height: 15px !important;
|
||||
line-height: 15px !important;
|
||||
}
|
||||
|
||||
.bell-button {
|
||||
&:hover {
|
||||
.bell-icon {
|
||||
animation: bell-ring 1s both;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bell-ring {
|
||||
0%,
|
||||
100% {
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
15% {
|
||||
transform: rotateZ(10deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: rotateZ(-10deg);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: rotateZ(5deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotateZ(-5deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotateZ(2deg);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -10,6 +10,7 @@ import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
|
||||
import GlobalSearch from '../global-search/index.vue';
|
||||
import ThemeButton from './components/theme-button.vue';
|
||||
import UserAvatar from './components/user-avatar.vue';
|
||||
import MessageButton from './components/message-button.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'GlobalHeader'
|
||||
@ -44,7 +45,8 @@ const tenantId = ref<CommonType.IdType>(authStore.userInfo?.user?.tenantId || '0
|
||||
</div>
|
||||
<div class="h-full flex-y-center justify-end">
|
||||
<TenantSelect v-if="!appStore.isMobile" v-model:value="tenantId" class="mr-12px w-150px" />
|
||||
<GlobalSearch />
|
||||
<GlobalSearch v-if="themeStore.header.globalSearch.visible" />
|
||||
<MessageButton />
|
||||
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
|
||||
<LangSwitch
|
||||
v-if="themeStore.header.multilingual.visible"
|
||||
|
@ -6,6 +6,7 @@ import LayoutMode from './modules/layout-mode.vue';
|
||||
import ThemeColor from './modules/theme-color.vue';
|
||||
import PageFun from './modules/page-fun.vue';
|
||||
import ConfigOperation from './modules/config-operation.vue';
|
||||
import TableProps from './modules/table-props.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ThemeDrawer'
|
||||
@ -21,6 +22,7 @@ const appStore = useAppStore();
|
||||
<LayoutMode />
|
||||
<ThemeColor />
|
||||
<PageFun />
|
||||
<TableProps />
|
||||
<template #footer>
|
||||
<ConfigOperation />
|
||||
</template>
|
||||
|
@ -130,6 +130,9 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
|
||||
<SettingItem key="9" :label="$t('theme.header.multilingual.visible')">
|
||||
<NSwitch v-model:value="themeStore.header.multilingual.visible" />
|
||||
</SettingItem>
|
||||
<SettingItem key="10" :label="$t('theme.header.globalSearch.visible')">
|
||||
<NSwitch v-model:value="themeStore.header.globalSearch.visible" />
|
||||
</SettingItem>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
|
44
src/layouts/modules/theme-drawer/modules/table-props.vue
Normal file
44
src/layouts/modules/theme-drawer/modules/table-props.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { themeTableSizeOptions } from '@/constants/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { translateOptions } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
import SettingItem from '../components/setting-item.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'TableProps'
|
||||
});
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDivider>{{ $t('theme.tablePropsTitle') }}</NDivider>
|
||||
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
|
||||
<SettingItem key="0" :label="$t('theme.table.size.title')">
|
||||
<NSelect
|
||||
v-model:value="themeStore.table.size"
|
||||
:options="translateOptions(themeTableSizeOptions)"
|
||||
size="small"
|
||||
class="w-120px"
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem key="1" :label="$t('theme.table.bordered')">
|
||||
<NSwitch v-model:value="themeStore.table.bordered" />
|
||||
</SettingItem>
|
||||
<SettingItem key="2" :label="$t('theme.table.bottomBordered')">
|
||||
<NSwitch v-model:value="themeStore.table.bottomBordered" />
|
||||
</SettingItem>
|
||||
<SettingItem key="3" :label="$t('theme.table.singleColumn')">
|
||||
<NSwitch v-model:value="themeStore.table.singleColumn" :checked-value="false" :unchecked-value="true" />
|
||||
</SettingItem>
|
||||
<SettingItem key="4" :label="$t('theme.table.singleLine')">
|
||||
<NSwitch v-model:value="themeStore.table.singleLine" :checked-value="false" :unchecked-value="true" />
|
||||
</SettingItem>
|
||||
<SettingItem key="5" :label="$t('theme.table.striped')">
|
||||
<NSwitch v-model:value="themeStore.table.striped" />
|
||||
</SettingItem>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -135,6 +135,9 @@ const local: App.I18n.Schema = {
|
||||
},
|
||||
multilingual: {
|
||||
visible: 'Display multilingual button'
|
||||
},
|
||||
globalSearch: {
|
||||
visible: 'Display GlobalSearch button'
|
||||
}
|
||||
},
|
||||
tab: {
|
||||
@ -165,6 +168,20 @@ const local: App.I18n.Schema = {
|
||||
visible: 'Watermark Full Screen Visible',
|
||||
text: 'Watermark Text'
|
||||
},
|
||||
tablePropsTitle: 'Table Props',
|
||||
table: {
|
||||
size: {
|
||||
title: 'Table Size',
|
||||
small: 'Small',
|
||||
medium: 'Medium',
|
||||
large: 'Large'
|
||||
},
|
||||
bordered: 'Bordered',
|
||||
bottomBordered: 'Bottom Bordered',
|
||||
singleColumn: 'Single Column',
|
||||
singleLine: 'Single Line',
|
||||
striped: 'Striped'
|
||||
},
|
||||
themeDrawerTitle: 'Theme Configuration',
|
||||
pageFunTitle: 'Page Function',
|
||||
resetCacheStrategy: {
|
||||
@ -222,6 +239,13 @@ const local: App.I18n.Schema = {
|
||||
'workflow_process-instance': 'Process Instance',
|
||||
workflow_leave: 'Leave Apply'
|
||||
},
|
||||
dict: {
|
||||
sys_user_sex: {
|
||||
male: 'Male',
|
||||
female: 'Female',
|
||||
unknown: 'Unknown'
|
||||
}
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
common: {
|
||||
@ -579,6 +603,7 @@ const local: App.I18n.Schema = {
|
||||
buttonPermissionList: 'Button Permission List',
|
||||
emptyMenu: 'Empty Menu',
|
||||
menuDetail: 'Menu Detail',
|
||||
cascadeDeleteContent: 'Cascade delete menu will delete the selected menu and all its sub-menus, are you sure?',
|
||||
iconifyTip: 'iconify address:`https://icones.js.org`',
|
||||
isFrameTip: 'If you choose External Link, the routing address needs to start with `http(s)://`',
|
||||
isCacheTip:
|
||||
@ -603,6 +628,10 @@ const local: App.I18n.Schema = {
|
||||
required: 'Please select Menu Icon',
|
||||
invalid: 'Menu Icon cannot be empty'
|
||||
},
|
||||
menuIds: {
|
||||
required: 'Please select Menu',
|
||||
invalid: 'Menu cannot be empty'
|
||||
},
|
||||
menuName: {
|
||||
required: 'Please enter Menu Name',
|
||||
invalid: 'Menu Name cannot be empty'
|
||||
@ -660,7 +689,8 @@ const local: App.I18n.Schema = {
|
||||
button: 'Button',
|
||||
addMenu: 'Add Menu',
|
||||
addChildMenu: 'Add Child Menu',
|
||||
editMenu: 'Edit Menu'
|
||||
editMenu: 'Edit Menu',
|
||||
cascadeDelete: 'Cascade Delete Menu'
|
||||
},
|
||||
notice: {
|
||||
title: 'Notice List',
|
||||
|
@ -135,6 +135,9 @@ const local: App.I18n.Schema = {
|
||||
},
|
||||
multilingual: {
|
||||
visible: '显示多语言按钮'
|
||||
},
|
||||
globalSearch: {
|
||||
visible: '显示全局搜索按钮'
|
||||
}
|
||||
},
|
||||
tab: {
|
||||
@ -165,6 +168,20 @@ const local: App.I18n.Schema = {
|
||||
visible: '显示全屏水印',
|
||||
text: '水印文本'
|
||||
},
|
||||
tablePropsTitle: '表格配置',
|
||||
table: {
|
||||
size: {
|
||||
title: '表格大小',
|
||||
small: '小',
|
||||
medium: '中',
|
||||
large: '大'
|
||||
},
|
||||
bordered: '边框',
|
||||
bottomBordered: '底部边框',
|
||||
singleColumn: '设定行的分割线',
|
||||
singleLine: '设定列的分割线',
|
||||
striped: '斑马线条纹'
|
||||
},
|
||||
themeDrawerTitle: '主题配置',
|
||||
pageFunTitle: '页面功能',
|
||||
resetCacheStrategy: {
|
||||
@ -222,6 +239,13 @@ const local: App.I18n.Schema = {
|
||||
'workflow_process-instance': '流程实例',
|
||||
workflow_leave: '请假申请'
|
||||
},
|
||||
dict: {
|
||||
sys_user_sex: {
|
||||
male: '男',
|
||||
female: '女',
|
||||
unknown: '未知'
|
||||
}
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
common: {
|
||||
@ -579,6 +603,7 @@ const local: App.I18n.Schema = {
|
||||
buttonPermissionList: '按钮权限列表',
|
||||
emptyMenu: '暂无菜单',
|
||||
menuDetail: '菜单详情',
|
||||
cascadeDeleteContent: '级联删除菜单将删除所选中的菜单,是否继续?',
|
||||
iconifyTip: 'iconify 地址:https://icones.js.org',
|
||||
isFrameTip: '选择是外链则路由地址需要以`http(s)://`开头',
|
||||
isCacheTip: '选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致',
|
||||
@ -597,6 +622,10 @@ const local: App.I18n.Schema = {
|
||||
required: '请选择菜单类型',
|
||||
invalid: '菜单类型不能为空'
|
||||
},
|
||||
menuIds: {
|
||||
required: '请选择菜单',
|
||||
invalid: '菜单不能为空'
|
||||
},
|
||||
icon: {
|
||||
required: '请选择菜单图标',
|
||||
invalid: '菜单图标不能为空'
|
||||
@ -658,7 +687,8 @@ const local: App.I18n.Schema = {
|
||||
button: '按钮',
|
||||
addMenu: '新增菜单',
|
||||
addChildMenu: '新增子菜单',
|
||||
editMenu: '编辑菜单'
|
||||
editMenu: '编辑菜单',
|
||||
cascadeDelete: '级联删除菜单'
|
||||
},
|
||||
notice: {
|
||||
title: '通知公告列表',
|
||||
|
@ -25,8 +25,8 @@ export function setupAppVersionNotification() {
|
||||
|
||||
const buildTime = await getHtmlBuildTime();
|
||||
|
||||
// If build time hasn't changed, no update is needed
|
||||
if (buildTime === BUILD_TIME) {
|
||||
// If failed to get build time or build time hasn't changed, no update is needed.
|
||||
if (!buildTime || buildTime === BUILD_TIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -88,16 +88,22 @@ export function setupAppVersionNotification() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getHtmlBuildTime() {
|
||||
async function getHtmlBuildTime(): Promise<string | null> {
|
||||
const baseUrl = import.meta.env.VITE_BASE_URL || '/';
|
||||
|
||||
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
|
||||
|
||||
const html = await res.text();
|
||||
if (!res.ok) {
|
||||
console.error('getHtmlBuildTime error:', res.status, res.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = html.match(/<meta name="buildTime" content="(.*)">/);
|
||||
|
||||
const buildTime = match?.[1] || '';
|
||||
|
||||
return buildTime;
|
||||
const html = await res.text();
|
||||
const match = html.match(/<meta name="buildTime" content="(.*)">/);
|
||||
return match?.[1] || null;
|
||||
} catch (error) {
|
||||
console.error('getHtmlBuildTime error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
/** 后台返回的路由动态生成name 解决缓存问题 感谢 @fourteendp 详见 https://github.com/vbenjs/vue-vben-admin/issues/3927 */
|
||||
import type { Component } from 'vue';
|
||||
import { defineComponent, h } from 'vue';
|
||||
|
||||
interface Options {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export type RouteComponentLoader = () => Promise<any>;
|
||||
|
||||
/**
|
||||
* 作用相当于给组件包了一层 & 设置name 解决keepAlive问题
|
||||
*
|
||||
* @param loader 导入的路由
|
||||
* @param options options
|
||||
* @returns components
|
||||
*/
|
||||
export function createCustomNameComponent(
|
||||
loader: RouteComponentLoader,
|
||||
options: Options = {}
|
||||
): () => Promise<Component> {
|
||||
const { name } = options;
|
||||
let component: Component | null = null;
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const { default: loadedComponent } = await loader();
|
||||
component = loadedComponent;
|
||||
} catch (error) {
|
||||
console.error(`Cannot resolve component ${name}, error:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
return async () => {
|
||||
if (!component) {
|
||||
await load();
|
||||
}
|
||||
|
||||
return Promise.resolve(
|
||||
defineComponent({
|
||||
name,
|
||||
render() {
|
||||
return h(component as Component);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
@ -59,3 +59,11 @@ export function fetchGetTenantPackageMenuTreeSelect(packageId: CommonType.IdType
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
// 级联删除菜单
|
||||
export function fetchCascadeDeleteMenu(menuIds: CommonType.IdType[]) {
|
||||
return request<boolean>({
|
||||
url: `/system/menu/cascade/${menuIds.join(',')}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { AxiosRequestConfig, GenericAbortSignal } from 'axios';
|
||||
import { request } from '@/service/request';
|
||||
|
||||
/** 获取文件管理列表 */
|
||||
@ -18,36 +17,10 @@ export function fetchBatchDeleteOss(ossIds: CommonType.IdType[]) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Axios上传进度事件 */
|
||||
export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];
|
||||
|
||||
/** 默认上传结果 */
|
||||
export interface UploadResult {
|
||||
url: string;
|
||||
fileName: string;
|
||||
ossId: string;
|
||||
}
|
||||
|
||||
export interface UploadApiOptions {
|
||||
onUploadProgress?: AxiosProgressEvent;
|
||||
signal?: GenericAbortSignal;
|
||||
}
|
||||
|
||||
/** 上传文件接口 */
|
||||
export function uploadApi(file: File | Blob, options?: UploadApiOptions) {
|
||||
const { onUploadProgress, signal } = options ?? {};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
return request<UploadResult>({
|
||||
url: '/resource/oss/upload',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
onUploadProgress,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
signal
|
||||
// 查询OSS对象基于id串
|
||||
export function fetchGetOssListByIds(ossIds: CommonType.IdType[]) {
|
||||
return request<Api.System.Oss[]>({
|
||||
url: `/resource/oss/listByIds/${ossIds.join(',')}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
@ -36,6 +36,15 @@ export function fetchUpdateRoleStatus(data: Api.System.RoleOperateParams) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改角色数据权限 */
|
||||
export function fetchUpdateRoleDataScope(data: Api.System.RoleOperateParams) {
|
||||
return request<boolean>({
|
||||
url: '/system/role/dataScope',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量删除角色信息 */
|
||||
export function fetchBatchDeleteRole(roleIds: CommonType.IdType[]) {
|
||||
return request<boolean>({
|
||||
|
@ -12,6 +12,7 @@ import { clearAuthStorage, getToken } from './shared';
|
||||
|
||||
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const routeStore = useRouteStore();
|
||||
const tabStore = useTabStore();
|
||||
const { toLogin, redirectFromLogin } = useRouterPush(false);
|
||||
@ -37,8 +38,6 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
|
||||
|
||||
/** Reset auth store */
|
||||
async function resetStore() {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
recordUserId();
|
||||
|
||||
clearAuthStorage();
|
||||
|
@ -1,11 +1,17 @@
|
||||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export const useDictStore = defineStore('dict', () => {
|
||||
const dictData = ref<{ [key: string]: Api.System.DictData[] }>({});
|
||||
|
||||
const getDict = (key: string) => {
|
||||
return dictData.value[key];
|
||||
return dictData.value[key]?.map(item => ({
|
||||
...item,
|
||||
dictLabel: item.dictLabel?.startsWith(`dict.${item.dictType}.`)
|
||||
? $t(item.dictLabel as App.I18n.I18nKey)
|
||||
: item.dictLabel
|
||||
}));
|
||||
};
|
||||
|
||||
const setDict = (key: string, dict: Api.System.DictData[]) => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { reactive } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { SetupStoreId } from '@/enum';
|
||||
|
||||
interface NoticeItem {
|
||||
title?: string;
|
||||
@ -8,7 +9,7 @@ interface NoticeItem {
|
||||
time: string;
|
||||
}
|
||||
|
||||
export const useNoticeStore = defineStore('notice', () => {
|
||||
export const useNoticeStore = defineStore(SetupStoreId.Notice, () => {
|
||||
const state = reactive({
|
||||
notices: [] as NoticeItem[]
|
||||
});
|
||||
@ -21,6 +22,10 @@ export const useNoticeStore = defineStore('notice', () => {
|
||||
state.notices.splice(state.notices.indexOf(notice), 1);
|
||||
};
|
||||
|
||||
const readNotice = (notice: NoticeItem) => {
|
||||
state.notices[state.notices.indexOf(notice)].read = true;
|
||||
};
|
||||
|
||||
// 实现全部已读
|
||||
const readAll = () => {
|
||||
state.notices.forEach((item: any) => {
|
||||
@ -31,10 +36,12 @@ export const useNoticeStore = defineStore('notice', () => {
|
||||
const clearNotice = () => {
|
||||
state.notices = [];
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
addNotice,
|
||||
removeNotice,
|
||||
readNotice,
|
||||
readAll,
|
||||
clearNotice
|
||||
};
|
||||
|
@ -103,6 +103,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
// eslint-disable-next-line complexity
|
||||
function parseRouter(route: ElegantConstRoute, parent?: ElegantConstRoute) {
|
||||
route.meta = route.meta ? route.meta : { title: route.name };
|
||||
if (route.meta.title.startsWith('route.')) {
|
||||
route.meta.i18nKey = route.meta.title as App.I18n.I18nKey;
|
||||
}
|
||||
const isLayout = route.component === 'Layout';
|
||||
const isFramePage = route.component === 'FrameView';
|
||||
const isParentLayout = route.component === 'ParentView';
|
||||
|
@ -100,7 +100,9 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
|
||||
const removedTabRouteKey = tabs.value[removeTabIndex].routeKey;
|
||||
const isRemoveActiveTab = activeTabId.value === tabId;
|
||||
const nextTab = tabs.value[removeTabIndex + 1] || homeTab.value;
|
||||
|
||||
// if remove the last tab, then switch to the second last tab
|
||||
const nextTab = tabs.value[removeTabIndex + 1] || tabs.value[removeTabIndex - 1] || homeTab.value;
|
||||
|
||||
// remove tab
|
||||
tabs.value.splice(removeTabIndex, 1);
|
||||
|
@ -30,6 +30,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
},
|
||||
multilingual: {
|
||||
visible: true
|
||||
},
|
||||
globalSearch: {
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
tab: {
|
||||
@ -57,6 +60,14 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
||||
visible: import.meta.env.VITE_WATERMARK === 'Y',
|
||||
text: 'RuoYi-Vue-Plus'
|
||||
},
|
||||
table: {
|
||||
bordered: true,
|
||||
bottomBordered: true,
|
||||
singleColumn: false,
|
||||
singleLine: true,
|
||||
size: 'small',
|
||||
striped: false
|
||||
},
|
||||
tokens: {
|
||||
light: {
|
||||
colors: {
|
||||
|
15
src/typings/api/monitor.api.d.ts
vendored
15
src/typings/api/monitor.api.d.ts
vendored
@ -10,6 +10,9 @@ declare namespace Api {
|
||||
* backend api module: "monitor"
|
||||
*/
|
||||
namespace Monitor {
|
||||
/** 业务操作类型 */
|
||||
type BusinessType = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
||||
|
||||
/** oper log */
|
||||
type OperLog = Common.CommonRecord<{
|
||||
/** 日志主键 */
|
||||
@ -19,13 +22,13 @@ declare namespace Api {
|
||||
/** 系统模块 */
|
||||
title: string;
|
||||
/** 操作类型 */
|
||||
businessType: number;
|
||||
businessType: Monitor.BusinessType;
|
||||
/** 方法名称 */
|
||||
method: string;
|
||||
/** 请求方式 */
|
||||
requestMethod: string;
|
||||
/** 操作类别 */
|
||||
operatorType: number;
|
||||
operatorType: string;
|
||||
/** 操作人员 */
|
||||
operName: string;
|
||||
/** 部门名称 */
|
||||
@ -41,7 +44,7 @@ declare namespace Api {
|
||||
/** 返回参数 */
|
||||
jsonResult: string;
|
||||
/** 操作状态 */
|
||||
status: number;
|
||||
status: Common.EnableStatus;
|
||||
/** 错误消息 */
|
||||
errorMsg: string;
|
||||
/** 操作时间 */
|
||||
@ -70,7 +73,7 @@ declare namespace Api {
|
||||
/** 客户端 */
|
||||
clientKey: string;
|
||||
/** 设备类型 */
|
||||
deviceType: string;
|
||||
deviceType: System.DeviceType;
|
||||
/** 登录IP地址 */
|
||||
ipaddr: string;
|
||||
/** 登录地点 */
|
||||
@ -80,7 +83,7 @@ declare namespace Api {
|
||||
/** 操作系统 */
|
||||
os: string;
|
||||
/** 登录状态(0成功 1失败) */
|
||||
status: string;
|
||||
status: Common.EnableStatus;
|
||||
/** 提示消息 */
|
||||
msg: string;
|
||||
/** 访问时间 */
|
||||
@ -149,7 +152,7 @@ declare namespace Api {
|
||||
/** 所在部门 */
|
||||
deptName: string;
|
||||
/** 设备类型 */
|
||||
deviceType: string;
|
||||
deviceType: System.DeviceType;
|
||||
/** 登录时间 */
|
||||
loginTime: number;
|
||||
/** 令牌ID */
|
||||
|
43
src/typings/api/system.api.d.ts
vendored
43
src/typings/api/system.api.d.ts
vendored
@ -34,7 +34,7 @@ declare namespace Api {
|
||||
/** 显示顺序 */
|
||||
roleSort: number;
|
||||
/** 角色状态(0正常 1停用) */
|
||||
status: string;
|
||||
status: Common.EnableStatus;
|
||||
/** 是否管理员 */
|
||||
superAdmin: boolean;
|
||||
}>;
|
||||
@ -115,7 +115,7 @@ declare namespace Api {
|
||||
/** 密码 */
|
||||
password: string;
|
||||
/** 帐号状态(0正常 1停用) */
|
||||
status: string;
|
||||
status: Common.EnableStatus;
|
||||
/** 最后登录IP */
|
||||
loginIp: string;
|
||||
/** 最后登录时间 */
|
||||
@ -356,7 +356,7 @@ declare namespace Api {
|
||||
/** 字典键值 */
|
||||
dictValue: string;
|
||||
/** 是否默认(Y是 N否) */
|
||||
isDefault: string;
|
||||
isDefault: Common.YesOrNoStatus;
|
||||
/** 表格回显样式 */
|
||||
listClass: NaiveUI.ThemeColor;
|
||||
/** 备注 */
|
||||
@ -402,7 +402,7 @@ declare namespace Api {
|
||||
/** 邮箱 */
|
||||
email: string;
|
||||
/** 部门状态(0正常 1停用) */
|
||||
status: string;
|
||||
status: Common.EnableStatus;
|
||||
/** 子部门 */
|
||||
children: Dept[];
|
||||
}>;
|
||||
@ -440,7 +440,7 @@ declare namespace Api {
|
||||
/** 显示顺序 */
|
||||
postSort: number;
|
||||
/** 状态(0正常 1停用) */
|
||||
status: string;
|
||||
status: Common.EnableStatus;
|
||||
/** 备注 */
|
||||
remark: string;
|
||||
}>;
|
||||
@ -476,7 +476,7 @@ declare namespace Api {
|
||||
/** 参数键值 */
|
||||
configValue: string;
|
||||
/** 是否内置 */
|
||||
configType: string;
|
||||
configType: Common.YesOrNoStatus;
|
||||
/** 备注 */
|
||||
remark: string;
|
||||
}>;
|
||||
@ -523,7 +523,7 @@ declare namespace Api {
|
||||
/** 用户数量(-1不限制) */
|
||||
accountCount: number;
|
||||
/** 租户状态(0正常 1停用) */
|
||||
status: string;
|
||||
status: Common.EnableStatus;
|
||||
/** 删除标志(0代表存在 1代表删除) */
|
||||
delFlag: string;
|
||||
}>;
|
||||
@ -577,7 +577,7 @@ declare namespace Api {
|
||||
/** 菜单树选择项是否关联显示 */
|
||||
menuCheckStrictly: boolean;
|
||||
/** 状态(0正常 1停用) */
|
||||
status: string;
|
||||
status: Common.EnableStatus;
|
||||
/** 删除标志(0代表存在 1代表删除) */
|
||||
delFlag: string;
|
||||
}>;
|
||||
@ -602,6 +602,9 @@ declare namespace Api {
|
||||
/** tenant package select list */
|
||||
type TenantPackageSelectList = Common.CommonRecord<Pick<TenantPackage, 'packageId' | 'packageName'>>;
|
||||
|
||||
/** 通知公告类型 */
|
||||
type NoticeType = '1' | '2';
|
||||
|
||||
/** notice */
|
||||
type Notice = Common.CommonRecord<{
|
||||
/** 公告ID */
|
||||
@ -611,11 +614,11 @@ declare namespace Api {
|
||||
/** 公告标题 */
|
||||
noticeTitle: string;
|
||||
/** 公告类型 */
|
||||
noticeType: string;
|
||||
noticeType: System.NoticeType;
|
||||
/** 公告内容 */
|
||||
noticeContent: string;
|
||||
/** 公告状态 */
|
||||
status: string;
|
||||
status: Common.EnableStatus;
|
||||
/** 创建者 */
|
||||
createByName: string;
|
||||
/** 备注 */
|
||||
@ -635,6 +638,12 @@ declare namespace Api {
|
||||
/** notice list */
|
||||
type NoticeList = Api.Common.PaginatingQueryRecord<Notice>;
|
||||
|
||||
/** 授权类型 */
|
||||
type GrantType = 'password' | 'sms' | 'password' | 'email' | 'xcx' | 'social';
|
||||
|
||||
/** 设备类型 */
|
||||
type DeviceType = 'pc' | 'android' | 'ios' | 'xcx';
|
||||
|
||||
/** client */
|
||||
type Client = Common.CommonRecord<{
|
||||
/** id */
|
||||
@ -646,17 +655,17 @@ declare namespace Api {
|
||||
/** 客户端秘钥 */
|
||||
clientSecret: string;
|
||||
/** 授权类型 */
|
||||
grantType: string;
|
||||
grantType: System.GrantType;
|
||||
/** 授权类型列表 */
|
||||
grantTypeList: string[];
|
||||
grantTypeList: System.GrantType[];
|
||||
/** 设备类型 */
|
||||
deviceType: string;
|
||||
deviceType: System.DeviceType;
|
||||
/** token活跃超时时间 */
|
||||
activeTimeout: number;
|
||||
/** token固定超时 */
|
||||
timeout: number;
|
||||
/** 状态 */
|
||||
status: string;
|
||||
status: Common.EnableStatus;
|
||||
/** 删除标志(0代表存在 1代表删除) */
|
||||
delFlag: string;
|
||||
}>;
|
||||
@ -758,13 +767,13 @@ declare namespace Api {
|
||||
/** 自定义域名 */
|
||||
domain: string;
|
||||
/** 是否https(Y=是,N=否) */
|
||||
isHttps: Api.Common.YesOrNoStatus;
|
||||
isHttps: Common.YesOrNoStatus;
|
||||
/** 域 */
|
||||
region: string;
|
||||
/** 桶权限类型 */
|
||||
accessPolicy: Api.System.OssAccessPolicy;
|
||||
accessPolicy: System.OssAccessPolicy;
|
||||
/** 是否默认(0=是,1=否) */
|
||||
status: Api.Common.EnableStatus;
|
||||
status: Common.EnableStatus;
|
||||
/** 扩展字段 */
|
||||
ext1: string;
|
||||
/** 备注 */
|
||||
|
34
src/typings/app.d.ts
vendored
34
src/typings/app.d.ts
vendored
@ -58,6 +58,10 @@ declare namespace App {
|
||||
/** Whether to show the multilingual */
|
||||
visible: boolean;
|
||||
};
|
||||
globalSearch: {
|
||||
/** Whether to show the GlobalSearch */
|
||||
visible: boolean;
|
||||
};
|
||||
};
|
||||
/** Tab */
|
||||
tab: {
|
||||
@ -109,6 +113,20 @@ declare namespace App {
|
||||
/** Watermark text */
|
||||
text: string;
|
||||
};
|
||||
table: {
|
||||
/** Whether to show the table border */
|
||||
bordered: boolean;
|
||||
/** Whether to show the table bottom border */
|
||||
bottomBordered: boolean;
|
||||
/** Whether to show the table single column */
|
||||
singleColumn: boolean;
|
||||
/** Whether to show the table single line */
|
||||
singleLine: boolean;
|
||||
/** Whether to show the table size */
|
||||
size: UnionKey.ThemeTableSize;
|
||||
/** Whether to show the table striped */
|
||||
striped: boolean;
|
||||
};
|
||||
/** define some theme settings tokens, will transform to css variables */
|
||||
tokens: {
|
||||
light: ThemeSettingToken;
|
||||
@ -401,6 +419,9 @@ declare namespace App {
|
||||
multilingual: {
|
||||
visible: string;
|
||||
};
|
||||
globalSearch: {
|
||||
visible: string;
|
||||
};
|
||||
};
|
||||
tab: {
|
||||
visible: string;
|
||||
@ -426,6 +447,15 @@ declare namespace App {
|
||||
visible: string;
|
||||
text: string;
|
||||
};
|
||||
tablePropsTitle: string;
|
||||
table: {
|
||||
size: { title: string } & Record<UnionKey.ThemeTableSize, string>;
|
||||
bordered: string;
|
||||
bottomBordered: string;
|
||||
singleColumn: string;
|
||||
singleLine: string;
|
||||
striped: string;
|
||||
};
|
||||
themeDrawerTitle: string;
|
||||
pageFunTitle: string;
|
||||
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
|
||||
@ -437,6 +467,7 @@ declare namespace App {
|
||||
};
|
||||
};
|
||||
route: Record<I18nRouteKey, string>;
|
||||
dict: Record<string, Record<string, string>>;
|
||||
page: {
|
||||
common: {
|
||||
id: string;
|
||||
@ -680,6 +711,7 @@ declare namespace App {
|
||||
buttonPermissionList: string;
|
||||
emptyMenu: string;
|
||||
menuDetail: string;
|
||||
cascadeDeleteContent: string;
|
||||
iconifyTip: string;
|
||||
isFrameTip: string;
|
||||
isCacheTip: string;
|
||||
@ -691,6 +723,7 @@ declare namespace App {
|
||||
form: {
|
||||
parentId: FormMsg;
|
||||
menuType: FormMsg;
|
||||
menuIds: FormMsg;
|
||||
icon: FormMsg;
|
||||
menuName: FormMsg;
|
||||
orderNum: FormMsg;
|
||||
@ -717,6 +750,7 @@ declare namespace App {
|
||||
addMenu: string;
|
||||
addChildMenu: string;
|
||||
editMenu: string;
|
||||
cascadeDelete: string;
|
||||
};
|
||||
notice: {
|
||||
title: string;
|
||||
|
4
src/typings/components.d.ts
vendored
4
src/typings/components.d.ts
vendored
@ -15,6 +15,7 @@ declare module 'vue' {
|
||||
copy: typeof import('./../components/custom/dept-tree-select copy.vue')['default']
|
||||
CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
||||
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
|
||||
DataTable: typeof import('./../components/common/data-table.vue')['default']
|
||||
DeptTree: typeof import('./../components/custom/dept-tree.vue')['default']
|
||||
DeptTreeSelect: typeof import('./../components/custom/dept-tree-select.vue')['default']
|
||||
DictRadio: typeof import('./../components/custom/dict-radio.vue')['default']
|
||||
@ -64,6 +65,7 @@ declare module 'vue' {
|
||||
NA: typeof import('naive-ui')['NA']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
|
||||
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
@ -84,6 +86,7 @@ declare module 'vue' {
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
@ -130,6 +133,7 @@ declare module 'vue' {
|
||||
NUpload: typeof import('naive-ui')['NUpload']
|
||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||
NWatermark: typeof import('naive-ui')['NWatermark']
|
||||
OssUpload: typeof import('./../components/custom/oss-upload.vue')['default']
|
||||
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
|
||||
PostSelect: typeof import('./../components/custom/post-select.vue')['default']
|
||||
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
|
||||
|
9
src/typings/union-key.d.ts
vendored
9
src/typings/union-key.d.ts
vendored
@ -51,6 +51,15 @@ declare namespace UnionKey {
|
||||
*/
|
||||
type ThemeTabMode = import('@sa/materials').PageTabMode;
|
||||
|
||||
/**
|
||||
* The table size
|
||||
*
|
||||
* - small: small size
|
||||
* - medium: medium size
|
||||
* - large: large size
|
||||
*/
|
||||
type ThemeTableSize = 'small' | 'medium' | 'large';
|
||||
|
||||
/** Unocss animate key */
|
||||
type UnoCssAnimateKey =
|
||||
| 'pulse'
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { AcceptType } from '@/enum/business';
|
||||
import { $t } from '@/locales';
|
||||
/**
|
||||
* Transform record to option
|
||||
@ -87,8 +88,7 @@ export function isNull(value: any) {
|
||||
|
||||
/** 判断是否为图片类型 */
|
||||
export function isImage(suffix: string) {
|
||||
const imgSuffixList = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
return imgSuffixList.includes(suffix.toLowerCase());
|
||||
return AcceptType.Image.split(',').includes(suffix.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@ import { useLoading } from '@sa/hooks';
|
||||
import { fetchForceLogout, fetchGetOnlineDeviceList } from '@/service/api/monitor';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useTable } from '@/hooks/common/table';
|
||||
import { getBrowserIcon, getOsIcon } from '@/utils/format';
|
||||
import { getBrowserIcon, getOsIcon } from '@/utils/icon-tag-format';
|
||||
import { $t } from '@/locales';
|
||||
import ButtonIcon from '@/components/custom/button-icon.vue';
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||
@ -16,12 +16,8 @@ defineOptions({
|
||||
const appStore = useAppStore();
|
||||
const { loading: btnLoading, startLoading: startBtnLoading, endLoading: endBtnLoading } = useLoading(false);
|
||||
|
||||
const { columns, data, loading, mobilePagination, getData } = useTable({
|
||||
const { columns, data, loading, getData } = useTable({
|
||||
apiFn: fetchGetOnlineDeviceList,
|
||||
apiParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 15
|
||||
},
|
||||
columns: () => [
|
||||
{ title: '用户名', key: 'userName', align: 'center', minWidth: 120 },
|
||||
{ title: 'IP地址', key: 'ipaddr', align: 'center', minWidth: 120 },
|
||||
@ -109,7 +105,6 @@ async function forceLogout(tokenId: string) {
|
||||
:loading="loading"
|
||||
remote
|
||||
:row-key="row => row.noticeId"
|
||||
:pagination="mobilePagination"
|
||||
class="h-full"
|
||||
/>
|
||||
</template>
|
||||
|
@ -184,11 +184,10 @@ async function handleExport() {
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable
|
||||
<DataTable
|
||||
v-model:checked-row-keys="checkedRowKeys"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
size="small"
|
||||
:flex-height="!appStore.isMobile"
|
||||
:scroll-x="962"
|
||||
:loading="loading"
|
||||
|
@ -53,12 +53,13 @@ function createDefaultModel(): Model {
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Extract<keyof Model, 'id' | 'deptId' | 'userId' | 'testKey' | 'value'>;
|
||||
type RuleKey = Extract<keyof Model, 'id' | 'deptId' | 'userId' | 'orderNum' | 'testKey' | 'value'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
id: createRequiredRule('主键不能为空'),
|
||||
deptId: createRequiredRule('部门不能为空'),
|
||||
userId: createRequiredRule('用户不能为空'),
|
||||
orderNum: createRequiredRule('排序号不能为空'),
|
||||
testKey: createRequiredRule('key 键不能为空'),
|
||||
value: createRequiredRule('值不能为空')
|
||||
};
|
||||
@ -124,7 +125,7 @@ watch(visible, () => {
|
||||
<NInput v-model:value="model.testKey" placeholder="请输入 key 键" />
|
||||
</NFormItem>
|
||||
<NFormItem label="值" path="value">
|
||||
<NInput v-model:value="model.value" placeholder="请输入值" />
|
||||
<OssUpload v-model:value="model.value as string" upload-type="image" placeholder="请输入值" />
|
||||
</NFormItem>
|
||||
<NFormItem label="备注" path="remark">
|
||||
<NInput v-model:value="model.remark" type="textarea" placeholder="请输入备注" />
|
||||
|
@ -209,12 +209,11 @@ function handleExport() {
|
||||
</template>
|
||||
</TableHeaderOperation>
|
||||
</template>
|
||||
<NDataTable
|
||||
<DataTable
|
||||
v-model:checked-row-keys="checkedRowKeys"
|
||||
v-model:expanded-row-keys="expandedRowKeys"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
size="small"
|
||||
:flex-height="!appStore.isMobile"
|
||||
:scroll-x="962"
|
||||
:loading="loading"
|
||||
|
@ -11,7 +11,7 @@ import { useAuth } from '@/hooks/business/auth';
|
||||
import { useDownload } from '@/hooks/business/download';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { getBrowserIcon, getOsIcon } from '@/utils/format';
|
||||
import { getBrowserIcon, getOsIcon } from '@/utils/icon-tag-format';
|
||||
import DictTag from '@/components/custom/dict-tag.vue';
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||
import { $t } from '@/locales';
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { getBrowserIcon, getOsIcon } from '@/utils/format';
|
||||
import { getBrowserIcon, getOsIcon } from '@/utils/icon-tag-format';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
|
@ -5,7 +5,7 @@ import { useAppStore } from '@/store/modules/app';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
import { useTable } from '@/hooks/common/table';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { getBrowserIcon, getOsIcon } from '@/utils/format';
|
||||
import { getBrowserIcon, getOsIcon } from '@/utils/icon-tag-format';
|
||||
import ButtonIcon from '@/components/custom/button-icon.vue';
|
||||
import DictTag from '@/components/custom/dict-tag.vue';
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="tsx">
|
||||
import { NDescriptions, NDescriptionsItem, NTag } from 'naive-ui';
|
||||
import { getRequestMethodTagType } from '@/utils/format';
|
||||
import { getRequestMethodTagType } from '@/utils/icon-tag-format';
|
||||
import { $t } from '@/locales';
|
||||
import DictTag from '@/components/custom/dict-tag.vue';
|
||||
|
||||
|
@ -47,7 +47,7 @@ function createDefaultModel(): Model {
|
||||
configName: '',
|
||||
configKey: '',
|
||||
configValue: '',
|
||||
configType: '',
|
||||
configType: 'Y',
|
||||
remark: ''
|
||||
};
|
||||
}
|
||||
|
@ -44,13 +44,15 @@ type Model = Api.System.DictDataOperateParams;
|
||||
|
||||
const model: Model = reactive(createDefaultModel());
|
||||
|
||||
const listClassOptions = [
|
||||
{ label: 'primary', value: 'primary' },
|
||||
{ label: 'success', value: 'success' },
|
||||
{ label: 'info', value: 'info' },
|
||||
{ label: 'warning', value: 'warning' },
|
||||
{ label: 'error', value: 'error' },
|
||||
{ label: 'default', value: 'default' }
|
||||
const listClassOptions: Record<string, string>[] = [
|
||||
{ label: 'Text', value: 'text' },
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Tertiary', value: 'tertiary' },
|
||||
{ label: 'Primary', value: 'primary' },
|
||||
{ label: 'Info', value: 'info' },
|
||||
{ label: 'Success', value: 'success' },
|
||||
{ label: 'Warning', value: 'warning' },
|
||||
{ label: 'Error', value: 'error' }
|
||||
];
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
@ -134,6 +136,9 @@ watch(visible, () => {
|
||||
});
|
||||
|
||||
function renderTagLabel(option: { label: string; value: string }) {
|
||||
if (option.value === 'text') {
|
||||
return option.label;
|
||||
}
|
||||
return (
|
||||
<NTag size="small" type={option.value as any}>
|
||||
{option.label}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="tsx">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { DataTableColumns, TreeInst, TreeOption } from 'naive-ui';
|
||||
import { NButton, NDivider, NIcon, NInput, NPopconfirm } from 'naive-ui';
|
||||
import { useBoolean, useLoading } from '@sa/hooks';
|
||||
@ -14,6 +14,7 @@ import SvgIcon from '@/components/custom/svg-icon.vue';
|
||||
import DictTag from '@/components/custom/dict-tag.vue';
|
||||
import ButtonIcon from '@/components/custom/button-icon.vue';
|
||||
import MenuOperateDrawer from './modules/menu-operate-drawer.vue';
|
||||
import MenuCascadeDeleteModal from './modules/menu-cascade-delete-modal.vue';
|
||||
|
||||
useDict('sys_show_hide');
|
||||
useDict('sys_normal_disable');
|
||||
@ -26,6 +27,7 @@ const editingData = ref<Api.System.Menu>();
|
||||
const operateType = ref<NaiveUI.TableOperateType>('add');
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { bool: drawerVisible, setTrue: openDrawer } = useBoolean();
|
||||
const { bool: cascadeDeleteVisible, setTrue: openCascadeDeleteDrawer } = useBoolean();
|
||||
const { loading: btnLoading, startLoading: startBtnLoading, endLoading: endBtnLoading } = useLoading();
|
||||
/** tree pattern name , use tree search */
|
||||
const name = ref<string>();
|
||||
@ -36,6 +38,18 @@ const treeData = ref<Api.System.Menu[]>([]);
|
||||
const checkedKeys = ref<CommonType.IdType[]>([0]);
|
||||
const expandedKeys = ref<CommonType.IdType[]>([0]);
|
||||
|
||||
// 是否为目录类型
|
||||
const isCatalog = computed(() => currentMenu.value?.menuType === 'M');
|
||||
|
||||
// 是否为菜单类型
|
||||
const isMenu = computed(() => currentMenu.value?.menuType === 'C');
|
||||
|
||||
// 外链类型
|
||||
const isExternalType = computed(() => currentMenu.value?.isFrame === '0');
|
||||
|
||||
// iframe类型
|
||||
const isIframeType = computed(() => currentMenu.value?.isFrame === '2');
|
||||
|
||||
const menuTreeRef = ref<TreeInst>();
|
||||
const btnData = ref<Api.System.MenuList>([]);
|
||||
|
||||
@ -292,10 +306,18 @@ const btnColumns: DataTableColumns<Api.System.Menu> = [
|
||||
v-if="hasAuth('system:menu:add')"
|
||||
size="small"
|
||||
icon="material-symbols:add-rounded"
|
||||
class="h-28px text-icon"
|
||||
class="h-28px text-icon color-primary"
|
||||
:tooltip-content="$t('page.system.menu.addMenu')"
|
||||
@click.stop="handleAddMenu(0)"
|
||||
/>
|
||||
<ButtonIcon
|
||||
v-if="hasAuth('system:menu:add')"
|
||||
size="small"
|
||||
icon="material-symbols:delete-outline"
|
||||
class="h-28px text-icon color-error"
|
||||
:tooltip-content="$t('page.system.menu.cascadeDelete')"
|
||||
@click.stop="openCascadeDeleteDrawer"
|
||||
/>
|
||||
<ButtonIcon
|
||||
size="small"
|
||||
icon="material-symbols:refresh-rounded"
|
||||
@ -347,7 +369,7 @@ const btnColumns: DataTableColumns<Api.System.Menu> = [
|
||||
<template #header-extra>
|
||||
<NSpace>
|
||||
<NButton
|
||||
v-if="currentMenu.menuType === 'M' && hasAuth('system:menu:add')"
|
||||
v-if="isCatalog && hasAuth('system:menu:add')"
|
||||
size="small"
|
||||
ghost
|
||||
type="primary"
|
||||
@ -391,30 +413,30 @@ const btnColumns: DataTableColumns<Api.System.Menu> = [
|
||||
label-class="w-20% min-w-88px"
|
||||
content-class="w-100px"
|
||||
>
|
||||
<NDescriptionsItem :label="$t('page.system.menu.menuName')">
|
||||
<NDescriptionsItem :label="$t('page.system.menu.menuType')">
|
||||
<NTag class="m-1" size="small" type="primary">{{ menuTypeRecord[currentMenu.menuType!] }}</NTag>
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem :label="$t('page.system.menu.status')">
|
||||
<DictTag size="small" :value="currentMenu.status" dict-code="sys_normal_disable" />
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem :label="$t('page.system.menu.addChildMenu')">
|
||||
<NDescriptionsItem :label="$t('page.system.menu.menuName')">
|
||||
{{ currentMenu.menuName }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="currentMenu.menuType === 'C'" :label="$t('page.system.menu.component')">
|
||||
<NDescriptionsItem v-if="isMenu" :label="$t('page.system.menu.component')">
|
||||
{{ currentMenu.component }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem
|
||||
:label="currentMenu.isFrame !== '0' ? $t('page.system.menu.path') : $t('page.system.menu.externalPath')"
|
||||
:label="!isExternalType ? $t('page.system.menu.path') : $t('page.system.menu.externalPath')"
|
||||
>
|
||||
{{ currentMenu.path }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem
|
||||
v-if="currentMenu.menuType === 'C'"
|
||||
:label="currentMenu.isFrame !== '2' ? $t('page.system.menu.query') : $t('page.system.menu.iframeQuery')"
|
||||
v-if="isMenu && !isExternalType"
|
||||
:label="!isIframeType ? $t('page.system.menu.query') : $t('page.system.menu.iframeQuery')"
|
||||
>
|
||||
{{ currentMenu.queryParam }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="currentMenu.menuType !== 'M'" :label="$t('page.system.menu.perms')">
|
||||
<NDescriptionsItem v-if="!isCatalog" :label="$t('page.system.menu.perms')">
|
||||
{{ currentMenu.perms }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem :label="$t('page.system.menu.isFrame')">
|
||||
@ -425,7 +447,7 @@ const btnColumns: DataTableColumns<Api.System.Menu> = [
|
||||
<NDescriptionsItem :label="$t('page.system.menu.visible')">
|
||||
<DictTag size="small" :value="currentMenu.visible" dict-code="sys_show_hide" />
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="currentMenu.menuType === 'C'" :label="$t('page.system.menu.isCache')">
|
||||
<NDescriptionsItem v-if="isMenu" :label="$t('page.system.menu.isCache')">
|
||||
<NTag v-if="currentMenu.isCache" class="m-1" size="small" :type="tagMap[currentMenu.isCache]">
|
||||
{{ currentMenu.isCache === '0' ? $t('page.system.menu.cache') : $t('page.system.menu.noCache') }}
|
||||
</NTag>
|
||||
@ -465,6 +487,7 @@ const btnColumns: DataTableColumns<Api.System.Menu> = [
|
||||
:menu-type="createType"
|
||||
@submitted="handleSubmitted"
|
||||
/>
|
||||
<MenuCascadeDeleteModal v-model:visible="cascadeDeleteVisible" @submitted="handleSubmitted" />
|
||||
</TableSiderLayout>
|
||||
</template>
|
||||
|
||||
|
118
src/views/system/menu/modules/menu-cascade-delete-modal.vue
Normal file
118
src/views/system/menu/modules/menu-cascade-delete-modal.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { fetchCascadeDeleteMenu } from '@/service/api/system';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import MenuTree from '@/components/custom/menu-tree.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'MenuCascadeDeleteModal'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const menuTreeRef = ref<InstanceType<typeof MenuTree> | null>(null);
|
||||
const menuOptions = ref<Api.System.MenuList>([]);
|
||||
const { loading: menuLoading } = useLoading();
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
type Model = {
|
||||
menuIds: CommonType.IdType[];
|
||||
};
|
||||
|
||||
const model: Model = reactive(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
menuIds: []
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Extract<keyof Model, 'menuIds'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
menuIds: createRequiredRule($t('page.system.menu.form.menuIds.invalid'))
|
||||
};
|
||||
|
||||
async function handleUpdateModelWhenEdit() {
|
||||
menuOptions.value = [];
|
||||
Object.assign(model, createDefaultModel());
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
window.$dialog?.warning({
|
||||
title: $t('page.system.menu.cascadeDelete'),
|
||||
content: $t('page.system.menu.cascadeDeleteContent'),
|
||||
positiveText: $t('common.delete'),
|
||||
positiveButtonProps: {
|
||||
type: 'error'
|
||||
},
|
||||
negativeText: $t('common.cancel'),
|
||||
onPositiveClick: async () => {
|
||||
const { error } = await fetchCascadeDeleteMenu(model.menuIds);
|
||||
if (error) return;
|
||||
window.$message?.success($t('common.deleteSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleUpdateModelWhenEdit();
|
||||
restoreValidation();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal
|
||||
v-model:show="visible"
|
||||
:title="$t('page.system.menu.cascadeDelete')"
|
||||
preset="card"
|
||||
:bordered="false"
|
||||
display-directive="show"
|
||||
class="max-w-90% w-500px"
|
||||
@close="closeDrawer"
|
||||
>
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :show-label="false" path="menuIds">
|
||||
<MenuTree
|
||||
v-if="visible"
|
||||
ref="menuTreeRef"
|
||||
v-model:options="menuOptions"
|
||||
v-model:loading="menuLoading"
|
||||
v-model:checked-keys="model.menuIds"
|
||||
:cascade="true"
|
||||
:show-header="false"
|
||||
:immediate="true"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NSpace justify="end" :size="16">
|
||||
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -83,8 +83,28 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
component: createRequiredRule($t('page.system.menu.form.component.invalid'))
|
||||
};
|
||||
|
||||
const isBtn = computed(() => model.menuType === 'F');
|
||||
// 是否为目录类型
|
||||
const isCatalog = computed(() => model.menuType === 'M');
|
||||
|
||||
// 是否为菜单类型
|
||||
const isMenu = computed(() => model.menuType === 'C');
|
||||
|
||||
// 是否为按钮类型
|
||||
const isBtn = computed(() => model.menuType === 'F');
|
||||
|
||||
// 外链类型
|
||||
const isExternalType = computed(() => model.isFrame === '0');
|
||||
|
||||
// 内部类型
|
||||
const isInternalType = computed(() => model.isFrame === '1');
|
||||
|
||||
// iframe类型
|
||||
const isIframeType = computed(() => model.isFrame === '2');
|
||||
|
||||
// 本地图标类型
|
||||
const isLocalIcon = computed(() => iconType.value === '2');
|
||||
|
||||
// 本地图标
|
||||
const localIcons = getLocalMenuIcons();
|
||||
const localIconOptions = localIcons.map<SelectOption>(item => ({
|
||||
label: () => (
|
||||
@ -102,7 +122,7 @@ function handleInitModel() {
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
Object.assign(model, props.rowData);
|
||||
if (isMenu.value && model.isFrame === '1') {
|
||||
if (isMenu.value && isInternalType.value) {
|
||||
model.component = model.component?.slice(0, -6);
|
||||
}
|
||||
iconType.value = model.icon?.startsWith('local-icon-') ? '2' : '1';
|
||||
@ -118,6 +138,44 @@ function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
// 处理路径
|
||||
function processPath(path: string | null | undefined): string {
|
||||
return path?.startsWith('/') ? path.substring(1) : path || '';
|
||||
}
|
||||
|
||||
// 处理组件
|
||||
function processComponent(component: string | null | undefined): string {
|
||||
if (isCatalog.value && isInternalType.value) {
|
||||
return 'Layout';
|
||||
}
|
||||
if (isIframeType.value || isExternalType.value) {
|
||||
return 'FrameView';
|
||||
}
|
||||
if (isMenu.value && isInternalType.value) {
|
||||
return component?.endsWith('/index') ? component : `${component || ''}/index`;
|
||||
}
|
||||
return component || '';
|
||||
}
|
||||
|
||||
function processQueryParam(queryParam: string | null | undefined): string {
|
||||
// 外链类型不需要查询参数
|
||||
if (isExternalType.value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 内部链接类型,处理动态参数
|
||||
if (isInternalType.value && queryList.value.length) {
|
||||
return JSON.stringify(Object.fromEntries(queryList.value.map(({ key, value }) => [key, value])));
|
||||
}
|
||||
|
||||
// iframe类型,直接使用原始参数
|
||||
if (isIframeType.value) {
|
||||
return queryParam || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
@ -133,77 +191,36 @@ async function handleSubmit() {
|
||||
visible: menuVisible,
|
||||
status,
|
||||
perms,
|
||||
remark
|
||||
remark,
|
||||
component,
|
||||
queryParam
|
||||
} = model;
|
||||
|
||||
let queryParam = model.queryParam;
|
||||
if (isFrame === '0') {
|
||||
queryParam = '';
|
||||
} else if (isFrame === '1' && queryList.value.length) {
|
||||
const queryObj: { [key: string]: string } = {};
|
||||
queryList.value.forEach(item => (queryObj[item.key] = item.value));
|
||||
queryParam = JSON.stringify(queryObj);
|
||||
}
|
||||
|
||||
const path = model.path?.startsWith('/') ? model.path?.substring(1) : model.path;
|
||||
|
||||
let component = model.component;
|
||||
if (isFrame === '1' && menuType === 'M') {
|
||||
component = 'Layout';
|
||||
} else if (isFrame === '2') {
|
||||
component = 'FrameView';
|
||||
} else if (isMenu.value && model.isFrame === '1') {
|
||||
component = component?.endsWith('/index') ? component : `${component}/index`;
|
||||
}
|
||||
|
||||
// request
|
||||
if (props.operateType === 'add') {
|
||||
const { error } = await fetchCreateMenu({
|
||||
menuName,
|
||||
path,
|
||||
parentId,
|
||||
orderNum,
|
||||
queryParam,
|
||||
isFrame,
|
||||
isCache,
|
||||
menuType,
|
||||
visible: menuVisible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
component,
|
||||
remark
|
||||
});
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
window.$message?.success($t('common.addSuccess'));
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit') {
|
||||
const { error } = await fetchUpdateMenu({
|
||||
menuId,
|
||||
menuName,
|
||||
path,
|
||||
parentId,
|
||||
orderNum,
|
||||
queryParam,
|
||||
isFrame,
|
||||
isCache,
|
||||
menuType,
|
||||
visible: menuVisible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
component,
|
||||
remark
|
||||
});
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
window.$message?.success($t('common.updateSuccess'));
|
||||
const payload = {
|
||||
menuName,
|
||||
path: processPath(model.path),
|
||||
parentId,
|
||||
orderNum,
|
||||
queryParam: processQueryParam(queryParam),
|
||||
isFrame,
|
||||
isCache,
|
||||
menuType,
|
||||
visible: menuVisible,
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
component: processComponent(component),
|
||||
remark
|
||||
};
|
||||
|
||||
const { error } =
|
||||
props.operateType === 'add' ? await fetchCreateMenu(payload) : await fetchUpdateMenu({ ...payload, menuId });
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted', menuType!);
|
||||
}
|
||||
@ -224,7 +241,7 @@ function onCreate() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="800" class="max-w-90%">
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="600" class="max-w-90%">
|
||||
<NDrawerContent :title="drawerTitle" :native-scrollbar="false" closable>
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NGrid responsive="screen" item-responsive>
|
||||
@ -238,12 +255,7 @@ function onCreate() {
|
||||
:placeholder="$t('page.system.menu.form.parentId.required')"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi
|
||||
v-if="model.menuType !== 'F'"
|
||||
:span="24"
|
||||
:label="$t('page.system.menu.menuType')"
|
||||
path="menuType"
|
||||
>
|
||||
<NFormItemGi v-if="!isBtn" :span="24" :label="$t('page.system.menu.menuType')" path="menuType">
|
||||
<NRadioGroup v-model:value="model.menuType">
|
||||
<NRadioButton
|
||||
v-for="item in menuTypeOptions.filter(item => item.value !== 'F')"
|
||||
@ -253,22 +265,30 @@ function onCreate() {
|
||||
/>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi :span="24" :label="$t('page.system.menu.menuName')" path="menuName">
|
||||
<NFormItemGi span="24" :label="$t('page.system.menu.menuName')" path="menuName">
|
||||
<NInput v-model:value="model.menuName" :placeholder="$t('page.system.menu.form.menuName.required')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" span="24" :label="$t('page.system.menu.iconType')">
|
||||
<NFormItemGi v-if="!isBtn" span="12" :label="$t('page.system.menu.iconType')">
|
||||
<NRadioGroup v-model:value="iconType">
|
||||
<NRadio v-for="item in menuIconTypeOptions" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" span="24" path="icon">
|
||||
<NFormItemGi v-if="!isBtn" span="12" path="icon">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTip :content="$t('page.system.menu.iconifyTip')" />
|
||||
<span class="pl-3px">{{ $t('page.system.menu.icon') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="iconType === '1'">
|
||||
<template v-if="isLocalIcon">
|
||||
<NSelect
|
||||
v-model:value="model.icon"
|
||||
:placeholder="$t('page.system.menu.placeholder.localIconPlaceholder')"
|
||||
filterable
|
||||
:options="localIconOptions"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NInput
|
||||
v-model:value="model.icon"
|
||||
:placeholder="$t('page.system.menu.placeholder.iconifyIconPlaceholder')"
|
||||
@ -279,27 +299,51 @@ function onCreate() {
|
||||
</template>
|
||||
</NInput>
|
||||
</template>
|
||||
<template v-if="iconType === '2'">
|
||||
<NSelect
|
||||
v-model:value="model.icon"
|
||||
:placeholder="$t('page.system.menu.placeholder.localIconPlaceholder')"
|
||||
filterable
|
||||
:options="localIconOptions"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" :span="12" path="isFrame">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTip :content="$t('page.system.menu.isFrameTip')" />
|
||||
<span>{{ $t('page.system.menu.isFrame') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<NRadioGroup v-model:value="model.isFrame">
|
||||
<NSpace>
|
||||
<NRadio
|
||||
v-for="option in menuIsFrameOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:label="option.label"
|
||||
/>
|
||||
</NSpace>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="isMenu" :span="12" path="isCache">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTip :content="$t('page.system.menu.isCacheTip')" />
|
||||
<span>{{ $t('page.system.menu.isCache') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<NRadioGroup v-model:value="model.isCache">
|
||||
<NSpace>
|
||||
<NRadio value="0" label="是" />
|
||||
<NRadio value="1" label="否" />
|
||||
</NSpace>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" :span="24" path="path">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTip :content="$t('page.system.menu.pathTip')" />
|
||||
<span>
|
||||
{{ model.isFrame !== '0' ? $t('page.system.menu.path') : $t('page.system.menu.externalPath') }}
|
||||
{{ !isExternalType ? $t('page.system.menu.path') : $t('page.system.menu.externalPath') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<NInput v-model:value="model.path" :placeholder="$t('page.system.menu.form.path.required')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="isMenu && model.isFrame === '1'" :span="24" path="component">
|
||||
<NFormItemGi v-if="isMenu && isInternalType" :span="24" path="component">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTip :content="$t('page.system.menu.componentTip')" />
|
||||
@ -313,13 +357,13 @@ function onCreate() {
|
||||
</NInputGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi
|
||||
v-if="isMenu && model.isFrame !== '0'"
|
||||
v-if="isMenu && !isExternalType"
|
||||
span="24"
|
||||
:show-feedback="!queryList.length"
|
||||
:label="model.isFrame !== '2' ? $t('page.system.menu.query') : $t('page.system.menu.iframeQuery')"
|
||||
:label="isInternalType ? $t('page.system.menu.query') : $t('page.system.menu.iframeQuery')"
|
||||
>
|
||||
<NDynamicInput
|
||||
v-if="model.isFrame !== '2'"
|
||||
v-if="isInternalType"
|
||||
v-model:value="queryList"
|
||||
item-style="margin-bottom: 0"
|
||||
:on-create="onCreate"
|
||||
@ -369,38 +413,7 @@ function onCreate() {
|
||||
</template>
|
||||
<NInput v-model:value="model.perms" :placeholder="$t('page.system.menu.form.perms.required')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="!isBtn" :span="12" path="isFrame">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTip :content="$t('page.system.menu.isFrameTip')" />
|
||||
<span>{{ $t('page.system.menu.isFrame') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<NRadioGroup v-model:value="model.isFrame">
|
||||
<NSpace>
|
||||
<NRadio
|
||||
v-for="option in menuIsFrameOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:label="option.label"
|
||||
/>
|
||||
</NSpace>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi v-if="isMenu" :span="12" path="isCache">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
<FormTip :content="$t('page.system.menu.isCacheTip')" />
|
||||
<span>{{ $t('page.system.menu.isCache') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<NRadioGroup v-model:value="model.isCache">
|
||||
<NSpace>
|
||||
<NRadio value="0" label="是" />
|
||||
<NRadio value="1" label="否" />
|
||||
</NSpace>
|
||||
</NRadioGroup>
|
||||
</NFormItemGi>
|
||||
|
||||
<NFormItemGi v-if="!isBtn" :span="12" :label="$t('page.system.menu.visible')" path="visible">
|
||||
<template #label>
|
||||
<div class="flex-center">
|
||||
|
@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
import FileUpload from '@/components/custom/file-upload.vue';
|
||||
import { AcceptType } from '@/enum/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'OssUploadModal'
|
||||
});
|
||||
const fileUploadRef = ref<InstanceType<typeof FileUpload> | null>(null);
|
||||
|
||||
interface Props {
|
||||
uploadType: 'file' | 'image';
|
||||
}
|
||||
@ -22,9 +24,9 @@ const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const accept = computed(() => {
|
||||
return props.uploadType === 'file' ? '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf' : '.jpg,.jpeg,.png,.gif,.bmp,.webp';
|
||||
});
|
||||
const accept = computed(() => (props.uploadType === 'file' ? AcceptType.File : AcceptType.Image));
|
||||
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
function handleUpdateModelWhenUpload() {}
|
||||
|
||||
@ -34,7 +36,7 @@ function closeDrawer() {
|
||||
|
||||
function handleClose() {
|
||||
closeDrawer();
|
||||
if (fileUploadRef.value?.refreshList) {
|
||||
if (fileList.value?.length > 0) {
|
||||
emit('close');
|
||||
}
|
||||
}
|
||||
@ -56,7 +58,7 @@ watch(visible, () => {
|
||||
:bordered="false"
|
||||
@after-leave="handleClose"
|
||||
>
|
||||
<FileUpload ref="fileUploadRef" :upload-type="uploadType" :accept="accept" />
|
||||
<FileUpload v-model:file-list="fileList" :upload-type="uploadType" :accept="accept" />
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
|
@ -82,7 +82,7 @@ const {
|
||||
key: 'dataScope',
|
||||
title: '数据范围',
|
||||
align: 'center',
|
||||
minWidth: 120,
|
||||
minWidth: 180,
|
||||
render: row => {
|
||||
return <NTag type="info">{dataScopeRecord[row.dataScope]}</NTag>;
|
||||
}
|
||||
@ -113,7 +113,7 @@ const {
|
||||
key: 'operate',
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
width: 220,
|
||||
width: 230,
|
||||
render: row => {
|
||||
if (row.roleId === 1) return null;
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { dataScopeOptions } from '@/constants/business';
|
||||
import { fetchGetRoleDeptTreeSelect, fetchUpdateRole } from '@/service/api/system/role';
|
||||
import { fetchGetRoleDeptTreeSelect, fetchUpdateRoleDataScope } from '@/service/api/system/role';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import DeptTree from '@/components/custom/dept-tree.vue';
|
||||
@ -86,7 +86,7 @@ async function handleSubmit() {
|
||||
|
||||
const { roleId, roleName, roleKey, roleSort, dataScope, deptIds, menuIds } = model;
|
||||
|
||||
const { error } = await fetchUpdateRole({
|
||||
const { error } = await fetchUpdateRoleDataScope({
|
||||
roleId,
|
||||
roleName,
|
||||
roleKey,
|
||||
|
Reference in New Issue
Block a user