feat(preferences): 公司品牌信息化编辑与全局联动(登录页/菜单栏/网页标题),偏好设置 tab
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace SystemCompanyConfigApi { |
| | | /** å
¬å¸åçä¿¡æ¯ */ |
| | | export interface CompanyConfig { |
| | | id?: number; |
| | | companyName?: string; |
| | | logoUrl?: string; |
| | | logoBlobId?: number; |
| | | loginTitle?: string; |
| | | loginDescription?: string; |
| | | } |
| | | } |
| | | |
| | | /** è·å¾å
¬å¸åçä¿¡æ¯ï¼å
ç»å½å
¬å¼ï¼ç»å½é¡µ / åºç¨å¯å¨æ¶ä½¿ç¨ï¼ */ |
| | | export async function getCompanyConfig() { |
| | | return requestClient.get<SystemCompanyConfigApi.CompanyConfig | null>( |
| | | '/system/company-config/get', |
| | | ); |
| | | } |
| | | |
| | | /** ä¿åå
¬å¸åçä¿¡æ¯ */ |
| | | export async function saveCompanyConfig( |
| | | data: SystemCompanyConfigApi.CompanyConfig, |
| | | ) { |
| | | return requestClient.post<number>('/system/company-config/save', data); |
| | | } |
| | |
| | | ); |
| | | } |
| | | |
| | | /** å
Œ
±æä»¶ä¸ä¼ ï¼æ°¸ä¹
ææï¼è¿åæ°¸ä¹
å
¬å¼ previewURLï¼ç¨äºç»å½é¡µçå
ç»å½å±ç¤ºçåºæ¯ï¼ */ |
| | | export function publicUploadFile(files: globalThis.File[]) { |
| | | const formData = new FormData(); |
| | | files.forEach((file) => formData.append('files', file)); |
| | | return requestClient.post<SystemStorageApi.StorageBlob[]>( |
| | | '/system/storage-blob/public-upload', |
| | | formData, |
| | | { headers: { 'Content-Type': 'multipart/form-data' } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢éä»¶å表 */ |
| | | export function listAttachments(params: SystemStorageApi.AttachmentListParams) { |
| | | return requestClient.get<SystemStorageApi.StorageBlob[]>( |
| | |
| | | import App from './app.vue'; |
| | | import { router } from './router'; |
| | | |
| | | import { getCompanyConfig } from '#/api/system/company-config'; |
| | | import { applyCompanyConfig } from '#/utils/company-config'; |
| | | |
| | | async function bootstrap(namespace: string) { |
| | | // åå§åç»ä»¶éé
å¨ |
| | | await initComponentAdapter(); |
| | |
| | | // é
ç½® pinia-store |
| | | await initStores(app, { namespace }); |
| | | |
| | | // å è½½å
¬å¸åçä¿¡æ¯ï¼ä»å端ï¼å
ç»å½æ¥å£ï¼ï¼èå¨ç»å½é¡µ/èåæ /ç½é¡µæ é¢ã |
| | | // æ³¨ï¼æ¤æ¶ pinia å°æªå¨ç»ä»¶å¤æ¿æ´»ï¼è¿éç¨çº¯å½æ°ï¼ä¸ä¾èµ storeã |
| | | try { |
| | | const companyConfig = await getCompanyConfig(); |
| | | if (companyConfig) { |
| | | applyCompanyConfig(companyConfig); |
| | | } |
| | | } catch { |
| | | // å端ä¸å¯ç¨ææªé
ç½®ï¼ä¿æç¼è¯æé»è®¤ |
| | | } |
| | | |
| | | // å®è£
æéæä»¤ |
| | | registerAccessDirective(app); |
| | | |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | import { computed, onMounted } from 'vue'; |
| | | |
| | | import { AuthPageLayout } from '@vben/layouts'; |
| | | import { preferences } from '@vben/preferences'; |
| | | |
| | | import { $t } from '#/locales'; |
| | | import { useCompanyConfigStore } from '#/store/company-config'; |
| | | |
| | | declare const __APP_LOGO__: string; |
| | | |
| | | const companyConfigStore = useCompanyConfigStore(); |
| | | |
| | | onMounted(() => { |
| | | // ç»å½é¡µå±ç¤º loginTitleï¼å¯å¨å è½½èµ°çæ¯çº¯å½æ°ï¼ä¸å¡« storeï¼ï¼è¿å
¥ç»å½é¡µæ¶æå䏿¬¡ |
| | | companyConfigStore.loadFromServer(); |
| | | }); |
| | | |
| | | const appName = computed(() => preferences.copyright.companyName); |
| | | const logo = computed(() => __APP_LOGO__ || preferences.logo.source); |
| | | const logoDark = computed(() => preferences.logo.sourceDark); |
| | | // ç»å½é¡µæ é¢/æè¿°ä¼å
åå端é
ç½®ï¼æªé
ç½®æ¶åéå° i18n é»è®¤ |
| | | const pageTitle = computed( |
| | | () => companyConfigStore.loginTitle || $t('authentication.pageTitle'), |
| | | ); |
| | | const pageDescription = computed( |
| | | () => companyConfigStore.loginDescription || $t('authentication.pageDesc'), |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | :app-name="appName" |
| | | :logo="logo" |
| | | :logo-dark="logoDark" |
| | | :page-description="$t('authentication.pageDesc')" |
| | | :page-title="$t('authentication.pageTitle')" |
| | | :page-description="pageDescription" |
| | | :page-title="pageTitle" |
| | | > |
| | | <!-- èªå®ä¹å·¥å
·æ --> |
| | | <!-- <template #toolbar></template> --> |
| | |
| | | |
| | | import { TabsTrigger } from 'reka-ui'; |
| | | |
| | | import { cn } from '../../../../../base/shared/src/utils'; |
| | | |
| | | import { Tabs, TabsContent, TabsList } from '../../ui'; |
| | | import TabsIndicator from './tabs-indicator.vue'; |
| | | |
| | | interface Props { |
| | | defaultValue?: string; |
| | | tabs?: SegmentedItem[]; |
| | | /** æ¯å¦å
è®¸æ¨ªåæ»å¨ï¼tab è¾å¤æ¶å¯ç¨ï¼å tab æå
容宽度æåï¼è¶
åºé¨å坿»å¨ */ |
| | | scrollable?: boolean; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | defaultValue: '', |
| | | tabs: () => [], |
| | | scrollable: false, |
| | | }); |
| | | |
| | | const activeTab = defineModel<string>(); |
| | |
| | | }); |
| | | |
| | | const tabsStyle = computed(() => { |
| | | if (props.scrollable) { |
| | | return undefined; |
| | | } |
| | | return { |
| | | 'grid-template-columns': `repeat(${props.tabs.length}, minmax(0, 1fr))`, |
| | | }; |
| | | }); |
| | | |
| | | const tabsIndicatorStyle = computed(() => { |
| | | if (props.scrollable) { |
| | | return undefined; |
| | | } |
| | | return { |
| | | width: `${(100 / props.tabs.length).toFixed(0)}%`, |
| | | }; |
| | |
| | | <Tabs v-model="activeTab" :default-value="getDefaultValue"> |
| | | <TabsList |
| | | :style="tabsStyle" |
| | | class="bg-accent outline-heavy! relative grid w-full outline-2!" |
| | | :class=" |
| | | cn( |
| | | 'bg-accent outline-heavy! relative outline-2!', |
| | | // 坿»å¨æ¨¡å¼ï¼å·¦å¯¹é½ï¼è¦ç reka-ui ç justify-centerï¼å¦åå
容è¶
宽æ¶å·¦ä¾§è¢«æ¤åºä¸æ æ³æ»å¨ï¼ãå
¨å®½ãæ¨ªåæ»å¨ |
| | | scrollable |
| | | ? 'flex! w-full! overflow-x-auto justify-start!' |
| | | : 'grid w-full', |
| | | ) |
| | | " |
| | | > |
| | | <TabsIndicator :style="tabsIndicatorStyle" /> |
| | | <TabsIndicator v-if="!scrollable" :style="tabsIndicatorStyle" /> |
| | | <template v-for="tab in tabs" :key="tab.value"> |
| | | <TabsTrigger |
| | | :value="tab.value" |
| | | :class="activeClass(tab.value)" |
| | | class="hover:text-primary z-20 inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50" |
| | | class="hover:text-primary z-20 inline-flex shrink-0 items-center justify-center rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50" |
| | | > |
| | | {{ tab.label }} |
| | | </TabsTrigger> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { message } from 'ant-design-vue'; |
| | | import { Button, Input, Upload } from 'ant-design-vue'; |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | // 注æï¼æ¬æä»¶ä½äº @vben ç packages å
é¨ï¼æ æ³ä½¿ç¨åºç¨å±å«å #/ï¼éç¨ç¸å¯¹è·¯å¾ï¼ä¸åç®å½ custom.vue ä¿æä¸è´ï¼ |
| | | import { publicUploadFile } from '../../../../../../../../api/system/storage'; |
| | | import { $t } from '../../../../../../../locales/src'; |
| | | import { useCompanyConfigStore } from '../../../../../../../../store/company-config'; |
| | | |
| | | defineOptions({ |
| | | name: 'PreferenceCompanyConfig', |
| | | }); |
| | | |
| | | const companyConfigStore = useCompanyConfigStore(); |
| | | |
| | | const companyName = ref(companyConfigStore.companyName); |
| | | const loginTitle = ref(companyConfigStore.loginTitle); |
| | | const loginDescription = ref(companyConfigStore.loginDescription); |
| | | const logoUrl = ref(companyConfigStore.logoUrl); |
| | | const uploading = ref(false); |
| | | |
| | | // æå¼åºåæ¶ä»å端æåææ°å
¬å¸é
置并å填表åï¼store åå¼å¯è½ä¸ºç©ºï¼å å¯å¨å è½½èµ°çæ¯çº¯å½æ°ï¼ |
| | | onMounted(async () => { |
| | | await companyConfigStore.loadFromServer(); |
| | | companyName.value = companyConfigStore.companyName; |
| | | loginTitle.value = companyConfigStore.loginTitle; |
| | | loginDescription.value = companyConfigStore.loginDescription; |
| | | logoUrl.value = companyConfigStore.logoUrl; |
| | | }); |
| | | |
| | | /** ä¸ä¼ logoï¼æ°¸ä¹
å
¬å¼ï¼ä¾ç»å½é¡µå
ç»å½å±ç¤ºï¼ */ |
| | | async function handleBeforeUpload(file: File) { |
| | | uploading.value = true; |
| | | try { |
| | | const blobs = await publicUploadFile([file]); |
| | | const blob = blobs?.[0]; |
| | | if (blob?.previewURL || blob?.url) { |
| | | logoUrl.value = blob.previewURL || blob.url || ''; |
| | | message.success($t('preferences.company.uploadSuccess')); |
| | | } else { |
| | | message.error($t('preferences.company.uploadFailed')); |
| | | } |
| | | } catch { |
| | | message.error($t('preferences.company.uploadFailed')); |
| | | } finally { |
| | | uploading.value = false; |
| | | } |
| | | // è¿å falseï¼é»æ¢ç»ä»¶é»è®¤ä¸ä¼ ï¼å·²éè¿æ¥å£ä¸ä¼ ï¼ |
| | | return false; |
| | | } |
| | | |
| | | async function handleSave() { |
| | | if (!companyName.value.trim()) { |
| | | message.warning($t('preferences.company.companyNameRequired')); |
| | | return; |
| | | } |
| | | try { |
| | | await companyConfigStore.save({ |
| | | companyName: companyName.value.trim(), |
| | | logoUrl: logoUrl.value || undefined, |
| | | loginTitle: loginTitle.value, |
| | | loginDescription: loginDescription.value, |
| | | }); |
| | | message.success($t('preferences.company.saveSuccess')); |
| | | } catch { |
| | | message.error($t('preferences.company.saveFailed')); |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="flex flex-col"> |
| | | <!-- å
¬å¸åç§° --> |
| | | <div class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"> |
| | | <span class="flex items-center text-sm"> |
| | | {{ $t('preferences.company.companyName') }} |
| | | </span> |
| | | <Input |
| | | v-model:value="companyName" |
| | | :placeholder="$t('preferences.company.companyNamePlaceholder')" |
| | | class="w-41.25" |
| | | allow-clear |
| | | /> |
| | | </div> |
| | | |
| | | <!-- ç»å½é¡µæ é¢ --> |
| | | <div class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"> |
| | | <span class="flex items-center text-sm"> |
| | | {{ $t('preferences.company.loginTitle') }} |
| | | </span> |
| | | <Input |
| | | v-model:value="loginTitle" |
| | | :placeholder="$t('preferences.company.loginTitlePlaceholder')" |
| | | class="w-41.25" |
| | | allow-clear |
| | | /> |
| | | </div> |
| | | |
| | | <!-- ç»å½é¡µæè¿° --> |
| | | <div class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"> |
| | | <span class="flex items-center text-sm"> |
| | | {{ $t('preferences.company.loginDescription') }} |
| | | </span> |
| | | <Input |
| | | v-model:value="loginDescription" |
| | | :placeholder="$t('preferences.company.loginDescriptionPlaceholder')" |
| | | class="w-41.25" |
| | | allow-clear |
| | | /> |
| | | </div> |
| | | |
| | | <!-- å
¬å¸ Logo --> |
| | | <div class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1"> |
| | | <span class="flex items-center text-sm"> |
| | | {{ $t('preferences.company.logo') }} |
| | | </span> |
| | | <div class="flex items-center gap-2"> |
| | | <Upload |
| | | :show-upload-list="false" |
| | | accept="image/*" |
| | | :before-upload="handleBeforeUpload" |
| | | > |
| | | <Button size="small" :loading="uploading"> |
| | | {{ $t('preferences.company.uploadLogo') }} |
| | | </Button> |
| | | </Upload> |
| | | <img |
| | | v-if="logoUrl" |
| | | :src="logoUrl" |
| | | alt="company-logo" |
| | | class="h-9 w-9 rounded border border-border object-contain" |
| | | /> |
| | | <span v-else class="text-xs text-muted-foreground"> |
| | | {{ $t('preferences.company.logoEmpty') }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="uploading" class="px-2 pb-1 text-xs text-muted-foreground"> |
| | | {{ $t('preferences.company.uploading') }} |
| | | </div> |
| | | |
| | | <!-- ä¿å --> |
| | | <div class="mt-3 px-2"> |
| | | <Button type="primary" class="w-full" @click="handleSave"> |
| | | {{ $t('preferences.company.save') }} |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | export { default as Block } from './block.vue'; |
| | | export { default as Company } from './company/company.vue'; |
| | | export { default as Custom } from './custom/custom.vue'; |
| | | export { default as Animation } from './general/animation.vue'; |
| | | export { default as General } from './general/general.vue'; |
| | |
| | | Breadcrumb, |
| | | BuiltinTheme, |
| | | ColorMode, |
| | | Company, |
| | | Content, |
| | | Copyright, |
| | | Custom, |
| | |
| | | label: $t('preferences.general'), |
| | | value: 'general', |
| | | }, |
| | | { |
| | | label: $t('preferences.company.title'), |
| | | value: 'company', |
| | | }, |
| | | ]; |
| | | |
| | | if (showCustomTab.value) { |
| | |
| | | <VbenSegmented |
| | | v-model="activeTab" |
| | | :tabs="tabs" |
| | | scrollable |
| | | :class="{ |
| | | 'sticky-tabs-header': appEnableStickyPreferencesNavigationBar, |
| | | }" |
| | |
| | | /> |
| | | </Block> |
| | | </template> |
| | | <template #company> |
| | | <Block :title="$t('preferences.company.title')"> |
| | | <Company /> |
| | | </Block> |
| | | </template> |
| | | <template #custom> |
| | | <Block :title="customTabTitle"> |
| | | <Custom |
| | |
| | | "placeholder": "Please enter the default report title" |
| | | } |
| | | } |
| | | }, |
| | | "company": { |
| | | "title": "Company Info", |
| | | "companyName": "Company Name", |
| | | "companyNamePlaceholder": "Please enter company name", |
| | | "companyNameRequired": "Company name is required", |
| | | "loginTitle": "Login Page Title", |
| | | "loginTitlePlaceholder": "Enter login page title (optional)", |
| | | "loginDescription": "Login Page Description", |
| | | "loginDescriptionPlaceholder": "Enter login page description (optional)", |
| | | "logo": "Company Logo", |
| | | "uploadLogo": "Upload Logo", |
| | | "uploadSuccess": "Logo uploaded successfully", |
| | | "uploadFailed": "Logo upload failed", |
| | | "uploading": "Uploading...", |
| | | "logoEmpty": "No logo set", |
| | | "save": "Save", |
| | | "saveSuccess": "Company info saved successfully", |
| | | "saveFailed": "Failed to save company info" |
| | | } |
| | | } |
| | |
| | | "placeholder": "请è¾å
¥æ¥è¡¨é»è®¤æ é¢" |
| | | } |
| | | } |
| | | }, |
| | | "company": { |
| | | "title": "å
¬å¸ä¿¡æ¯", |
| | | "companyName": "å
¬å¸åç§°", |
| | | "companyNamePlaceholder": "请è¾å
¥å
¬å¸åç§°", |
| | | "companyNameRequired": "å
¬å¸åç§°ä¸è½ä¸ºç©º", |
| | | "loginTitle": "ç»å½é¡µæ é¢", |
| | | "loginTitlePlaceholder": "请è¾å
¥ç»å½é¡µæ é¢ï¼å¯éï¼", |
| | | "loginDescription": "ç»å½é¡µæè¿°", |
| | | "loginDescriptionPlaceholder": "请è¾å
¥ç»å½é¡µæè¿°ï¼å¯éï¼", |
| | | "logo": "å
¬å¸ Logo", |
| | | "uploadLogo": "ä¸ä¼ Logo", |
| | | "uploadSuccess": "Logo ä¸ä¼ æå", |
| | | "uploadFailed": "Logo ä¸ä¼ 失败", |
| | | "uploading": "ä¸ä¼ ä¸...", |
| | | "logoEmpty": "æªè®¾ç½® Logo", |
| | | "save": "ä¿å", |
| | | "saveSuccess": "å
¬å¸ä¿¡æ¯ä¿åæå", |
| | | "saveFailed": "å
¬å¸ä¿¡æ¯ä¿å失败" |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { ref } from 'vue'; |
| | | import { defineStore } from 'pinia'; |
| | | |
| | | import { |
| | | getCompanyConfig, |
| | | saveCompanyConfig as saveCompanyConfigApi, |
| | | } from '#/api/system/company-config'; |
| | | import { applyCompanyConfig } from '#/utils/company-config'; |
| | | |
| | | /** |
| | | * å
¬å¸åçä¿¡æ¯å
¨å±é
ç½® |
| | | * |
| | | * å¯å¨æ¶ä»å端å è½½ï¼ä¿åæ¶ååå端ï¼å¹¶å°å
¬å¸å/logo åæ¥å° preferencesï¼ |
| | | * ä»èèå¨ç»å½é¡µãèåæ ãç½é¡µæ é¢çæææ¶è´¹ç¹ã |
| | | */ |
| | | export const useCompanyConfigStore = defineStore('companyConfig', () => { |
| | | const companyName = ref(''); |
| | | const logoUrl = ref(''); |
| | | const loginTitle = ref(''); |
| | | const loginDescription = ref(''); |
| | | |
| | | /** å°å
¬å¸ä¿¡æ¯åºç¨å° preferencesï¼èå¨ç»å½é¡µ/èåæ /æ é¢ */ |
| | | function applyToPreferences() { |
| | | applyCompanyConfig({ |
| | | companyName: companyName.value, |
| | | logoUrl: logoUrl.value, |
| | | }); |
| | | } |
| | | |
| | | /** ä»å端å è½½å
¬å¸é
置并åºç¨ */ |
| | | async function loadFromServer() { |
| | | try { |
| | | const config = await getCompanyConfig(); |
| | | if (config) { |
| | | companyName.value = config.companyName ?? ''; |
| | | logoUrl.value = config.logoUrl ?? ''; |
| | | loginTitle.value = config.loginTitle ?? ''; |
| | | loginDescription.value = config.loginDescription ?? ''; |
| | | applyToPreferences(); |
| | | } |
| | | } catch { |
| | | // å端ä¸å¯ç¨ææªé
ç½®ï¼ä¿æç¼è¯æé»è®¤ |
| | | } |
| | | } |
| | | |
| | | /** ä¿åå
¬å¸é
ç½®å°å端并åºç¨ */ |
| | | async function save(data: { |
| | | companyName: string; |
| | | logoUrl?: string; |
| | | logoBlobId?: number; |
| | | loginTitle?: string; |
| | | loginDescription?: string; |
| | | }) { |
| | | await saveCompanyConfigApi(data); |
| | | companyName.value = data.companyName; |
| | | logoUrl.value = data.logoUrl ?? ''; |
| | | loginTitle.value = data.loginTitle ?? ''; |
| | | loginDescription.value = data.loginDescription ?? ''; |
| | | applyToPreferences(); |
| | | } |
| | | |
| | | return { |
| | | companyName, |
| | | logoUrl, |
| | | loginTitle, |
| | | loginDescription, |
| | | applyToPreferences, |
| | | loadFromServer, |
| | | save, |
| | | }; |
| | | }); |
| | |
| | | export * from './auth'; |
| | | export * from './company-config'; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { preferencesManager } from '@vben/preferences'; |
| | | |
| | | /** å
¬å¸åçä¿¡æ¯ï¼ä¸å端 system_company_config è¡¨åæ®µå¯¹åºï¼ */ |
| | | export interface CompanyConfigData { |
| | | companyName?: string; |
| | | logoUrl?: string; |
| | | loginTitle?: string; |
| | | loginDescription?: string; |
| | | } |
| | | |
| | | /** |
| | | * å°å
¬å¸åçä¿¡æ¯åºç¨å°å
¨å±ï¼ |
| | | * - å
¬å¸å â preferences.app.name / copyright.companyNameï¼èå¨èåæ ãç»å½é¡µãç½é¡µæ é¢ãçæï¼ |
| | | * - logo â preferences.logo.sourceï¼èå¨ç»å½é¡µãä¾§è¾¹æ logoï¼ |
| | | * - æå
¬å¸åæ¶ï¼è®¾ç½® document.titleï¼åºç¡æ é¢ï¼dynamicTitle å¼å¯æ¶è·¯ç±æ é¢å¨æ¤åºç¡ä¸æ¼æ¥ï¼ |
| | | */ |
| | | export function applyCompanyConfig(config: CompanyConfigData) { |
| | | const updates: Record<string, any> = {}; |
| | | if (config.logoUrl) { |
| | | updates.logo = { source: config.logoUrl }; |
| | | } |
| | | if (config.companyName) { |
| | | updates.app = { name: config.companyName }; |
| | | updates.copyright = { companyName: config.companyName }; |
| | | } |
| | | if (Object.keys(updates).length > 0) { |
| | | preferencesManager.updatePreferences(updates); |
| | | } |
| | | if (config.companyName) { |
| | | document.title = config.companyName; |
| | | } |
| | | } |