Merge branch 'main' into example

This commit is contained in:
Soybean
2025-06-09 22:49:41 +08:00
61 changed files with 2594 additions and 2382 deletions

View File

@@ -210,6 +210,10 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
// render chart
await render();
if (chart) {
await onUpdated?.(chart);
}
}
scope.run(() => {

View File

@@ -38,7 +38,7 @@ const { isFullscreen, toggle } = useFullscreen();
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
</div>
<div class="h-full flex-y-center justify-end">
<GlobalSearch />
<GlobalSearch v-if="themeStore.header.globalSearch.visible" />
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
<LangSwitch
v-if="themeStore.header.multilingual.visible"

View File

@@ -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() {

View File

@@ -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>

View File

@@ -112,6 +112,9 @@ const local: App.I18n.Schema = {
},
multilingual: {
visible: 'Display multilingual button'
},
globalSearch: {
visible: 'Display GlobalSearch button'
}
},
tab: {

View File

@@ -112,6 +112,9 @@ const local: App.I18n.Schema = {
},
multilingual: {
visible: '显示多语言按钮'
},
globalSearch: {
visible: '显示全局搜索按钮'
}
},
tab: {

View File

@@ -25,8 +25,8 @@ export function setupAppVersionNotification() {
const buildTime = await getHtmlBuildTime();
// If build time hasn't changed, no update is needed
if (buildTime === BUILD_TIME) {
// If failed to get build time or build time hasn't changed, no update is needed.
if (!buildTime || buildTime === BUILD_TIME) {
return;
}
@@ -88,16 +88,22 @@ export function setupAppVersionNotification() {
}
}
async function getHtmlBuildTime() {
async function getHtmlBuildTime(): Promise<string | null> {
const baseUrl = import.meta.env.VITE_BASE_URL || '/';
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
try {
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
const html = await res.text();
if (!res.ok) {
console.error('getHtmlBuildTime error:', res.status, res.statusText);
return null;
}
const match = html.match(/<meta name="buildTime" content="(.*)">/);
const buildTime = match?.[1] || '';
return buildTime;
const html = await res.text();
const match = html.match(/<meta name="buildTime" content="(.*)">/);
return match?.[1] || null;
} catch (error) {
console.error('getHtmlBuildTime error:', error);
return null;
}
}

View File

@@ -1,4 +1,4 @@
import { addAPIProvider, disableCache } from '@iconify/vue';
import { addAPIProvider } from '@iconify/vue';
/** Setup the iconify offline */
export function setupIconifyOffline() {
@@ -6,7 +6,5 @@ export function setupIconifyOffline() {
if (VITE_ICONIFY_URL) {
addAPIProvider('', { resources: [VITE_ICONIFY_URL] });
disableCache('all');
}
}

View File

@@ -2,8 +2,8 @@ import { createAlovaRequest } from '@sa/alova';
import { createAlovaMockAdapter } from '@sa/alova/mock';
import adapterFetch from '@sa/alova/fetch';
import { useAuthStore } from '@/store/modules/auth';
import { $t } from '@/locales';
import { getServiceBaseURL } from '@/utils/service';
import { $t } from '@/locales';
import featureUsers20241014 from '../mocks/feature-users-20241014';
import { getAuthorization, handleRefreshToken, showErrorMsg } from './shared';
import type { RequestInstanceState } from './type';

View File

@@ -40,6 +40,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
/** Reset auth store */
async function resetStore() {
recordUserId();
clearAuthStorage();
authStore.$reset();
@@ -52,6 +54,41 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
routeStore.resetStore();
}
/** Record the user ID of the previous login session Used to compare with the current user ID on next login */
function recordUserId() {
if (!userInfo.userId) {
return;
}
// Store current user ID locally for next login comparison
localStg.set('lastLoginUserId', userInfo.userId);
}
/**
* Check if current login user is different from previous login user If different, clear all tabs
*
* @returns {boolean} Whether to clear all tabs
*/
function checkTabClear(): boolean {
if (!userInfo.userId) {
return false;
}
const lastLoginUserId = localStg.get('lastLoginUserId');
// Clear all tabs if current user is different from previous user
if (!lastLoginUserId || lastLoginUserId !== userInfo.userId) {
localStg.remove('globalTabs');
tabStore.clearTabs();
localStg.remove('lastLoginUserId');
return true;
}
localStg.remove('lastLoginUserId');
return false;
}
/**
* Login
*
@@ -68,7 +105,15 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const pass = await loginByToken(loginToken);
if (pass) {
await redirectFromLogin(redirect);
// Check if the tab needs to be cleared
const isClear = checkTabClear();
let needRedirect = redirect;
if (isClear) {
// If the tab needs to be cleared,it means we don't need to redirect.
needRedirect = false;
}
await redirectFromLogin(needRedirect);
window.$notification?.success({
title: $t('page.login.common.loginSuccess'),

View File

@@ -98,13 +98,24 @@ 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;
// if remove the last tab, then switch to the second last tab
const nextTab = tabs.value[removeTabIndex + 1] || tabs.value[removeTabIndex - 1] || homeTab.value;
// remove tab
tabs.value.splice(removeTabIndex, 1);
// 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 +142,26 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
*/
async function clearTabs(excludes: string[] = []) {
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);
function update() {
@@ -142,13 +170,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();

View File

@@ -30,6 +30,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
},
multilingual: {
visible: true
},
globalSearch: {
visible: true
}
},
tab: {

View File

@@ -58,6 +58,10 @@ declare namespace App {
/** Whether to show the multilingual */
visible: boolean;
};
globalSearch: {
/** Whether to show the GlobalSearch */
visible: boolean;
};
};
/** Tab */
tab: {
@@ -377,6 +381,9 @@ declare namespace App {
multilingual: {
visible: string;
};
globalSearch: {
visible: string;
};
};
tab: {
visible: string;

View File

@@ -14,7 +14,7 @@ declare namespace CommonType {
* @property value: The option value
* @property label: The option label
*/
type Option<K = string> = { value: K; label: string };
type Option<K = string, M = string> = { value: K; label: M };
type YesOrNo = 'Y' | 'N';

View File

@@ -37,5 +37,7 @@ declare namespace StorageType {
layout: UnionKey.ThemeLayoutMode;
siderCollapse: boolean;
};
/** The last login user id */
lastLoginUserId: string;
}
}

View File

@@ -108,6 +108,8 @@ declare namespace Env {
readonly VITE_AUTOMATICALLY_DETECT_UPDATE?: CommonType.YesOrNo;
/** show proxy url log in terminal */
readonly VITE_PROXY_LOG?: CommonType.YesOrNo;
/** The launch editor */
readonly VITE_DEVTOOLS_LAUNCH_EDITOR?: import('vite-plugin-vue-devtools').VitePluginVueDevToolsOptions['launchEditor'];
}
}

View File

@@ -22,7 +22,7 @@ export function transformRecordToOption<T extends Record<string, string>>(record
return Object.entries(record).map(([value, label]) => ({
value,
label
})) as CommonType.Option<keyof T>[];
})) as CommonType.Option<keyof T, T[keyof T]>[];
}
/**
@@ -30,10 +30,10 @@ export function transformRecordToOption<T extends Record<string, string>>(record
*
* @param options
*/
export function translateOptions(options: CommonType.Option<string>[]) {
export function translateOptions(options: CommonType.Option<string, App.I18n.I18nKey>[]) {
return options.map(option => ({
...option,
label: $t(option.label as App.I18n.I18nKey)
label: $t(option.label)
}));
}

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { $t } from '@/locales';
import pkg from '~/package.json';
const appStore = useAppStore();

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { actionDelegationMiddleware, useAutoRequest } from '@sa/alova/client';
import { ref } from 'vue';
import { actionDelegationMiddleware, useAutoRequest } from '@sa/alova/client';
import { alova } from '@/service-alova/request';
const getLastTime = alova.Get<{ time: string }>('/mock/getLastTime', { cacheFor: null });

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue';
import { actionDelegationMiddleware, useCaptcha, useForm } from '@sa/alova/client';
import { $t } from '@/locales';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
import { sendCaptcha, verifyCaptcha } from '@/service-alova/api';
defineOptions({

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { actionDelegationMiddleware, useAutoRequest } from '@sa/alova/client';
import { ref } from 'vue';
import { actionDelegationMiddleware, useAutoRequest } from '@sa/alova/client';
import { alova } from '@/service-alova/request';
const getLastTime = alova.Get<{ time: string }>('/mock/getLastTime', { cacheFor: null });

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { actionDelegationMiddleware, useAutoRequest } from '@sa/alova/client';
import { ref } from 'vue';
import { actionDelegationMiddleware, useAutoRequest } from '@sa/alova/client';
import { alova } from '@/service-alova/request';
const getLastTime = alova.Get<{ time: string }>('/mock/getLastTime', { cacheFor: null });

View File

@@ -1,6 +1,6 @@
import type { TableColumnCheck } from '@sa/hooks';
import { computed, ref } from 'vue';
import type { DataTableBaseColumn, DataTableColumn } from 'naive-ui';
import type { TableColumnCheck } from '@sa/hooks';
import { $t } from '@/locales';
import type { AlovaGenerics, Method } from '~/packages/alova/src';

View File

@@ -1,6 +1,6 @@
import { useBoolean } from '@sa/hooks';
import type { Ref } from 'vue';
import { ref } from 'vue';
import { useBoolean } from '@sa/hooks';
import { jsonClone } from '@sa/utils';
import { $t } from '@/locales';

View File

@@ -1,11 +1,11 @@
<script setup lang="tsx">
import { reactive } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { usePagination } from '@sa/alova/client';
import { reactive } from 'vue';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { useAppStore } from '@/store/modules/app';
import { batchDeleteUser, deleteUser, fetchGetUserList } from '@/service-alova/api';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import useCheckedColumns from './hooks/use-checked-columns';
import useTableOperate from './hooks/use-table-operate';
import UserOperateDrawer from './modules/user-operate-drawer.vue';

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { computed, watch } from 'vue';
import { useForm, useWatcher } from '@sa/alova/client';
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import type { UserModel } from '@/service-alova/api';
import { addUser, fetchGetAllRoles, updateUser } from '@/service-alova/api';
import { $t } from '@/locales';
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
defineOptions({
name: 'UserOperateDrawer'

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@/locales';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
defineOptions({
name: 'UserSearch'

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { $t } from '@/locales';
import { fetchCustomBackendError } from '@/service/api';
import { $t } from '@/locales';
async function logout() {
await fetchCustomBackendError('8888', $t('request.logoutMsg'));

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useTabStore } from '@/store/modules/tab';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import { useTabStore } from '@/store/modules/tab';
const tabStore = useTabStore();
const { routerPushByKey } = useRouterPush();

View File

@@ -3,12 +3,12 @@ import { ref } from 'vue';
import type { Ref } from 'vue';
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { useBoolean } from '@sa/hooks';
import { yesOrNoRecord } from '@/constants/common';
import { enableStatusRecord, menuTypeRecord } from '@/constants/business';
import { fetchGetAllPages, fetchGetMenuList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales';
import { yesOrNoRecord } from '@/constants/common';
import { enableStatusRecord, menuTypeRecord } from '@/constants/business';
import SvgIcon from '@/components/custom/svg-icon.vue';
import MenuOperateModal, { type OperateType } from './modules/menu-operate-modal.vue';

View File

@@ -1,12 +1,12 @@
<script setup lang="tsx">
import { computed, ref, watch } from 'vue';
import type { SelectOption } from 'naive-ui';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
import { enableStatusOptions, menuIconTypeOptions, menuTypeOptions } from '@/constants/business';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { getLocalIcons } from '@/utils/icon';
import { fetchGetAllRoles } from '@/service/api';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { getLocalIcons } from '@/utils/icon';
import { $t } from '@/locales';
import SvgIcon from '@/components/custom/svg-icon.vue';
import {
getLayoutAndPage,
getPathParamFromRoutePath,

View File

@@ -1,10 +1,10 @@
<script setup lang="tsx">
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { enableStatusRecord } from '@/constants/business';
import { fetchGetRoleList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales';
import { enableStatusRecord } from '@/constants/business';
import RoleOperateDrawer from './modules/role-operate-drawer.vue';
import RoleSearch from './modules/role-search.vue';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, shallowRef, watch } from 'vue';
import { $t } from '@/locales';
import { fetchGetAllPages, fetchGetMenuTree } from '@/service/api';
import { $t } from '@/locales';
defineOptions({
name: 'MenuAuthModal'

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useBoolean } from '@sa/hooks';
import { enableStatusOptions } from '@/constants/business';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
import { enableStatusOptions } from '@/constants/business';
import MenuAuthModal from './menu-auth-modal.vue';
import ButtonAuthModal from './button-auth-modal.vue';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { $t } from '@/locales';
import { enableStatusOptions } from '@/constants/business';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
defineOptions({
name: 'RoleSearch'

View File

@@ -1,10 +1,10 @@
<script setup lang="tsx">
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { fetchGetUserList } from '@/service/api';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { fetchGetUserList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales';
import UserOperateDrawer from './modules/user-operate-drawer.vue';
import UserSearch from './modules/user-search.vue';

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { fetchGetAllRoles } from '@/service/api';
import { $t } from '@/locales';
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
import { fetchGetAllRoles } from '@/service/api';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'UserOperateDrawer'

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@/locales';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { enableStatusOptions, userGenderOptions } from '@/constants/business';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { translateOptions } from '@/utils/common';
import { $t } from '@/locales';
defineOptions({
name: 'UserSearch'

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import JsBarcode from 'jsbarcode';
import type { Options } from 'jsbarcode';
import { onMounted } from 'vue';
const text = 'Soybean';

View File

@@ -1,10 +1,10 @@
<script setup lang="tsx">
import { NButton, NTag } from 'naive-ui';
import { utils, writeFile } from 'xlsx';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { fetchGetUserList } from '@/service/api';
import { useAppStore } from '@/store/modules/app';
import { useTable } from '@/hooks/common/table';
import { fetchGetUserList } from '@/service/api';
import { enableStatusRecord, userGenderRecord } from '@/constants/business';
import { $t } from '@/locales';
const appStore = useAppStore();