Compare commits

..

1 Commits

Author SHA1 Message Date
Soybean
f805a328ec reafctor(projects): use new elegant-router plugin 2025-07-31 00:26:31 +08:00
57 changed files with 1758 additions and 2511 deletions

13
.vscode/settings.json vendored
View File

@@ -4,18 +4,7 @@
"source.organizeImports": "never"
},
"editor.formatOnSave": false,
"eslint.validate": [
"html",
"css",
"scss",
"json",
"jsonc",
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"eslint.validate": ["html", "css", "scss", "json", "jsonc"],
"i18n-ally.displayLanguage": "zh-cn",
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.enabledFrameworks": ["vue"],

View File

@@ -13,10 +13,8 @@
[![gitcode star](https://gitcode.com/soybeanjs/soybean-admin/star/badge.svg)](https://gitcode.com/soybeanjs/soybean-admin)
[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
<div style="display: flex; gap: 12px; align-items: center;">
<a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
> [!NOTE]
> If you think `SoybeanAdmin` is helpful to you, or you like our project, please give us a ⭐️ on GitHub. Your support is the driving force for us to continue to improve and add new features! Thank you for your support!

View File

@@ -13,10 +13,7 @@
[![gitcode star](https://gitcode.com/soybeanjs/soybean-admin/star/badge.svg)](https://gitcode.com/soybeanjs/soybean-admin)
[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
<div style="display: flex; gap: 12px; align-items: center;">
<a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
> [!NOTE]
> 如果您觉得 `SoybeanAdmin`对您有所帮助,或者您喜欢我们的项目,请在 GitHub 上给我们一个 ⭐️。您的支持是我们持续改进和增加新功能的动力!感谢您的支持!

View File

@@ -1,4 +1,4 @@
import type { ProxyOptions } from 'vite';
import type { HttpProxy, ProxyOptions } from 'vite';
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
import { consola } from 'consola';
import { createServiceConfig } from '../../src/utils/service';
@@ -33,7 +33,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean
proxy[item.proxyPattern] = {
target: item.baseURL,
changeOrigin: true,
configure: (_proxy, options) => {
configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
if (!enableLog) return;

View File

@@ -54,54 +54,53 @@
"@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*",
"@sa/utils": "workspace:*",
"@vueuse/core": "13.9.0",
"@vueuse/core": "13.5.0",
"clipboard": "2.0.11",
"dayjs": "1.11.18",
"dayjs": "1.11.13",
"defu": "6.1.4",
"echarts": "6.0.0",
"echarts": "5.6.0",
"json5": "2.2.3",
"naive-ui": "2.43.1",
"naive-ui": "2.42.0",
"nprogress": "0.2.0",
"pinia": "3.0.3",
"tailwind-merge": "3.3.1",
"vue": "3.5.22",
"vue": "3.5.17",
"vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.12",
"vue-i18n": "11.1.10",
"vue-router": "4.5.1"
},
"devDependencies": {
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.395",
"@iconify/json": "2.2.359",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.1",
"@types/node": "24.7.2",
"@types/node": "24.0.15",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.5.3",
"@unocss/preset-icons": "66.5.3",
"@unocss/preset-uno": "66.5.3",
"@unocss/transformer-directives": "66.5.3",
"@unocss/transformer-variant-group": "66.5.3",
"@unocss/vite": "66.5.3",
"@vitejs/plugin-vue": "6.0.1",
"@vitejs/plugin-vue-jsx": "5.1.1",
"@unocss/eslint-config": "66.3.3",
"@unocss/preset-icons": "66.3.3",
"@unocss/preset-uno": "66.3.3",
"@unocss/transformer-directives": "66.3.3",
"@unocss/transformer-variant-group": "66.3.3",
"@unocss/vite": "66.3.3",
"@vitejs/plugin-vue": "6.0.0",
"@vitejs/plugin-vue-jsx": "5.0.1",
"consola": "3.4.2",
"elegant-router": "1.0.4",
"eslint": "9.37.0",
"eslint-plugin-vue": "10.5.0",
"elegant-router": "1.0.4-beta.10",
"eslint": "9.31.0",
"eslint-plugin-vue": "10.3.0",
"kolorist": "1.8.0",
"sass": "1.93.2",
"simple-git-hooks": "2.13.1",
"tsx": "4.20.6",
"typescript": "5.9.3",
"unplugin-icons": "22.4.2",
"unplugin-vue-components": "29.1.0",
"vite": "7.1.9",
"sass": "1.89.2",
"simple-git-hooks": "2.13.0",
"tsx": "4.20.3",
"typescript": "5.8.3",
"unplugin-icons": "22.1.0",
"unplugin-vue-components": "28.8.0",
"vite": "7.0.5",
"vite-plugin-progress": "0.0.7",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "8.0.2",
"vite-plugin-vue-devtools": "7.7.7",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.1.1"
"vue-tsc": "3.0.3"
},
"simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify",

View File

@@ -11,7 +11,7 @@
},
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.12.2",
"axios": "1.10.0",
"axios-retry": "4.5.0",
"qs": "6.14.0"
},

View File

@@ -3,7 +3,6 @@ import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } f
import axiosRetry from 'axios-retry';
import { nanoid } from '@sa/utils';
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
import { transformResponse } from './shared';
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
import type {
CustomAxiosRequestConfig,
@@ -54,8 +53,6 @@ function createCommonRequest<
async response => {
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
await transformResponse(response);
if (responseType !== 'json' || opts.isBackendSuccess(response)) {
return Promise.resolve(response);
}

View File

@@ -1,5 +1,4 @@
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import type { ResponseType } from './type';
export function getContentType(config: InternalAxiosRequestConfig) {
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
@@ -27,53 +26,3 @@ export function isResponseJson(response: AxiosResponse) {
return responseType === 'json' || responseType === undefined;
}
export async function transformResponse(response: AxiosResponse) {
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
if (responseType === 'json') return;
const isJson = response.headers['content-type']?.includes('application/json');
if (!isJson) return;
if (responseType === 'blob') {
await transformBlobToJson(response);
}
if (responseType === 'arrayBuffer') {
await transformArrayBufferToJson(response);
}
}
export async function transformBlobToJson(response: AxiosResponse) {
try {
let data = response.data;
if (typeof data === 'string') {
data = JSON.parse(data);
}
if (Object.prototype.toString.call(data) === '[object Blob]') {
const json = await data.text();
data = JSON.parse(json);
}
response.data = data;
} catch {}
}
export async function transformArrayBufferToJson(response: AxiosResponse) {
try {
let data = response.data;
if (typeof data === 'string') {
data = JSON.parse(data);
}
if (Object.prototype.toString.call(data) === '[object ArrayBuffer]') {
const json = new TextDecoder().decode(data);
data = JSON.parse(json);
}
response.data = data;
} catch {}
}

View File

@@ -95,27 +95,3 @@
.chrome-tab_dark .chrome-tab-divider {
background-color: rgba(255, 255, 255, 0.9);
}
.slider-tab {
background-color: transparent;
height: 100%;
border-bottom: 2px solid transparent;
}
.slider-tab_dark {
background-color: transparent;
}
.slider-tab:hover {
color: var(--soy-primary-color);
}
.slider-tab_active {
color: var(--soy-primary-color);
background-color: var(--soy-primary-color-opacity1);
border-bottom-color: var(--soy-primary-color);
}
.slider-tab_active_dark {
background-color: var(--soy-primary-color-opacity2);
}

View File

@@ -10,10 +10,6 @@ declare const styles: {
readonly 'chrome-tab_dark': string;
readonly 'chrome-tab-divider': string;
readonly 'svg-close': string;
readonly 'slider-tab': string;
readonly 'slider-tab_active': string;
readonly 'slider-tab_active_dark': string;
readonly 'slider-tab_dark': string;
};
export default styles;

View File

@@ -5,7 +5,6 @@ import type { PageTabMode, PageTabProps } from '../../types';
import { ACTIVE_COLOR, createTabCssVars } from './shared';
import ChromeTab from './chrome-tab.vue';
import ButtonTab from './button-tab.vue';
import SliderTab from './slider-tab.vue';
import SvgClose from './svg-close.vue';
import style from './index.module.css';
@@ -27,7 +26,7 @@ interface Emits {
const emit = defineEmits<Emits>();
const activeTabComponent = computed(() => {
const { mode, chromeClass, buttonClass, sliderClass } = props;
const { mode, chromeClass, buttonClass } = props;
const tabComponentMap = {
chrome: {
@@ -37,10 +36,6 @@ const activeTabComponent = computed(() => {
button: {
component: ButtonTab,
class: buttonClass
},
slider: {
component: SliderTab,
class: sliderClass
}
} satisfies Record<PageTabMode, { component: Component; class?: string }>;
@@ -50,7 +45,7 @@ const activeTabComponent = computed(() => {
const cssVars = computed(() => createTabCssVars(props.activeColor));
const bindProps = computed(() => {
const { chromeClass: _chromeCls, buttonClass: _btnCls, sliderClass: _sliderCls, ...rest } = props;
const { chromeClass: _chromeCls, buttonClass: _btnCls, ...rest } = props;
return rest;
});

View File

@@ -1,53 +0,0 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types';
import style from './index.module.css';
defineOptions({
name: 'SliderTab'
});
defineProps<PageTabProps>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn;
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn;
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-6px whitespace-nowrap px-12px py-4px"
:class="[
style['slider-tab'],
{ [style['slider-tab_dark']]: darkMode },
{ [style['slider-tab_active']]: active },
{ [style['slider-tab_active_dark']]: active && darkMode }
]"
>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
</div>
</template>
<style scoped></style>

View File

@@ -239,7 +239,7 @@ export type LayoutCssVars = {
*
* @default chrome
*/
export type PageTabMode = 'button' | 'chrome' | 'slider';
export type PageTabMode = 'button' | 'chrome';
export interface PageTabProps {
/** Whether is dark mode */
@@ -262,8 +262,6 @@ export interface PageTabProps {
buttonClass?: string;
/** The class of the chrome tab */
chromeClass?: string;
/** The class of the title tab */
sliderClass?: string;
/** Whether the tab is active */
active?: boolean;
/** The color of the active tab */

View File

@@ -13,16 +13,15 @@
}
},
"devDependencies": {
"@soybeanjs/changelog": "0.3.25",
"bumpp": "10.3.1",
"c12": "3.3.0",
"@soybeanjs/changelog": "0.3.24",
"bumpp": "10.2.0",
"c12": "3.1.0",
"cac": "6.7.14",
"consola": "3.4.2",
"enquirer": "2.4.1",
"execa": "9.6.0",
"kolorist": "1.8.0",
"npm-check-updates": "19.0.0",
"picomatch": "4.0.3",
"npm-check-updates": "18.0.1",
"rimraf": "6.0.1"
}
}

View File

@@ -14,7 +14,7 @@
"crypto-js": "4.2.0",
"klona": "2.0.6",
"localforage": "1.10.0",
"nanoid": "5.1.6"
"nanoid": "5.1.5"
},
"devDependencies": {
"@types/crypto-js": "4.2.2"

View File

@@ -32,8 +32,7 @@ export function createStorage<T extends object>(type: StorageType, storagePrefix
storageData = JSON.parse(json);
} catch {}
// storageData may be `false` if it is boolean type
if (storageData !== null) {
if (storageData) {
return storageData as T[K];
}
}

2869
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
<script lang="ts" setup>
import { computed, useSlots } from 'vue';
import type { PopoverPlacement } from 'naive-ui';
defineOptions({ name: 'IconTooltip' });
interface Props {
icon?: string;
localIcon?: string;
desc?: string;
placement?: PopoverPlacement;
}
const props = withDefaults(defineProps<Props>(), {
icon: 'mdi-help-circle',
localIcon: '',
desc: '',
placement: 'top'
});
const slots = useSlots();
const hasCustomTrigger = computed(() => Boolean(slots.trigger));
if (!hasCustomTrigger.value && !props.icon && !props.localIcon) {
throw new Error('icon or localIcon is required when no custom trigger slot is provided');
}
</script>
<template>
<NTooltip :placement="placement">
<template #trigger>
<slot name="trigger">
<div class="cursor-pointer">
<SvgIcon :icon="icon" :local-icon="localIcon" />
</div>
</slot>
</template>
<slot>
<span>{{ desc }}</span>
</slot>
</NTooltip>
</template>

View File

@@ -40,8 +40,7 @@ export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRec
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
chrome: 'theme.layout.tab.mode.chrome',
button: 'theme.layout.tab.mode.button',
slider: 'theme.layout.tab.mode.slider'
button: 'theme.layout.tab.mode.button'
};
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
@@ -58,6 +57,13 @@ export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode,
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
close: 'theme.layout.resetCacheStrategy.close',
refresh: 'theme.layout.resetCacheStrategy.refresh'
};
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
export const DARK_CLASS = 'dark';
export const watermarkTimeFormatOptions = [

View File

@@ -38,13 +38,6 @@ export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptio
getColumns
});
// calculate the total width of the table this is used for horizontal scrolling
const scrollX = computed(() => {
return result.columns.value.reduce((acc, column) => {
return acc + Number(column.width ?? column.minWidth ?? 120);
}, 0);
});
scope.run(() => {
watch(
() => appStore.locale,
@@ -58,10 +51,7 @@ export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptio
scope.stop();
});
return {
...result,
scrollX
};
return result;
}
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;

View File

@@ -30,17 +30,15 @@ const { selectedKey } = useMenu();
/>
</Teleport>
<Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
<div class="h-full pt-2">
<FirstLevelMenu
:menus="firstLevelMenus"
:active-menu-key="activeFirstLevelMenuKey"
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectFirstLevelMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
/>
</div>
<FirstLevelMenu
:menus="firstLevelMenus"
:active-menu-key="activeFirstLevelMenuKey"
:sider-collapse="appStore.siderCollapse"
:dark-mode="themeStore.darkMode"
:theme-color="themeStore.themeColor"
@select="handleSelectFirstLevelMenu"
@toggle-sider-collapse="appStore.toggleSiderCollapse"
/>
</Teleport>
</template>

View File

@@ -169,9 +169,7 @@ init();
<div
ref="tabRef"
class="h-full flex pr-18px"
:class="[
themeStore.tab.mode === 'chrome' || themeStore.tab.mode === 'slider' ? 'items-end' : 'items-center gap-12px'
]"
:class="[themeStore.tab.mode === 'chrome' ? 'items-end' : 'items-center gap-12px']"
>
<PageTab
v-for="tab in tabStore.tabs"

View File

@@ -80,7 +80,7 @@ function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
class="flex-col-center cursor-pointer"
@click="handleChangeMode(key)"
>
<IconTooltip :placement="item.placement">
<NTooltip :placement="item.placement">
<template #trigger>
<div
class="h-64px w-96px gap-6px rd-4px p-6px shadow ring-2 ring-transparent transition-all hover:ring-primary"
@@ -92,7 +92,7 @@ function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
</div>
</template>
{{ $t(`theme.layout.layoutMode.${key}_detail`) }}
</IconTooltip>
</NTooltip>
<p class="mt-8px text-12px">{{ $t(themeLayoutModeRecord[key]) }}</p>
</div>
</div>

View File

@@ -13,7 +13,7 @@ defineProps<Props>();
<template>
<div class="w-full flex-y-center justify-between">
<div class="flex-y-center">
<div>
<span class="pr-8px text-base-text">{{ label }}</span>
<slot name="suffix"></slot>
</div>

View File

@@ -6,7 +6,6 @@ import AppearanceSettings from './modules/appearance/index.vue';
import LayoutSettings from './modules/layout/index.vue';
import GeneralSettings from './modules/general/index.vue';
import ConfigOperation from './modules/config-operation.vue';
import PresetSettings from './modules/preset/index.vue';
defineOptions({
name: 'ThemeDrawer'
@@ -34,7 +33,6 @@ const drawerWidth = computed(() => {
<NTab name="appearance" :tab="$t('theme.tabs.appearance')"></NTab>
<NTab name="layout" :tab="$t('theme.tabs.layout')"></NTab>
<NTab name="general" :tab="$t('theme.tabs.general')"></NTab>
<NTab name="preset" :tab="$t('theme.tabs.preset')"></NTab>
</NTabs>
<div class="min-h-400px">
@@ -42,7 +40,6 @@ const drawerWidth = computed(() => {
<AppearanceSettings v-if="activeTab === 'appearance'" />
<LayoutSettings v-else-if="activeTab === 'layout'" />
<GeneralSettings v-else-if="activeTab === 'general'" />
<PresetSettings v-else-if="activeTab === 'preset'" />
</KeepAlive>
</div>

View File

@@ -36,28 +36,27 @@ const swatches: string[] = [
<template>
<NDivider>{{ $t('theme.appearance.themeColor.title') }}</NDivider>
<div class="flex-col-stretch gap-12px">
<SettingItem key="recommend-color" :label="$t('theme.appearance.recommendColor')">
<template #suffix>
<IconTooltip>
<p>
<span class="pr-12px">{{ $t('theme.appearance.recommendColorDesc') }}</span>
<br />
<NButton
text
tag="a"
href="https://uicolors.app/create"
target="_blank"
rel="noopener noreferrer"
class="text-gray"
>
https://uicolors.app/create
</NButton>
</p>
</IconTooltip>
<NTooltip placement="top-start">
<template #trigger>
<SettingItem key="recommend-color" :label="$t('theme.appearance.recommendColor')">
<NSwitch v-model:value="themeStore.recommendColor" />
</SettingItem>
</template>
<NSwitch v-model:value="themeStore.recommendColor" />
</SettingItem>
<p>
<span class="pr-12px">{{ $t('theme.appearance.recommendColorDesc') }}</span>
<br />
<NButton
text
tag="a"
href="https://uicolors.app/create"
target="_blank"
rel="noopener noreferrer"
class="text-gray"
>
https://uicolors.app/create
</NButton>
</p>
</NTooltip>
<SettingItem
v-for="(_, key) in themeStore.themeColors"
:key="key"

View File

@@ -19,9 +19,6 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
<NDivider>{{ $t('theme.layout.content.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="1" :label="$t('theme.layout.content.scrollMode.title')">
<template #suffix>
<IconTooltip :desc="$t('theme.layout.content.scrollMode.tip')" />
</template>
<NSelect
v-model:value="themeStore.layout.scrollMode"
:options="translateOptions(themeScrollModeOptions)"

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { themeTabModeOptions } from '@/constants/app';
import { resetCacheStrategyOptions, themeTabModeOptions } from '@/constants/app';
import { useThemeStore } from '@/store/modules/theme';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
@@ -15,13 +15,18 @@ const themeStore = useThemeStore();
<template>
<NDivider>{{ $t('theme.layout.tab.title') }}</NDivider>
<TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
<SettingItem key="0" :label="$t('theme.layout.resetCacheStrategy.title')">
<NSelect
v-model:value="themeStore.resetCacheStrategy"
:options="translateOptions(resetCacheStrategyOptions)"
size="small"
class="w-120px"
/>
</SettingItem>
<SettingItem key="1" :label="$t('theme.layout.tab.visible')">
<NSwitch v-model:value="themeStore.tab.visible" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="2" :label="$t('theme.layout.tab.cache')">
<template #suffix>
<IconTooltip :desc="$t('theme.layout.tab.cacheTip')" />
</template>
<NSwitch v-model:value="themeStore.tab.cache" />
</SettingItem>
<SettingItem v-if="themeStore.tab.visible" key="3" :label="$t('theme.layout.tab.height')">

View File

@@ -1,15 +0,0 @@
<script setup lang="ts">
import ThemePreset from './modules/theme-preset.vue';
defineOptions({
name: 'PresetSettings'
});
</script>
<template>
<div class="flex-col-stretch gap-16px">
<ThemePreset />
</div>
</template>
<style scoped></style>

View File

@@ -1,147 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
defineOptions({
name: 'ThemePreset'
});
type ThemePreset = Pick<
App.Theme.ThemeSetting,
| 'themeScheme'
| 'grayscale'
| 'colourWeakness'
| 'recommendColor'
| 'themeColor'
| 'otherColor'
| 'isInfoFollowPrimary'
| 'layout'
| 'page'
| 'header'
| 'tab'
| 'fixedHeaderAndTab'
| 'sider'
| 'footer'
| 'watermark'
| 'tokens'
> & {
name: string;
desc: string;
i18nkey?: string;
version: string;
};
const presetModules = import.meta.glob('@/theme/preset/*.json', { eager: true, import: 'default' });
const themeStore = useThemeStore();
// Extract preset data
const presets = computed(() =>
Object.entries(presetModules)
.map(([path, presetData]) => {
const fileName = path.split('/').pop()?.replace('.json', '') || '';
return {
id: fileName,
...(presetData as ThemePreset)
};
})
.sort((a, b) => {
if (a.name === 'default') return -1;
if (b.name === 'default') return 1;
return a.name.localeCompare(b.name);
})
);
const getPresetName = (preset: ThemePreset): string => {
if (!preset.i18nkey) return preset.name;
try {
const key = `${preset.i18nkey}.name` as App.I18n.I18nKey;
const translated = $t(key);
return translated !== key ? translated : preset.name;
} catch {
return preset.name;
}
};
const getPresetDesc = (preset: ThemePreset): string => {
if (!preset.i18nkey) return preset.desc;
try {
const key = `${preset.i18nkey}.desc` as App.I18n.I18nKey;
const translated = $t(key);
return translated !== key ? translated : preset.desc;
} catch {
return preset.desc;
}
};
const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark, ...rest }: ThemePreset): void => {
themeStore.setThemeScheme(themeScheme);
themeStore.setGrayscale(grayscale);
themeStore.setColourWeakness(colourWeakness);
themeStore.setThemeLayout(layout.mode);
themeStore.setWatermarkEnableUserName(watermark.enableUserName);
themeStore.setWatermarkEnableTime(watermark.enableTime);
Object.assign(themeStore, {
...rest,
layout: { ...themeStore.layout, scrollMode: layout.scrollMode },
page: { ...rest.page },
header: { ...rest.header },
tab: { ...rest.tab },
sider: { ...rest.sider },
footer: { ...rest.footer },
watermark: { ...watermark },
tokens: { ...rest.tokens }
});
window.$message?.success($t('theme.appearance.preset.applySuccess'));
};
</script>
<template>
<NDivider>{{ $t('theme.appearance.preset.title') }}</NDivider>
<div class="flex flex-col gap-3">
<div
v-for="preset in presets"
:key="preset.id"
class="border border-primary/10 rounded-lg border-solid bg-white/5 p-3 backdrop-blur-10 transition-all duration-300 hover:(shadow-md -translate-y-0.5)"
>
<div class="mb-2 flex items-center justify-between">
<div class="min-w-0 w-full flex flex-1 items-center justify-between gap-2">
<h5 class="m-0 truncate text-sm text-primary font-600">
{{ getPresetName(preset) }}
</h5>
<NBadge :value="`v${preset.version}`" type="info" size="small" class="flex-shrink-0 opacity-80" />
</div>
<NButton type="primary" size="tiny" ghost round class="ml-2 flex-shrink-0" @click="applyPreset(preset)">
{{ $t('theme.appearance.preset.apply') }}
</NButton>
</div>
<p class="line-clamp-2 mb-3 text-xs text-gray-500 leading-4">{{ getPresetDesc(preset) }}</p>
<div class="flex items-center justify-between">
<div class="flex gap-1">
<div
v-for="(color, key) in { primary: preset.themeColor, ...preset.otherColor }"
:key="key"
class="h-3 w-3 cursor-pointer border border-white/30 rounded-full transition-transform hover:scale-110"
:style="{ backgroundColor: color }"
:class="{ 'ring-1 ring-primary/50': key === 'primary' }"
:title="key"
/>
</div>
<div class="flex items-center gap-1">
<div class="text-lg">
{{ preset.themeScheme === 'dark' ? '🌙' : '☀️' }}
</div>
<div class="text-lg">
{{ preset.grayscale ? '🎨' : '' }}
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -62,8 +62,7 @@ const local: App.I18n.Schema = {
tabs: {
appearance: 'Appearance',
layout: 'Layout',
general: 'General',
preset: 'Preset'
general: 'General'
},
appearance: {
themeSchema: {
@@ -84,28 +83,7 @@ const local: App.I18n.Schema = {
followPrimary: 'Follow Primary'
},
recommendColor: 'Apply Recommended Color Algorithm',
recommendColorDesc: 'The recommended color algorithm refers to',
preset: {
title: 'Theme Presets',
apply: 'Apply',
applySuccess: 'Preset applied successfully',
default: {
name: 'Default Preset',
desc: 'Default theme preset with balanced settings'
},
dark: {
name: 'Dark Preset',
desc: 'Dark theme preset for night time usage'
},
compact: {
name: 'Compact Preset',
desc: 'Compact layout preset for small screens'
},
azir: {
name: "Azir's Preset",
desc: 'It is a cold and elegant preset that Azir likes'
}
}
recommendColorDesc: 'The recommended color algorithm refers to'
},
layout: {
layoutMode: {
@@ -131,11 +109,9 @@ const local: App.I18n.Schema = {
title: 'Tab Settings',
visible: 'Tab Visible',
cache: 'Tag Bar Info Cache',
cacheTip: 'One-click to open/close global keepalive',
height: 'Tab Height',
mode: {
title: 'Tab Mode',
slider: 'Slider',
chrome: 'Chrome',
button: 'Button'
}
@@ -168,7 +144,6 @@ const local: App.I18n.Schema = {
title: 'Content Area Settings',
scrollMode: {
title: 'Scroll Mode',
tip: 'The theme scroll only scrolls the main part, the outer scroll can carry the header and footer together',
wrapper: 'Wrapper',
content: 'Content'
},
@@ -186,6 +161,11 @@ const local: App.I18n.Schema = {
}
},
fixedHeaderAndTab: 'Fixed Header And Tab'
},
resetCacheStrategy: {
title: 'Reset Cache Strategy',
close: 'Close Page',
refresh: 'Refresh Page'
}
},
general: {

View File

@@ -62,8 +62,7 @@ const local: App.I18n.Schema = {
tabs: {
appearance: '外观',
layout: '布局',
general: '通用',
preset: '预设'
general: '通用'
},
appearance: {
themeSchema: {
@@ -84,28 +83,7 @@ const local: App.I18n.Schema = {
followPrimary: '跟随主色'
},
recommendColor: '应用推荐算法的颜色',
recommendColorDesc: '推荐颜色的算法参照',
preset: {
title: '主题预设',
apply: '应用',
applySuccess: '预设应用成功',
default: {
name: '默认预设',
desc: 'Soybean 默认主题预设'
},
dark: {
name: '暗色预设',
desc: '适用于夜间使用的暗色主题预设'
},
compact: {
name: '紧凑型',
desc: '适用于小屏幕的紧凑布局预设'
},
azir: {
name: 'Azir的预设',
desc: '是 Azir 比较喜欢的莫兰迪色系冷淡风'
}
}
recommendColorDesc: '推荐颜色的算法参照'
},
layout: {
layoutMode: {
@@ -128,11 +106,9 @@ const local: App.I18n.Schema = {
title: '标签栏设置',
visible: '显示标签栏',
cache: '标签栏信息缓存',
cacheTip: '一键开启/关闭全局 keepalive',
height: '标签栏高度',
mode: {
title: '标签栏风格',
slider: '滑块风格',
chrome: '谷歌风格',
button: '按钮风格'
}
@@ -159,13 +135,12 @@ const local: App.I18n.Schema = {
visible: '显示底部',
fixed: '固定底部',
height: '底部高度',
right: '底部右'
right: '底部右'
},
content: {
title: '内容区域设置',
scrollMode: {
title: '滚动模式',
tip: '主题滚动仅 main 部分滚动,外层滚动可携带头部底部一起滚动',
wrapper: '外层滚动',
content: '主体滚动'
},
@@ -183,6 +158,11 @@ const local: App.I18n.Schema = {
}
},
fixedHeaderAndTab: '固定头部和标签栏'
},
resetCacheStrategy: {
title: '重置缓存策略',
close: '关闭页面',
refresh: '刷新页面'
}
},
general: {

View File

@@ -19,7 +19,11 @@ export const views: Record<RouteFileKey, RawRouteComponent> = {
Home: () => import("@/views/home/index.vue"),
IframeUrl: () => import("@/views/(builtin)/iframe/[url].vue"),
Login: () => import("@/views/(builtin)/login/index.vue"),
ManageApi: () => import("@/views/manage/api/index.vue"),
ManageDictionary: () => import("@/views/manage/dictionary/index.vue"),
ManageMenu: () => import("@/views/manage/menu/index.vue"),
ManageOrganization: () => import("@/views/manage/organization/index.vue"),
ManagePermission: () => import("@/views/manage/permission/index.vue"),
ManageRole: () => import("@/views/manage/role/index.vue"),
ManageRoute: () => import("@/views/manage/route/index.vue"),
ManageUser: () => import("@/views/manage/user/index.vue"),

View File

@@ -82,6 +82,24 @@ export const routes: AutoRouterRoute[] = [
constant: true
},
},
{
name: 'ManageApi',
path: '/manage/api',
layout: 'base',
component: 'ManageApi',
meta: {
title: "ManageApi"
}
},
{
name: 'ManageDictionary',
path: '/manage/dictionary',
layout: 'base',
component: 'ManageDictionary',
meta: {
title: "ManageDictionary"
}
},
{
name: 'ManageMenu',
path: '/manage/menu',
@@ -91,6 +109,24 @@ export const routes: AutoRouterRoute[] = [
title: "ManageMenu"
},
},
{
name: 'ManageOrganization',
path: '/manage/organization',
layout: 'base',
component: 'ManageOrganization',
meta: {
title: "ManageOrganization"
}
},
{
name: 'ManagePermission',
path: '/manage/permission',
layout: 'base',
component: 'ManagePermission',
meta: {
title: "ManagePermission"
}
},
{
name: 'ManageRole',
path: '/manage/role',

View File

@@ -16,7 +16,11 @@ const routePathMap: RoutePathMap = {
"Home": "/home",
"IframeUrl": "/iframe/:url",
"Login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
"ManageApi": "/manage/api",
"ManageDictionary": "/manage/dictionary",
"ManageMenu": "/manage/menu",
"ManageOrganization": "/manage/organization",
"ManagePermission": "/manage/permission",
"ManageRole": "/manage/role",
"ManageRoute": "/manage/route",
"ManageUser": "/manage/user",

View File

@@ -1,4 +1,4 @@
import type { AutoRouterRedirect, AutoRouterRoute } from '@elegant-router/types';
import type { AutoRouterRoute } from '@elegant-router/types';
import { routes } from '../_generated/routes';
import { layouts, views } from '../_generated/imports';
import { transformToVueRoutes } from '../_generated/transformer';
@@ -8,24 +8,17 @@ export function createStaticRoutes() {
const constantRoutes: AutoRouterRoute[] = [];
const authRoutes: AutoRouterRoute[] = [];
let rootRoute: AutoRouterRedirect | undefined;
routes.forEach(item => {
if (item.meta?.constant) {
constantRoutes.push(item);
} else {
authRoutes.push(item);
}
if (item.name === 'Root') {
rootRoute = item as AutoRouterRedirect;
}
});
return {
constantRoutes,
authRoutes,
rootRoute: rootRoute as AutoRouterRedirect
authRoutes
};
}

View File

@@ -46,7 +46,10 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
});
setReloadFlag(true);
routeStore.resetRouteCache();
if (themeStore.resetCacheStrategy === 'refresh') {
routeStore.resetRouteCache();
}
}
const locale = ref<App.I18n.LangType>(localStg.get('lang') || 'zh-CN');

View File

@@ -112,8 +112,10 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
await switchRouteByTab(nextTab);
}
// reset route cache
routeStore.resetRouteCache(removedTabRouteKey);
// reset route cache if cache strategy is close
if (themeStore.resetCacheStrategy === 'close') {
routeStore.resetRouteCache(removedTabRouteKey);
}
}
/** remove active tab */
@@ -145,8 +147,10 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
const tabsToRemove = tabs.value.filter(tab => !remainTabIds.includes(tab.id));
const routeKeysToReset: RouteKey[] = [];
for (const tab of tabsToRemove) {
routeKeysToReset.push(tab.routeKey);
if (themeStore.resetCacheStrategy === 'close') {
for (const tab of tabsToRemove) {
routeKeysToReset.push(tab.routeKey);
}
}
const removedTabsIds = tabsToRemove.map(tab => tab.id);

View File

@@ -1,89 +0,0 @@
{
"name": "Azir's Preset",
"desc": "It is a cold and elegant preset that Azir likes",
"i18nkey": "theme.appearance.preset.azir",
"version": "1.0.0",
"themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": true,
"themeColor": "#78a878",
"otherColor": {
"info": "#89b989",
"success": "#99c299",
"warning": "#d4bb9d",
"error": "#c49a9a"
},
"isInfoFollowPrimary": true,
"layout": {
"mode": "vertical-mix",
"scrollMode": "wrapper"
},
"page": {
"animate": true,
"animateMode": "zoom-fade"
},
"header": {
"height": 64,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": true
},
"globalSearch": {
"visible": true
}
},
"tab": {
"visible": true,
"cache": true,
"height": 48,
"mode": "chrome"
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": false,
"width": 220,
"collapsedWidth": 64,
"mixWidth": 90,
"mixCollapsedWidth": 64,
"mixChildMenuWidth": 200
},
"footer": {
"visible": true,
"fixed": true,
"height": 56,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": true,
"timeFormat": "YYYY-MM-DD HH:mm:ss"
},
"tokens": {
"light": {
"colors": {
"container": "rgb(255, 255, 255)",
"layout": "rgb(247, 250, 252)",
"inverted": "rgb(0, 20, 40)",
"base-text": "rgb(31, 31, 31)"
},
"boxShadow": {
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
}
},
"dark": {
"colors": {
"container": "rgb(28, 28, 28)",
"layout": "rgb(18, 18, 18)",
"base-text": "rgb(224, 224, 224)"
}
}
}
}

View File

@@ -1,89 +0,0 @@
{
"name": "Compact Preset",
"desc": "Compact layout preset for small screens",
"i18nkey": "theme.appearance.preset.compact",
"version": "1.0.0",
"themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": false,
"themeColor": "#646cff",
"otherColor": {
"info": "#2080f0",
"success": "#52c41a",
"warning": "#faad14",
"error": "#f5222d"
},
"isInfoFollowPrimary": true,
"layout": {
"mode": "vertical",
"scrollMode": "content"
},
"page": {
"animate": true,
"animateMode": "fade-slide"
},
"header": {
"height": 48,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": false
},
"globalSearch": {
"visible": false
}
},
"tab": {
"visible": true,
"cache": true,
"height": 36,
"mode": "button"
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": false,
"width": 180,
"collapsedWidth": 48,
"mixWidth": 80,
"mixCollapsedWidth": 48,
"mixChildMenuWidth": 180
},
"footer": {
"visible": false,
"fixed": false,
"height": 40,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": false,
"timeFormat": "YYYY-MM-DD HH:mm"
},
"tokens": {
"light": {
"colors": {
"container": "rgb(255, 255, 255)",
"layout": "rgb(247, 250, 252)",
"inverted": "rgb(0, 20, 40)",
"base-text": "rgb(31, 31, 31)"
},
"boxShadow": {
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
}
},
"dark": {
"colors": {
"container": "rgb(28, 28, 28)",
"layout": "rgb(18, 18, 18)",
"base-text": "rgb(224, 224, 224)"
}
}
}
}

View File

@@ -1,89 +0,0 @@
{
"name": "Dark Preset",
"desc": "Dark theme preset for night time usage",
"i18nkey": "theme.appearance.preset.dark",
"version": "1.0.0",
"themeScheme": "dark",
"grayscale": false,
"colourWeakness": false,
"recommendColor": false,
"themeColor": "#409eff",
"otherColor": {
"info": "#2080f0",
"success": "#52c41a",
"warning": "#faad14",
"error": "#f5222d"
},
"isInfoFollowPrimary": true,
"layout": {
"mode": "vertical",
"scrollMode": "content"
},
"page": {
"animate": true,
"animateMode": "fade-slide"
},
"header": {
"height": 56,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": true
},
"globalSearch": {
"visible": true
}
},
"tab": {
"visible": true,
"cache": true,
"height": 44,
"mode": "chrome"
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": true,
"width": 220,
"collapsedWidth": 64,
"mixWidth": 90,
"mixCollapsedWidth": 64,
"mixChildMenuWidth": 200
},
"footer": {
"visible": true,
"fixed": false,
"height": 48,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": false,
"timeFormat": "YYYY-MM-DD HH:mm"
},
"tokens": {
"light": {
"colors": {
"container": "rgb(255, 255, 255)",
"layout": "rgb(247, 250, 252)",
"inverted": "rgb(0, 20, 40)",
"base-text": "rgb(31, 31, 31)"
},
"boxShadow": {
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
}
},
"dark": {
"colors": {
"container": "rgb(28, 28, 28)",
"layout": "rgb(18, 18, 18)",
"base-text": "rgb(224, 224, 224)"
}
}
}
}

View File

@@ -1,89 +0,0 @@
{
"name": "default",
"desc": "Default theme preset with balanced settings",
"i18nkey": "theme.appearance.preset.default",
"version": "1.0.0",
"themeScheme": "light",
"grayscale": false,
"colourWeakness": false,
"recommendColor": false,
"themeColor": "#646cff",
"otherColor": {
"info": "#2080f0",
"success": "#52c41a",
"warning": "#faad14",
"error": "#f5222d"
},
"isInfoFollowPrimary": true,
"layout": {
"mode": "vertical",
"scrollMode": "content"
},
"page": {
"animate": true,
"animateMode": "fade-slide"
},
"header": {
"height": 56,
"breadcrumb": {
"visible": true,
"showIcon": true
},
"multilingual": {
"visible": true
},
"globalSearch": {
"visible": true
}
},
"tab": {
"visible": true,
"cache": true,
"height": 44,
"mode": "chrome"
},
"fixedHeaderAndTab": true,
"sider": {
"inverted": false,
"width": 220,
"collapsedWidth": 64,
"mixWidth": 90,
"mixCollapsedWidth": 64,
"mixChildMenuWidth": 200
},
"footer": {
"visible": true,
"fixed": false,
"height": 48,
"right": true
},
"watermark": {
"visible": false,
"text": "SoybeanAdmin",
"enableUserName": false,
"enableTime": false,
"timeFormat": "YYYY-MM-DD HH:mm"
},
"tokens": {
"light": {
"colors": {
"container": "rgb(255, 255, 255)",
"layout": "rgb(247, 250, 252)",
"inverted": "rgb(0, 20, 40)",
"base-text": "rgb(31, 31, 31)"
},
"boxShadow": {
"header": "0 1px 2px rgb(0, 21, 41, 0.08)",
"sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
"tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
}
},
"dark": {
"colors": {
"container": "rgb(28, 28, 28)",
"layout": "rgb(18, 18, 18)",
"base-text": "rgb(224, 224, 224)"
}
}
}
}

View File

@@ -12,6 +12,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
error: '#f5222d'
},
isInfoFollowPrimary: true,
resetCacheStrategy: 'close',
layout: {
mode: 'vertical',
scrollMode: 'content'

View File

@@ -5,38 +5,46 @@
*/
declare namespace Api {
namespace Common {
interface CreatedUpdatedDTO {
/** 创建人 */
createdBy: string;
/** 创建时间 */
createdTime: string;
/** 更新人 */
updatedBy: string;
/** 更新时间 */
updatedTime: string;
}
interface CommonRecordDTO extends CreatedUpdatedDTO {
/** 主键 */
id: number;
/**
* 启用状态 Enable status
*
* - "1": 启用
* - "2": 禁用
*/
status: Union.EnableStatus | null;
}
interface PaginationQueryDTO {
page: number;
pageSize: number;
sort: string;
}
interface PaginationDTO extends Pick<PaginationQueryDTO, 'page' | 'pageSize'> {
/** 总条数 */
/** common params of paginating */
interface PaginatingCommonParams {
/** current page number */
current: number;
/** page size */
size: number;
/** total count */
total: number;
}
/** common params of paginating query list data */
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
records: T[];
}
/** common search params of table */
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>;
/**
* enable status
*
* - "1": enabled
* - "2": disabled
*/
type EnableStatus = '1' | '2';
/** common record */
type CommonRecord<T = any> = {
/** record id */
id: number;
/** record creator */
createBy: string;
/** record create time */
createTime: string;
/** record updater */
updateBy: string;
/** record update time */
updateTime: string;
/** record status */
status: EnableStatus | null;
} & T;
}
}

View File

@@ -1,42 +0,0 @@
declare namespace Api {
namespace Menu {
type RouteKey = import('@elegant-router/types').RouteKey;
interface Menu extends Common.CommonRecordDTO {
/** 菜单名称 */
name: string;
/** 菜单类型 */
menuType: Union.MenuType;
/** 菜单描述 */
description?: string | null;
/** 菜单图标 */
icon?: string | null;
/** 菜单图标类型 */
iconType?: string | null;
/** 父级菜单ID */
parentId?: number | null;
/** 菜单排序 */
order?: number | null;
/** 是否隐藏 */
isHidden?: boolean | null;
/** iframe 页面URL */
iframeUrl?: string | null;
/** 链接 */
href?: string | null;
/** 路由名称 */
routeName?: RouteKey | null;
/** 路由查询参数 */
routeQueries?: Record<string, string | number | boolean> | null;
/** 路由路径参数 */
routeParams?: Record<string, string | number | boolean> | null;
}
interface MenuListDTO extends Common.PaginationDTO {
list: Menu[];
}
interface MenuTreeDTO extends Menu {
children?: MenuTreeDTO[];
}
}
}

View File

@@ -1,34 +1,19 @@
declare namespace Api {
/**
* namespace Route
*
* backend api module: "route"
*/
namespace Route {
interface RouteDTO extends Common.CommonRecordDTO {
/** 路由路径 */
path: string;
/** 路由名称 */
name: string;
/** 路由布局 */
layout: string;
/** 路由组件 */
component: string;
/** 重定向路径 */
redirect?: string | null;
/** 路由图标 */
icon?: string | null;
/** 路由图标类型 */
iconType?: string | null;
/** 是否缓存 */
isCache?: boolean | null;
/** 是否支持多开 */
isMultiple?: boolean | null;
/** 固定索引 */
fixedIndex?: number | null;
/** 是否需要权限 */
requireAuth?: boolean | null;
/** 路由属性 */
props?: Record<string, string | number | boolean> | null;
type ElegantConstRoute = import('@elegant-router/types').ElegantConstRoute;
interface MenuRoute extends ElegantConstRoute {
id: string;
}
interface RouteListDTO extends Common.PaginationDTO {
list: RouteDTO[];
interface UserRoute {
routes: MenuRoute[];
home: import('@elegant-router/types').LastLevelRouteKey;
}
}
}

View File

@@ -1,31 +0,0 @@
declare namespace Api {
namespace Union {
/**
* 是否 Yes or No
*
* - "Y": yes
* - "N": no
*/
type YesOrNo = 'Y' | 'N';
/**
* 启用状态 Enable status
*
* - "1": 启用
* - "2": 禁用
*/
type EnableStatus = '1' | '2';
/**
* 菜单类型
*
* - "directory": 目录
* - "menu": 菜单
* - "iframe": iframe 页面iframe 嵌入页面)
* - "link": 链接(跳转外部链接)
* - "page": 页面(不展示在菜单中,可用于作为菜单类型的子菜单展示在面包屑中)
* - "other": 其他(作用自行定义)
*/
type MenuType = 'page' | 'directory' | 'menu' | 'iframe' | 'link' | 'other';
}
}

112
src/typings/app.d.ts vendored
View File

@@ -20,6 +20,8 @@ declare namespace App {
otherColor: OtherColor;
/** Whether info color is followed by the primary color */
isInfoFollowPrimary: boolean;
/** Reset cache strategy */
resetCacheStrategy: UnionKey.ResetCacheStrategy;
/** Layout */
layout: {
/** Layout mode */
@@ -179,9 +181,9 @@ declare namespace App {
type VNode = import('vue').VNode;
type RouteLocationNormalizedLoaded = import('vue-router').RouteLocationNormalizedLoaded;
type RouteKey = import('@elegant-router/types').RouteKey;
type RoutePathMap = import('@elegant-router/types').RoutePathMap;
type RouteMap = import('@elegant-router/types').RouteMap;
type RoutePath = import('@elegant-router/types').RoutePath;
type RouteFileKey = import('@elegant-router/types').RouteFileKey;
type LastLevelRouteKey = import('@elegant-router/types').LastLevelRouteKey;
/** The router push options */
type RouterPushOptions = {
@@ -199,25 +201,77 @@ declare namespace App {
showMenu?: boolean;
}
type MenuType = 'page' | 'directory' | 'menu' | 'iframe' | 'link' | 'button' | 'other';
/** The global menu */
interface Menu {
id: number;
name: string;
menuType: MenuType;
description?: string | null;
icon?: string | null;
iconType?: string | null;
parentId?: number | null;
order?: number | null;
isHidden?: boolean | null;
iframeUrl?: string | null;
href?: string | null;
routeName?: RouteKey | null;
routeQueries?: Record<string, string | number | boolean> | null;
routeParams?: Record<string, string | number | boolean> | null;
}
type Menu = {
/**
* The menu key
*
* Equal to the route key
*/
key: string;
/** The menu label */
label: string;
/** The menu i18n key */
i18nKey?: I18n.I18nKey | null;
/** The route key */
routeKey: RouteKey;
/** The route path */
routePath: RoutePath;
/** The menu icon */
icon?: () => VNode;
/** The menu children */
children?: Menu[];
};
type Breadcrumb = Omit<Menu, 'children'> & {
options?: Breadcrumb[];
};
/** Tab route */
type TabRoute = Pick<RouteLocationNormalizedLoaded, 'name' | 'path' | 'meta'> &
Partial<Pick<RouteLocationNormalizedLoaded, 'fullPath' | 'query' | 'matched'>>;
/** The global tab */
type Tab = {
/** The tab id */
id: string;
/** The tab label */
label: string;
/**
* The new tab label
*
* If set, the tab label will be replaced by this value
*/
newLabel?: string;
/**
* The old tab label
*
* when reset the tab label, the tab label will be replaced by this value
*/
oldLabel?: string;
/** The tab route key */
routeKey: LastLevelRouteKey;
/** The tab route path */
routePath: RouteMap[LastLevelRouteKey];
/** The tab route full path */
fullPath: string;
/** The tab fixed index */
fixedIndex?: number | null;
/**
* Tab icon
*
* Iconify icon
*/
icon?: string;
/**
* Tab local icon
*
* Local icon
*/
localIcon?: string;
/** I18n key */
i18nKey?: I18n.I18nKey | null;
};
/** Form rule */
type FormRule = import('naive-ui').FormItemRule;
@@ -313,7 +367,6 @@ declare namespace App {
appearance: string;
layout: string;
general: string;
preset: string;
};
appearance: {
themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
@@ -322,20 +375,9 @@ declare namespace App {
themeColor: {
title: string;
followPrimary: string;
} & Record<Theme.ThemeColorKey, string>;
} & Theme.ThemeColor;
recommendColor: string;
recommendColorDesc: string;
preset: {
title: string;
apply: string;
applySuccess: string;
[key: string]:
| {
name: string;
desc: string;
}
| string;
};
};
layout: {
layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string> & {
@@ -345,7 +387,6 @@ declare namespace App {
title: string;
visible: string;
cache: string;
cacheTip: string;
height: string;
mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
};
@@ -375,13 +416,14 @@ declare namespace App {
};
content: {
title: string;
scrollMode: { title: string; tip: string } & Record<UnionKey.ThemeScrollMode, string>;
scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>;
page: {
animate: string;
mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
};
fixedHeaderAndTab: string;
};
resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
};
general: {
title: string;

View File

@@ -25,13 +25,11 @@ declare module 'vue' {
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
IconUilSearch: typeof import('~icons/uil/search')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NBadge: typeof import('naive-ui')['NBadge']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
@@ -58,7 +56,6 @@ declare module 'vue' {
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopover: typeof import('naive-ui')['NPopover']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
@@ -66,7 +63,6 @@ declare module 'vue' {
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip']
NWatermark: typeof import('naive-ui')['NWatermark']

View File

@@ -31,7 +31,11 @@ declare module "@elegant-router/types" {
"Home": "/home";
"IframeUrl": "/iframe/:url";
"Login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
"ManageApi": "/manage/api";
"ManageDictionary": "/manage/dictionary";
"ManageMenu": "/manage/menu";
"ManageOrganization": "/manage/organization";
"ManagePermission": "/manage/permission";
"ManageRole": "/manage/role";
"ManageRoute": "/manage/route";
"ManageUser": "/manage/user";

View File

@@ -31,7 +31,11 @@ declare module "vue-router/auto-routes" {
"Home": RouteRecordInfo<"Home", "/home", Record<never, never>, Record<never, never>>;
"IframeUrl": RouteRecordInfo<"IframeUrl", "/iframe/:url", { url: ParamValue<true> }, { url: ParamValue<false> }>;
"Login": RouteRecordInfo<"Login", "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?", { module?: ParamValueZeroOrOne<true> }, { module?: ParamValueZeroOrOne<false> }>;
"ManageApi": RouteRecordInfo<"ManageApi", "/manage/api", Record<never, never>, Record<never, never>>;
"ManageDictionary": RouteRecordInfo<"ManageDictionary", "/manage/dictionary", Record<never, never>, Record<never, never>>;
"ManageMenu": RouteRecordInfo<"ManageMenu", "/manage/menu", Record<never, never>, Record<never, never>>;
"ManageOrganization": RouteRecordInfo<"ManageOrganization", "/manage/organization", Record<never, never>, Record<never, never>>;
"ManagePermission": RouteRecordInfo<"ManagePermission", "/manage/permission", Record<never, never>, Record<never, never>>;
"ManageRole": RouteRecordInfo<"ManageRole", "/manage/role", Record<never, never>, Record<never, never>>;
"ManageRoute": RouteRecordInfo<"ManageRoute", "/manage/route", Record<never, never>, Record<never, never>>;
"ManageUser": RouteRecordInfo<"ManageUser", "/manage/user", Record<never, never>, Record<never, never>>;

View File

@@ -14,6 +14,14 @@ declare namespace UnionKey {
/** Theme scheme */
type ThemeScheme = 'light' | 'dark' | 'auto';
/**
* Reset cache strategy
*
* - close: re-cache when close page
* - refresh: re-cache when refresh page
*/
type ResetCacheStrategy = 'close' | 'refresh';
/**
* The layout mode
*

View File

@@ -23,7 +23,7 @@ const appStore = useAppStore();
const themeStore = useThemeStore();
interface LoginModule {
label: App.I18n.I18nKey;
label: string;
component: Component;
}

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<div>ManageApi</div>
</template>

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<div>ManageDictionary</div>
</template>

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<div>ManageOrganization</div>
</template>

View File

@@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<div>ManagePermission</div>
</template>