mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
Merge branch 'master' into tauri
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}}
|
43
.github/workflows/deploy.yml
vendored
Normal file
43
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
|
||||
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)
|
@ -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">
|
||||
@ -346,13 +347,14 @@ console.log(t('common.confirm'));
|
||||
- **邮箱**: xlsea@linux.do
|
||||
- **作者主页**: https://gitee.com/xlsea
|
||||
|
||||
|
||||
- **作者**: Elio
|
||||
- **邮箱**: 1983933789@qq.com
|
||||
- **作者主页**: https://gitee.com/ahcode
|
||||
|
||||
## 💬 交流群
|
||||
|
||||
<img src="https://foruda.gitee.com/images/1747707357650535622/a9fcf43e_5601833.png" width="300px" />
|
||||
<img src="https://foruda.gitee.com/images/1748404753216665472/3d8b1a0b_5601833.png" width="300px" />
|
||||
|
||||
## 🧧 捐献作者
|
||||
|
||||
|
@ -26,9 +26,13 @@ UPDATE `sys_menu` SET `icon` = 'carbon:operations-record' WHERE `menu_id` = 500;
|
||||
UPDATE `sys_menu` SET `icon` = 'tabler:login-2' WHERE `menu_id` = 501;
|
||||
UPDATE `sys_menu` SET `icon` = 'gg:debug' WHERE `menu_id` = 1500;
|
||||
UPDATE `sys_menu` SET `icon` = 'gg:debug' WHERE `menu_id` = 1506;
|
||||
UPDATE `sys_menu` SET `path` = 'oss/config', `component` = 'system/oss-config/index', `icon` = 'hugeicons:configuration-01' WHERE `menu_id` = 133;
|
||||
|
||||
-- IFrame 类型
|
||||
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://ruoyi.xlsea.cn/admin/', `is_frame` = 2, `icon` = 'bx:bxl-spring-boot' WHERE `menu_id` = 117;
|
||||
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://preview.snailjob.opensnail.com/', `is_frame` = 2, `icon` = 'gridicons:scheduled' WHERE `menu_id` = 120;
|
||||
-- 外链类型
|
||||
UPDATE `sys_menu` SET `path` = 'https://gitee.com/xlsea/ruoyi-plus-soybean', `component` = 'FrameView', `icon` = 'local-icon-gitee' WHERE `menu_id` = 4;
|
||||
|
||||
-- plus-ui 需要禁用的页面
|
||||
UPDATE `sys_menu` SET `status` = '1' WHERE `menu_id` IN ( '116', '130', '131', '132', '11700', '11701' );
|
||||
|
6
docs/template/index-tree.vue.vm
vendored
6
docs/template/index-tree.vue.vm
vendored
@ -56,12 +56,6 @@ const {
|
||||
align: 'center',
|
||||
width: 48
|
||||
},
|
||||
{
|
||||
key: 'index',
|
||||
title: $t('common.index'),
|
||||
align: 'center',
|
||||
width: 64
|
||||
},
|
||||
#foreach ($column in $columns)
|
||||
#if($column.list)
|
||||
{
|
||||
|
@ -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",
|
||||
|
@ -2,40 +2,59 @@ import { computed, onScopeDispose, ref } from 'vue';
|
||||
import { useRafFn } from '@vueuse/core';
|
||||
|
||||
/**
|
||||
* count down
|
||||
* A hook for implementing a countdown timer. It uses `requestAnimationFrame` for smooth and accurate timing,
|
||||
* independent of the screen refresh rate.
|
||||
*
|
||||
* @param seconds - count down seconds
|
||||
* @param initialSeconds - The total number of seconds for the countdown.
|
||||
*/
|
||||
export default function useCountDown(seconds: number) {
|
||||
const FPS_PER_SECOND = 60;
|
||||
export default function useCountDown(initialSeconds: number) {
|
||||
const remainingSeconds = ref(0);
|
||||
|
||||
const fps = ref(0);
|
||||
const count = computed(() => Math.ceil(remainingSeconds.value));
|
||||
|
||||
const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
|
||||
|
||||
const isCounting = computed(() => fps.value > 0);
|
||||
const isCounting = computed(() => remainingSeconds.value > 0);
|
||||
|
||||
const { pause, resume } = useRafFn(
|
||||
() => {
|
||||
if (fps.value > 0) {
|
||||
fps.value -= 1;
|
||||
} else {
|
||||
({ delta }) => {
|
||||
// delta: milliseconds elapsed since the last frame.
|
||||
|
||||
// If countdown already reached zero or below, ensure it's 0 and stop.
|
||||
if (remainingSeconds.value <= 0) {
|
||||
remainingSeconds.value = 0;
|
||||
pause();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate seconds passed since the last frame.
|
||||
const secondsPassed = delta / 1000;
|
||||
remainingSeconds.value -= secondsPassed;
|
||||
|
||||
// If countdown has finished after decrementing.
|
||||
if (remainingSeconds.value <= 0) {
|
||||
remainingSeconds.value = 0;
|
||||
pause();
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
{ immediate: false } // The timer does not start automatically.
|
||||
);
|
||||
|
||||
function start(updateSeconds: number = seconds) {
|
||||
fps.value = FPS_PER_SECOND * updateSeconds;
|
||||
/**
|
||||
* Starts the countdown.
|
||||
*
|
||||
* @param [updatedSeconds=initialSeconds] - Optionally, start with a new duration. Default is `initialSeconds`
|
||||
*/
|
||||
function start(updatedSeconds: number = initialSeconds) {
|
||||
remainingSeconds.value = updatedSeconds;
|
||||
resume();
|
||||
}
|
||||
|
||||
/** Stops the countdown and resets the remaining time to 0. */
|
||||
function stop() {
|
||||
fps.value = 0;
|
||||
remainingSeconds.value = 0;
|
||||
pause();
|
||||
}
|
||||
|
||||
// Ensure the rAF loop is cleaned up when the component is unmounted.
|
||||
onScopeDispose(() => {
|
||||
pause();
|
||||
});
|
||||
|
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>
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useAttrs } from 'vue';
|
||||
import { type VNode, computed, useAttrs } from 'vue';
|
||||
import type { ButtonProps, PopoverPlacement } from 'naive-ui';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
@ -11,6 +11,8 @@ defineOptions({
|
||||
interface Props {
|
||||
/** Button class */
|
||||
class?: string;
|
||||
/** Show popconfirm icon */
|
||||
showPopconfirmIcon?: boolean;
|
||||
/** Iconify icon name */
|
||||
icon?: string;
|
||||
/** Local icon name */
|
||||
@ -19,8 +21,8 @@ interface Props {
|
||||
tooltipContent?: string;
|
||||
/** Tooltip placement */
|
||||
tooltipPlacement?: PopoverPlacement;
|
||||
/** Popconfirm content */
|
||||
popconfirmContent?: string;
|
||||
/** Popconfirm content - can be string or VNode */
|
||||
popconfirmContent?: string | VNode;
|
||||
zIndex?: number;
|
||||
quaternary?: boolean;
|
||||
[key: string]: any;
|
||||
@ -28,6 +30,7 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
class: '',
|
||||
showPopconfirmIcon: true,
|
||||
icon: '',
|
||||
localIcon: '',
|
||||
tooltipContent: '',
|
||||
@ -59,8 +62,11 @@ const handlePositiveClick = () => {
|
||||
<template>
|
||||
<NTooltip :placement="tooltipPlacement" :z-index="zIndex" :disabled="!tooltipContent">
|
||||
<template #trigger>
|
||||
<NPopconfirm :disabled="!popconfirmContent" @positive-click="handlePositiveClick">
|
||||
{{ popconfirmContent }}
|
||||
<NPopconfirm :show-icon="showPopconfirmIcon" :disabled="!popconfirmContent" @positive-click="handlePositiveClick">
|
||||
<template #default>
|
||||
<component :is="popconfirmContent" v-if="typeof popconfirmContent !== 'string'" />
|
||||
<template v-else>{{ popconfirmContent }}</template>
|
||||
</template>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
:quaternary="quaternary"
|
||||
|
@ -44,8 +44,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>
|
||||
|
@ -33,21 +33,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const attrs: UploadProps = useAttrs();
|
||||
|
||||
const value = defineModel<CommonType.IdType[]>('value', { required: false, default: [] });
|
||||
|
||||
let fileNum = 0;
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
const needRelaodData = defineModel<boolean>('needRelaodData', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const needRelaodData = ref(false);
|
||||
|
||||
defineExpose({
|
||||
refreshList: needRelaodData
|
||||
needRelaodData
|
||||
});
|
||||
watch(
|
||||
() => fileList.value,
|
||||
newValue => {
|
||||
needRelaodData.value = newValue.length > 0;
|
||||
value.value = newValue.map(item => item.id);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -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();
|
||||
@ -106,7 +108,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"
|
||||
|
@ -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);
|
||||
|
@ -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'
|
||||
}
|
||||
|
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-340px"
|
||||
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>
|
||||
<div>
|
||||
<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" />
|
||||
</div>
|
||||
<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"
|
||||
|
@ -5,7 +5,6 @@ import { useElementBounding } from '@vueuse/core';
|
||||
import { PageTab } from '@sa/materials';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouteStore } from '@/store/modules/route';
|
||||
import { useTabStore } from '@/store/modules/tab';
|
||||
import { isPC } from '@/utils/agent';
|
||||
import BetterScroll from '@/components/custom/better-scroll.vue';
|
||||
@ -18,7 +17,6 @@ defineOptions({
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
const routeStore = useRouteStore();
|
||||
const tabStore = useTabStore();
|
||||
|
||||
const bsWrapper = ref<HTMLElement>();
|
||||
@ -82,12 +80,8 @@ function getContextMenuDisabledKeys(tabId: string) {
|
||||
return disabledKeys;
|
||||
}
|
||||
|
||||
async function handleCloseTab(tab: App.Global.Tab) {
|
||||
await tabStore.removeTab(tab.id);
|
||||
|
||||
if (themeStore.resetCacheStrategy === 'close') {
|
||||
routeStore.resetRouteCache(tab.routeKey);
|
||||
}
|
||||
function handleCloseTab(tab: App.Global.Tab) {
|
||||
tabStore.removeTab(tab.id);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
|
@ -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: {
|
||||
@ -574,13 +591,14 @@ 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:
|
||||
'If you select yes, it will be cached by `keep-alive`, and the `name` and address of the matching component must be consistent',
|
||||
visibleTip: 'If you choose Hide, the route will not appear in the sidebar, but it can still be accessed.',
|
||||
statusTip: 'If you choose to disable, the route will not appear in the sidebar and cannot be accessed.',
|
||||
permsTip: "Permission string defined in the controller, such as: @SaCheckPermission('system:user:list')",
|
||||
permsTip: "Permission string defined in the controller, such as: {'@'}SaCheckPermission('system:user:list')",
|
||||
componentTip:
|
||||
'The component path to access, such as: `system/user/index`, which is in the `views` directory by default',
|
||||
pathTip:
|
||||
@ -598,6 +616,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'
|
||||
@ -655,7 +677,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: {
|
||||
@ -574,12 +591,13 @@ const local: App.I18n.Schema = {
|
||||
buttonPermissionList: '按钮权限列表',
|
||||
emptyMenu: '暂无菜单',
|
||||
menuDetail: '菜单详情',
|
||||
cascadeDeleteContent: '级联删除菜单将删除所选中的菜单,是否继续?',
|
||||
iconifyTip: 'iconify 地址:https://icones.js.org',
|
||||
isFrameTip: '选择是外链则路由地址需要以`http(s)://`开头',
|
||||
isCacheTip: '选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致',
|
||||
visibleTip: '选择隐藏则路由将不会出现在侧边栏,但仍然可以访问',
|
||||
statusTip: '选择停用则路由将不会出现在侧边栏,也不能被访问',
|
||||
permsTip: "控制器中定义的权限字符,如:`@SaCheckPermission('system:user:list')`",
|
||||
permsTip: "控制器中定义的权限字符,如:`{'@'}SaCheckPermission('system:user:list')`",
|
||||
componentTip: '访问的组件路径,如:`system/user/index`,默认在`views`目录下',
|
||||
pathTip:
|
||||
'Router path,Example:`user`,If the external network address needs to be accessed in the internal link,then `http(s)://` beginning',
|
||||
@ -592,6 +610,10 @@ const local: App.I18n.Schema = {
|
||||
required: '请选择菜单类型',
|
||||
invalid: '菜单类型不能为空'
|
||||
},
|
||||
menuIds: {
|
||||
required: '请选择菜单',
|
||||
invalid: '菜单不能为空'
|
||||
},
|
||||
icon: {
|
||||
required: '请选择菜单图标',
|
||||
invalid: '菜单图标不能为空'
|
||||
@ -653,7 +675,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 || '/';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error('getHtmlBuildTime error:', res.status, res.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
const html = await res.text();
|
||||
|
||||
const match = html.match(/<meta name="buildTime" content="(.*)">/);
|
||||
|
||||
const buildTime = match?.[1] || '';
|
||||
|
||||
return buildTime;
|
||||
return match?.[1] || null;
|
||||
} catch (error) {
|
||||
console.error('getHtmlBuildTime error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -123,31 +123,6 @@ const dynamicConstantRoutes: ElegantRoute[] = [
|
||||
icon: 'material-symbols:account-circle-full',
|
||||
hideInMenu: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'system',
|
||||
path: '/system',
|
||||
component: 'layout.base',
|
||||
meta: {
|
||||
title: 'system',
|
||||
i18nKey: 'route.system',
|
||||
localIcon: 'menu-system',
|
||||
order: 1
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'system_oss-config',
|
||||
path: '/system/oss-config',
|
||||
component: 'view.system_oss-config',
|
||||
meta: {
|
||||
title: 'system_oss-config',
|
||||
i18nKey: 'route.system_oss-config',
|
||||
constant: true,
|
||||
hideInMenu: true,
|
||||
icon: 'hugeicons:configuration-01'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
@ -80,6 +80,9 @@ export function fetchResetUserPassword(userId: CommonType.IdType, password: stri
|
||||
return request<boolean>({
|
||||
url: '/system/user/resetPwd',
|
||||
method: 'put',
|
||||
headers: {
|
||||
isEncrypt: true
|
||||
},
|
||||
data: { userId, password }
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -100,6 +100,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
authRoutes.value = Array.from(authRoutesMap.values());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
function parseRouter(route: ElegantConstRoute, parent?: ElegantConstRoute) {
|
||||
route.meta = route.meta ? route.meta : { title: route.name };
|
||||
const isLayout = route.component === 'Layout';
|
||||
@ -129,6 +130,10 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
|
||||
// @ts-expect-error no hidden field
|
||||
route.meta.hideInMenu = route.hidden;
|
||||
if (route.meta.hideInMenu && parent) {
|
||||
// @ts-expect-error parent.name is activeMenu type
|
||||
route.meta.activeMenu = parent.name;
|
||||
}
|
||||
// 是否需要keepAlive
|
||||
route.meta.keepAlive = !route.meta.noCache;
|
||||
|
||||
|
@ -98,13 +98,22 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
const removeTabIndex = tabs.value.findIndex(tab => tab.id === tabId);
|
||||
if (removeTabIndex === -1) return;
|
||||
|
||||
const removedTabRouteKey = tabs.value[removeTabIndex].routeKey;
|
||||
const isRemoveActiveTab = activeTabId.value === tabId;
|
||||
const nextTab = tabs.value[removeTabIndex + 1] || homeTab.value;
|
||||
|
||||
// remove tab
|
||||
tabs.value.splice(removeTabIndex, 1);
|
||||
|
||||
// if current tab is removed, then switch to next tab
|
||||
if (isRemoveActiveTab && nextTab) {
|
||||
await switchRouteByTab(nextTab);
|
||||
}
|
||||
|
||||
// reset route cache if cache strategy is close
|
||||
if (themeStore.resetCacheStrategy === 'close') {
|
||||
routeStore.resetRouteCache(removedTabRouteKey);
|
||||
}
|
||||
}
|
||||
|
||||
/** remove active tab */
|
||||
@ -131,9 +140,26 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
*/
|
||||
async function clearTabs(excludes: string[] = [], clearCache: boolean = false) {
|
||||
const remainTabIds = [...getFixedTabIds(tabs.value), ...excludes];
|
||||
const removedTabsIds = tabs.value.map(tab => tab.id).filter(id => !remainTabIds.includes(id));
|
||||
|
||||
// Identify tabs to be removed and collect their routeKeys if strategy is 'close'
|
||||
const tabsToRemove = tabs.value.filter(tab => !remainTabIds.includes(tab.id));
|
||||
const routeKeysToReset: RouteKey[] = [];
|
||||
|
||||
if (themeStore.resetCacheStrategy === 'close') {
|
||||
for (const tab of tabsToRemove) {
|
||||
routeKeysToReset.push(tab.routeKey);
|
||||
}
|
||||
}
|
||||
|
||||
const removedTabsIds = tabsToRemove.map(tab => tab.id);
|
||||
|
||||
// If no tabs are actually being removed based on excludes and fixed tabs, exit
|
||||
if (removedTabsIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isRemoveActiveTab = removedTabsIds.includes(activeTabId.value);
|
||||
// filterTabsByIds returns tabs NOT in removedTabsIds, so these are the tabs that will remain
|
||||
const updatedTabs = filterTabsByIds(removedTabsIds, tabs.value);
|
||||
|
||||
if (clearCache) {
|
||||
@ -152,13 +178,21 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
|
||||
|
||||
if (!isRemoveActiveTab) {
|
||||
update();
|
||||
return;
|
||||
} else {
|
||||
const activeTabCandidate = updatedTabs[updatedTabs.length - 1] || homeTab.value;
|
||||
|
||||
if (activeTabCandidate) {
|
||||
// Ensure there's a tab to switch to
|
||||
await switchRouteByTab(activeTabCandidate);
|
||||
}
|
||||
// Update the tabs array regardless of switch success or if a candidate was found
|
||||
update();
|
||||
}
|
||||
|
||||
const activeTab = updatedTabs[updatedTabs.length - 1] || homeTab.value;
|
||||
|
||||
await switchRouteByTab(activeTab);
|
||||
update();
|
||||
// After tabs are updated and route potentially switched, reset cache for removed tabs
|
||||
for (const routeKey of routeKeysToReset) {
|
||||
routeStore.resetRouteCache(routeKey);
|
||||
}
|
||||
}
|
||||
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
@ -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;
|
||||
/** 备注 */
|
||||
|
33
src/typings/app.d.ts
vendored
33
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>;
|
||||
@ -680,6 +710,7 @@ declare namespace App {
|
||||
buttonPermissionList: string;
|
||||
emptyMenu: string;
|
||||
menuDetail: string;
|
||||
cascadeDeleteContent: string;
|
||||
iconifyTip: string;
|
||||
isFrameTip: string;
|
||||
isCacheTip: string;
|
||||
@ -691,6 +722,7 @@ declare namespace App {
|
||||
form: {
|
||||
parentId: FormMsg;
|
||||
menuType: FormMsg;
|
||||
menuIds: FormMsg;
|
||||
icon: FormMsg;
|
||||
menuName: FormMsg;
|
||||
orderNum: FormMsg;
|
||||
@ -717,6 +749,7 @@ declare namespace App {
|
||||
addMenu: string;
|
||||
addChildMenu: string;
|
||||
editMenu: string;
|
||||
cascadeDelete: string;
|
||||
};
|
||||
notice: {
|
||||
title: string;
|
||||
|
3
src/typings/components.d.ts
vendored
3
src/typings/components.d.ts
vendored
@ -14,6 +14,7 @@ declare module 'vue' {
|
||||
ButtonIcon: typeof import('./../components/custom/button-icon.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']
|
||||
@ -62,6 +63,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']
|
||||
@ -81,6 +83,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']
|
||||
|
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'
|
||||
|
@ -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';
|
||||
|
@ -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"
|
||||
|
@ -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: ''
|
||||
};
|
||||
}
|
||||
|
@ -155,8 +155,13 @@ async function edit(row: TableDataWithIndex<Api.System.Dept>) {
|
||||
}
|
||||
|
||||
async function addInRow(row: TableDataWithIndex<Api.System.Dept>) {
|
||||
handleAdd();
|
||||
editingData.value = jsonClone(row);
|
||||
handleAdd();
|
||||
}
|
||||
|
||||
async function handleAddOperate() {
|
||||
editingData.value = null;
|
||||
handleAdd();
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -170,7 +175,7 @@ async function addInRow(row: TableDataWithIndex<Api.System.Dept>) {
|
||||
:loading="loading"
|
||||
:show-add="hasAuth('system:dept:add')"
|
||||
:show-delete="false"
|
||||
@add="handleAdd"
|
||||
@add="handleAddOperate"
|
||||
@refresh="getData"
|
||||
>
|
||||
<template #prefix>
|
||||
|
@ -55,7 +55,7 @@ const model: Model = reactive(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
parentId: props.rowData?.deptId,
|
||||
parentId: '',
|
||||
deptName: '',
|
||||
deptCategory: '',
|
||||
orderNum: null,
|
||||
@ -80,6 +80,7 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
function handleUpdateModelWhenEdit() {
|
||||
if (props.operateType === 'add') {
|
||||
Object.assign(model, createDefaultModel());
|
||||
model.parentId = props.rowData?.deptId || 0;
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
@ -185,7 +186,7 @@ watch(visible, () => {
|
||||
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.system.dept.parentId')" path="parentId">
|
||||
<NFormItem v-if="model.parentId != 0" :label="$t('page.system.dept.parentId')" path="parentId">
|
||||
<NTreeSelect
|
||||
v-model:value="model.parentId"
|
||||
:loading="deptLoading"
|
||||
|
@ -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,37 +191,17 @@ 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({
|
||||
const payload = {
|
||||
menuName,
|
||||
path,
|
||||
path: processPath(model.path),
|
||||
parentId,
|
||||
orderNum,
|
||||
queryParam,
|
||||
queryParam: processQueryParam(queryParam),
|
||||
isFrame,
|
||||
isCache,
|
||||
menuType,
|
||||
@ -171,39 +209,18 @@ async function handleSubmit() {
|
||||
status,
|
||||
perms,
|
||||
icon,
|
||||
component,
|
||||
component: processComponent(component),
|
||||
remark
|
||||
});
|
||||
};
|
||||
|
||||
const { error } =
|
||||
props.operateType === 'add' ? await fetchCreateMenu(payload) : await fetchUpdateMenu({ ...payload, menuId });
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
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">
|
||||
|
@ -34,7 +34,7 @@ function closeDrawer() {
|
||||
|
||||
function handleClose() {
|
||||
closeDrawer();
|
||||
if (fileUploadRef.value?.refreshList) {
|
||||
if (fileUploadRef.value?.needRelaodData) {
|
||||
emit('close');
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ const {
|
||||
text
|
||||
type="primary"
|
||||
icon="material-symbols:database"
|
||||
tooltipContent="数据范权限"
|
||||
tooltipContent="数据范围权限"
|
||||
onClick={() => handleDataScope(row)}
|
||||
/>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { h, ref, watch } from 'vue';
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
import { getToken } from '@/store/modules/auth/shared';
|
||||
import { useDownload } from '@/hooks/business/download';
|
||||
@ -77,7 +77,7 @@ function handleError(options: { file: UploadFileInfo; event?: ProgressEvent }) {
|
||||
const responseText = event?.target?.responseText;
|
||||
const msg = JSON.parse(responseText).msg;
|
||||
message.value = msg;
|
||||
window.$message?.error(msg || $t('common.importFail'));
|
||||
window.$message?.error(() => h('div', { innerHTML: msg || $t('common.importFail') }));
|
||||
success.value = false;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user