diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 00000000..786f8d98 --- /dev/null +++ b/.drone.yml @@ -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 diff --git a/.gitee/workflows/linter.yml b/.gitee/workflows/linter.yml deleted file mode 100644 index 450ec865..00000000 --- a/.gitee/workflows/linter.yml +++ /dev/null @@ -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 diff --git a/.gitee/workflows/release.yml b/.gitee/workflows/release.yml deleted file mode 100644 index 0bf7c92d..00000000 --- a/.gitee/workflows/release.yml +++ /dev/null @@ -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}} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..d2d3368f --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 450ec865..a11ed3c9 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -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) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a292c36d --- /dev/null +++ b/CHANGELOG.md @@ -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 版本正式发布,此版本不包含工作流与多语言,请期待后续版本发布。 + +###    ❤️ 贡献者 + +首次发版不展示过多贡献者,敬请谅解 + +[![soybeanjs](https://github.com/honghuangdc.png?size=48)](https://github.com/honghuangdc)  [![xlsea](https://github.com/m-xlsea.png?size=48)](https://gitee.com/xlsea)  [![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)  [![wangqiqi95](https://github.com/wangqiqi95.png?size=48)](https://github.com/wangqiqi95)  diff --git a/README.md b/README.md index 9b669db4..05152803 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@

一个基于 RuoYi-Vue-Plus 的后端能力和 Soybean Admin 前端特性的现代化多租户管理系统

- Gitee + Gitee + Github vue typescript 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 ## 💬 交流群 - + ## 🧧 捐献作者 diff --git a/docs/sql/sys_menu.sql b/docs/sql/sys_menu.sql index c1608184..a04de6d6 100644 --- a/docs/sql/sys_menu.sql +++ b/docs/sql/sys_menu.sql @@ -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' ); diff --git a/docs/template/index-tree.vue.vm b/docs/template/index-tree.vue.vm index 0ab2b7b7..cf6ebed6 100644 --- a/docs/template/index-tree.vue.vm +++ b/docs/template/index-tree.vue.vm @@ -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) { diff --git a/package.json b/package.json index fecc7bba..d044bea0 100644 --- a/package.json +++ b/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", diff --git a/packages/hooks/src/use-count-down.ts b/packages/hooks/src/use-count-down.ts index bfad064b..4f95b731 100644 --- a/packages/hooks/src/use-count-down.ts +++ b/packages/hooks/src/use-count-down.ts @@ -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(); }); diff --git a/src/assets/svg-icon/bell.svg b/src/assets/svg-icon/bell.svg new file mode 100644 index 00000000..bffd0ce6 --- /dev/null +++ b/src/assets/svg-icon/bell.svg @@ -0,0 +1 @@ + diff --git a/src/components/common/data-table.vue b/src/components/common/data-table.vue new file mode 100644 index 00000000..cc5ce083 --- /dev/null +++ b/src/components/common/data-table.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/components/custom/button-icon.vue b/src/components/custom/button-icon.vue index d7e0b5cb..604a4918 100644 --- a/src/components/custom/button-icon.vue +++ b/src/components/custom/button-icon.vue @@ -1,5 +1,5 @@ + + + + diff --git a/src/layouts/modules/global-header/index.vue b/src/layouts/modules/global-header/index.vue index 93c15ee2..85995c26 100644 --- a/src/layouts/modules/global-header/index.vue +++ b/src/layouts/modules/global-header/index.vue @@ -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(authStore.userInfo?.user?.tenantId || '0

- + + (); @@ -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() { diff --git a/src/layouts/modules/theme-drawer/index.vue b/src/layouts/modules/theme-drawer/index.vue index aa937d5a..1cb11390 100644 --- a/src/layouts/modules/theme-drawer/index.vue +++ b/src/layouts/modules/theme-drawer/index.vue @@ -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(); + diff --git a/src/layouts/modules/theme-drawer/modules/page-fun.vue b/src/layouts/modules/theme-drawer/modules/page-fun.vue index ef48adc0..aa0aadab 100644 --- a/src/layouts/modules/theme-drawer/modules/page-fun.vue +++ b/src/layouts/modules/theme-drawer/modules/page-fun.vue @@ -130,6 +130,9 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra + + + diff --git a/src/layouts/modules/theme-drawer/modules/table-props.vue b/src/layouts/modules/theme-drawer/modules/table-props.vue new file mode 100644 index 00000000..ac84e5ce --- /dev/null +++ b/src/layouts/modules/theme-drawer/modules/table-props.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index 8d076e9f..c2c4c9f5 100644 --- a/src/locales/langs/en-us.ts +++ b/src/locales/langs/en-us.ts @@ -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', diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts index 47b8d889..46b37507 100644 --- a/src/locales/langs/zh-cn.ts +++ b/src/locales/langs/zh-cn.ts @@ -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: '通知公告列表', diff --git a/src/plugins/app.ts b/src/plugins/app.ts index 1a0d8999..4943341f 100644 --- a/src/plugins/app.ts +++ b/src/plugins/app.ts @@ -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 { 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(//); - - const buildTime = match?.[1] || ''; - - return buildTime; + const html = await res.text(); + const match = html.match(//); + return match?.[1] || null; + } catch (error) { + console.error('getHtmlBuildTime error:', error); + return null; + } } diff --git a/src/router/routes/index.ts b/src/router/routes/index.ts index 1b2ee7a4..4cbcd1bf 100644 --- a/src/router/routes/index.ts +++ b/src/router/routes/index.ts @@ -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' - } - } - ] } ]; diff --git a/src/service/api/system/menu.ts b/src/service/api/system/menu.ts index 29853dc8..926b1ad7 100644 --- a/src/service/api/system/menu.ts +++ b/src/service/api/system/menu.ts @@ -59,3 +59,11 @@ export function fetchGetTenantPackageMenuTreeSelect(packageId: CommonType.IdType method: 'get' }); } + +// 级联删除菜单 +export function fetchCascadeDeleteMenu(menuIds: CommonType.IdType[]) { + return request({ + url: `/system/menu/cascade/${menuIds.join(',')}`, + method: 'delete' + }); +} diff --git a/src/service/api/system/user.ts b/src/service/api/system/user.ts index ba9b942a..e7c03d86 100644 --- a/src/service/api/system/user.ts +++ b/src/service/api/system/user.ts @@ -80,6 +80,9 @@ export function fetchResetUserPassword(userId: CommonType.IdType, password: stri return request({ url: '/system/user/resetPwd', method: 'put', + headers: { + isEncrypt: true + }, data: { userId, password } }); } diff --git a/src/store/modules/notice/index.ts b/src/store/modules/notice/index.ts index f2904a09..8af29067 100644 --- a/src/store/modules/notice/index.ts +++ b/src/store/modules/notice/index.ts @@ -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 }; diff --git a/src/store/modules/route/index.ts b/src/store/modules/route/index.ts index 7ac474b6..6edff7ea 100644 --- a/src/store/modules/route/index.ts +++ b/src/store/modules/route/index.ts @@ -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; diff --git a/src/store/modules/tab/index.ts b/src/store/modules/tab/index.ts index ace7d7dc..78aaac69 100644 --- a/src/store/modules/tab/index.ts +++ b/src/store/modules/tab/index.ts @@ -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(); diff --git a/src/theme/settings.ts b/src/theme/settings.ts index 57bc26d3..bb8b9bb9 100644 --- a/src/theme/settings.ts +++ b/src/theme/settings.ts @@ -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: { diff --git a/src/typings/api/monitor.api.d.ts b/src/typings/api/monitor.api.d.ts index acec1ce6..06bc1ebe 100644 --- a/src/typings/api/monitor.api.d.ts +++ b/src/typings/api/monitor.api.d.ts @@ -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 */ diff --git a/src/typings/api/system.api.d.ts b/src/typings/api/system.api.d.ts index 23e5c82f..d91b0fef 100644 --- a/src/typings/api/system.api.d.ts +++ b/src/typings/api/system.api.d.ts @@ -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>; + /** 通知公告类型 */ + 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; + /** 授权类型 */ + 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; /** 备注 */ diff --git a/src/typings/app.d.ts b/src/typings/app.d.ts index b37b9083..36c232c2 100644 --- a/src/typings/app.d.ts +++ b/src/typings/app.d.ts @@ -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; + bordered: string; + bottomBordered: string; + singleColumn: string; + singleLine: string; + striped: string; + }; themeDrawerTitle: string; pageFunTitle: string; resetCacheStrategy: { title: string } & Record; @@ -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; diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 6ae162ed..8be6129d 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -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'] diff --git a/src/typings/union-key.d.ts b/src/typings/union-key.d.ts index a783294b..dc27a701 100644 --- a/src/typings/union-key.d.ts +++ b/src/typings/union-key.d.ts @@ -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' diff --git a/src/utils/format.ts b/src/utils/icon-tag-format.ts similarity index 100% rename from src/utils/format.ts rename to src/utils/icon-tag-format.ts diff --git a/src/views/_builtin/user-center/modules/online-table.vue b/src/views/_builtin/user-center/modules/online-table.vue index d5d4c8af..e1c3eaad 100644 --- a/src/views/_builtin/user-center/modules/online-table.vue +++ b/src/views/_builtin/user-center/modules/online-table.vue @@ -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'; diff --git a/src/views/demo/demo/index.vue b/src/views/demo/demo/index.vue index fcc883fc..b9818f03 100644 --- a/src/views/demo/demo/index.vue +++ b/src/views/demo/demo/index.vue @@ -184,11 +184,10 @@ async function handleExport() { @refresh="getData" /> - - -import { getBrowserIcon, getOsIcon } from '@/utils/format'; +import { getBrowserIcon, getOsIcon } from '@/utils/icon-tag-format'; import { $t } from '@/locales'; defineOptions({ diff --git a/src/views/monitor/online/index.vue b/src/views/monitor/online/index.vue index 80b3f7e1..df4bc061 100644 --- a/src/views/monitor/online/index.vue +++ b/src/views/monitor/online/index.vue @@ -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'; diff --git a/src/views/monitor/operlog/modules/oper-log-view-drawer.vue b/src/views/monitor/operlog/modules/oper-log-view-drawer.vue index 849364c0..c6baea2b 100644 --- a/src/views/monitor/operlog/modules/oper-log-view-drawer.vue +++ b/src/views/monitor/operlog/modules/oper-log-view-drawer.vue @@ -1,6 +1,6 @@ @@ -170,7 +175,7 @@ async function addInRow(row: TableDataWithIndex) { :loading="loading" :show-add="hasAuth('system:dept:add')" :show-delete="false" - @add="handleAdd" + @add="handleAddOperate" @refresh="getData" >