feat(系统设置): 参数设置-基础设置&界面设置、部分组件调整、布局&登录增加预览模式

This commit is contained in:
baiqi 2023-08-02 14:18:24 +08:00 committed by fit2-zhao
parent e596e26051
commit 3e759804dc
33 changed files with 1050 additions and 132 deletions

View File

@ -7,7 +7,6 @@ import configArcoStyleImportPlugin from './plugin/arcoStyleImport';
import configArcoResolverPlugin from './plugin/arcoResolver'; import configArcoResolverPlugin from './plugin/arcoResolver';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import vueSetupExtend from 'vite-plugin-vue-setup-extend'; import vueSetupExtend from 'vite-plugin-vue-setup-extend';
import monacoEditorPlugin from 'vite-plugin-monaco-editor';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@ -23,7 +22,6 @@ export default defineConfig({
// 指定symbolId格式 // 指定symbolId格式
symbolId: 'icon-[name]', symbolId: 'icon-[name]',
}), }),
monacoEditorPlugin({}),
], ],
resolve: { resolve: {
alias: [ alias: [

View File

@ -36,10 +36,10 @@
"dependencies": { "dependencies": {
"@7polo/kity": "2.0.8", "@7polo/kity": "2.0.8",
"@7polo/kityminder-core": "1.4.53", "@7polo/kityminder-core": "1.4.53",
"@arco-design/web-vue": "^2.48.0", "@arco-design/web-vue": "^2.49.2",
"@arco-themes/vue-ms-theme-default": "^0.0.21", "@arco-themes/vue-ms-theme-default": "^0.0.24",
"@form-create/arco-design": "^3.1.21", "@form-create/arco-design": "^3.1.21",
"@vueuse/core": "^9.13.0", "@vueuse/core": "^10.2.1",
"ace-builds": "^1.22.0", "ace-builds": "^1.22.0",
"axios": "^0.24.0", "axios": "^0.24.0",
"dayjs": "^1.11.8", "dayjs": "^1.11.8",
@ -60,6 +60,7 @@
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-router": "^4.2.2", "vue-router": "^4.2.2",
"vue3-ace-editor": "^2.2.2", "vue3-ace-editor": "^2.2.2",
"vue3-colorpicker": "^2.1.6",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -13,6 +13,9 @@
import useLocale from '@/locale/useLocale'; import useLocale from '@/locale/useLocale';
import { saveBaseInfo, getBaseInfo } from '@/api/modules/setting/config'; import { saveBaseInfo, getBaseInfo } from '@/api/modules/setting/config';
import { getLocalStorage, setLocalStorage } from '@/utils/local-storage'; import { getLocalStorage, setLocalStorage } from '@/utils/local-storage';
import useAppStore from '@/store/modules/app';
const appStore = useAppStore();
const { currentLocale } = useLocale(); const { currentLocale } = useLocale();
const locale = computed(() => { const locale = computed(() => {
@ -26,9 +29,11 @@
} }
}); });
// url url
onBeforeMount(async () => { onBeforeMount(async () => {
try { try {
appStore.initSystemversion(); //
appStore.initPageConfig(); //
// url url
const isInitUrl = getLocalStorage('isInitUrl'); // url const isInitUrl = getLocalStorage('isInitUrl'); // url
if (isInitUrl === 'true') return; if (isInitUrl === 'true') return;
const res = await getBaseInfo(); const res = await getBaseInfo();

View File

@ -81,16 +81,22 @@ export class MSAxios {
/** /**
* @description: * @description:
*/ */
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams): Promise<T> { uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams, customFileKey = ''): Promise<T> {
const formData = new window.FormData(); const formData = new window.FormData();
const fileName = params.fileList.length === 1 ? 'file' : 'files'; const fileName = params.fileList.length === 1 ? 'file' : 'files';
params.fileList.forEach((file: File) => { if (customFileKey !== '') {
formData.append(fileName, file); params.fileList.forEach((file: File) => {
}); formData.append(customFileKey, file);
});
} else {
params.fileList.forEach((file: File) => {
formData.append(fileName, file);
});
}
if (params.request) { if (params.request) {
const requestData = JSON.stringify(params.request); const requestData = JSON.stringify(params.request);
formData.append('request', requestData); formData.append('request', new Blob([requestData], { type: ContentTypeEnum.JSON }));
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.axiosInstance this.axiosInstance

View File

@ -5,9 +5,18 @@ import {
SaveEmailInfoUrl, SaveEmailInfoUrl,
GetBaseInfoUrl, GetBaseInfoUrl,
GetEmailInfoUrl, GetEmailInfoUrl,
SavePageConfigUrl,
GetPageConfigUrl,
} from '@/api/requrls/setting/config'; } from '@/api/requrls/setting/config';
import type { SaveInfoParams, TestEmailParams, EmailConfig, BaseConfig } from '@/models/setting/config'; import type {
SaveInfoParams,
TestEmailParams,
EmailConfig,
BaseConfig,
SavePageConfigParams,
PageConfigReturns,
} from '@/models/setting/config';
// 测试邮箱连接 // 测试邮箱连接
export function testEmail(data: TestEmailParams) { export function testEmail(data: TestEmailParams) {
@ -33,3 +42,13 @@ export function saveEmailInfo(data: SaveInfoParams) {
export function getEmailInfo() { export function getEmailInfo() {
return MSR.get<EmailConfig>({ url: GetEmailInfoUrl }); return MSR.get<EmailConfig>({ url: GetEmailInfoUrl });
} }
// 保存界面配置
export function savePageConfig(data: SavePageConfigParams) {
return MSR.uploadFile({ url: SavePageConfigUrl }, data, 'files');
}
// 获取界面配置
export function getPageConfig() {
return MSR.get<PageConfigReturns>({ url: GetPageConfigUrl });
}

View File

@ -0,0 +1,10 @@
// 系统全局类的接口
import MSR from '@/api/http/index';
import { GetVersionUrl } from '@/api/requrls/system';
// 获取系统版本
export function getSystemVersion() {
return MSR.get<string>({ url: GetVersionUrl });
}
export default { getSystemVersion };

View File

@ -3,3 +3,5 @@ export const SaveBaseInfoUrl = '/system/parameter/save/base-info';
export const SaveEmailInfoUrl = '/system/parameter/edit/email-info'; export const SaveEmailInfoUrl = '/system/parameter/edit/email-info';
export const GetEmailInfoUrl = '/system/parameter/get/email-info'; export const GetEmailInfoUrl = '/system/parameter/get/email-info';
export const GetBaseInfoUrl = '/system/parameter/get/base-info'; export const GetBaseInfoUrl = '/system/parameter/get/base-info';
export const SavePageConfigUrl = '/display/save';
export const GetPageConfigUrl = '/display/info';

View File

@ -0,0 +1,5 @@
// 系统全局类的接口
export const GetVersionUrl = '/system/version/current';
export default { GetVersionUrl };

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -31,6 +31,9 @@
.arco-tabs-nav-add-btn { .arco-tabs-nav-add-btn {
font-size: var(--font-size-body-3); font-size: var(--font-size-body-3);
} }
.arco-tabs-tab {
padding: 13px 0 !important;
}
/** Modal对话框 **/ /** Modal对话框 **/
.arco-modal { .arco-modal {

View File

@ -78,3 +78,6 @@ body {
font-size: 12px; font-size: 12px;
color: rgb(var(--color-text-4)); color: rgb(var(--color-text-4));
} }
.one-line-text {
@apply overflow-hidden overflow-ellipsis whitespace-nowrap;
}

View File

@ -11,7 +11,7 @@
> >
<div v-if="!props.simple" class="card-header"> <div v-if="!props.simple" class="card-header">
<div v-if="!props.hideBack" class="back-btn" @click="back"><icon-arrow-left /></div> <div v-if="!props.hideBack" class="back-btn" @click="back"><icon-arrow-left /></div>
<div class="text-[var(--color-text-000)]">{{ props.title }}</div> <div class="font-medium text-[var(--color-text-000)]">{{ props.title }}</div>
</div> </div>
<a-divider v-if="!props.simple" class="mb-[16px]" /> <a-divider v-if="!props.simple" class="mb-[16px]" />
<div class="ms-card-container"> <div class="ms-card-container">

View File

@ -0,0 +1,33 @@
<template>
<ColorPicker v-model:pureColor="innerPureColor" :z-index="1" is-widget picker-type="chrome" round-history />
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ColorPicker } from 'vue3-colorpicker';
import 'vue3-colorpicker/style.css';
const props = defineProps<{
pureColor: string;
}>();
const emit = defineEmits(['update:pureColor']);
const innerPureColor = ref(props.pureColor || '#CF00FF');
watch(
() => props.pureColor,
(val) => {
innerPureColor.value = val;
}
);
watch(
() => innerPureColor.value,
(val) => {
emit('update:pureColor', val);
}
);
</script>
<style lang="less"></style>

View File

@ -8,31 +8,33 @@
@before-upload="beforeUpload" @before-upload="beforeUpload"
> >
<template #upload-button> <template #upload-button>
<div class="ms-upload-area"> <slot>
<div class="ms-upload-icon-box"> <div class="ms-upload-area">
<MsIcon v-if="fileList.length > 0" :type="IconMap[props.accept]" class="ms-upload-icon" /> <div class="ms-upload-icon-box">
<div v-else class="ms-upload-icon ms-upload-icon--default"></div> <MsIcon v-if="fileList.length > 0" :type="IconMap[props.accept]" class="ms-upload-icon" />
<div v-else class="ms-upload-icon ms-upload-icon--default"></div>
</div>
<template v-if="fileList.length === 0">
<div class="ms-upload-main-text">
{{ t(props.mainText || 'ms.upload.importModalDragtext') }}
</div>
<div class="ms-upload-sub-text">
{{
t(props.subText || 'ms.upload.importModalFileTip', {
type: UploadAcceptEnum[props.accept],
size: props.maxSize || defaultMaxSize,
})
}}
</div>
</template>
<template v-else>
<div class="ms-upload-main-text">
{{ fileList[0]?.name }}
</div>
<div class="ms-upload-sub-text">{{ formatFileSize(fileList[0]?.file?.size || 0) }}</div>
</template>
</div> </div>
<template v-if="fileList.length === 0"> </slot>
<div class="ms-upload-main-text">
{{ t(props.mainText || 'ms.upload.importModalDragtext') }}
</div>
<div class="ms-upload-sub-text">
{{
t(props.subText || 'ms.upload.importModalFileTip', {
type: UploadAcceptEnum[props.accept],
size: props.maxSize || defaultMaxSize,
})
}}
</div>
</template>
<template v-else>
<div class="ms-upload-main-text">
{{ fileList[0]?.name }}
</div>
<div class="ms-upload-sub-text">{{ formatFileSize(fileList[0]?.file?.size || 0) }}</div>
</template>
</div>
</template> </template>
</a-upload> </a-upload>
</template> </template>
@ -60,7 +62,7 @@
disabled: boolean; disabled: boolean;
iconType: string; iconType: string;
maxSize: number; // MB maxSize: number; // MB
[key: string]: any; sizeUnit: 'MB' | 'KB'; //
}> & { }> & {
accept: UploadType; accept: UploadType;
fileList: FileItem[]; fileList: FileItem[];
@ -107,7 +109,8 @@
fileList.value = []; fileList.value = [];
} }
const maxSize = props.maxSize || defaultMaxSize; const maxSize = props.maxSize || defaultMaxSize;
if (file.size > maxSize * 1024 * 1024) { const _maxSize = props.sizeUnit === 'MB' ? maxSize * 1024 * 1024 : maxSize * 1024;
if (file.size > _maxSize) {
Message.warning(t('ms.upload.overSize')); Message.warning(t('ms.upload.overSize'));
return Promise.resolve(false); return Promise.resolve(false);
} }

View File

@ -2,10 +2,16 @@
<div class="navbar"> <div class="navbar">
<div class="left-side"> <div class="left-side">
<a-space> <a-space>
<svg-icon width="145px" height="32px" name="MS-full-logo" /> <template v-if="props.logo">
<div class="flex max-w-[145px] items-center overflow-hidden">
<img :src="props.logo" class="mr-[4px] h-[32px] w-[32px]" />
{{ props.name }}
</div>
</template>
<svg-icon v-else width="145px" height="32px" name="MS-full-logo" />
</a-space> </a-space>
</div> </div>
<div class="center-side"> <div v-if="!props.isPreview" class="center-side">
<template v-if="showProjectSelect"> <template v-if="showProjectSelect">
<a-divider direction="vertical" class="ml-0" /> <a-divider direction="vertical" class="ml-0" />
<a-select <a-select
@ -29,7 +35,7 @@
</template> </template>
<TopMenu /> <TopMenu />
</div> </div>
<ul class="right-side"> <ul v-if="!props.isPreview" class="right-side">
<li> <li>
<a-tooltip :content="t('settings.navbar.search')"> <a-tooltip :content="t('settings.navbar.search')">
<a-button type="secondary"> <a-button type="secondary">
@ -190,6 +196,12 @@
import type { ProjectListItem } from '@/models/setting/project'; import type { ProjectListItem } from '@/models/setting/project';
const props = defineProps<{
isPreview?: boolean;
logo?: string;
name?: string;
}>();
const appStore = useAppStore(); const appStore = useAppStore();
// const { logout } = useUser(); // const { logout } = useUser();
const route = useRoute(); const route = useRoute();

View File

@ -8,7 +8,7 @@ export enum UploadAcceptEnum {
csv = '.csv', csv = '.csv',
zip = '.zip', zip = '.zip',
xmind = '.xmind', xmind = '.xmind',
image = '.jpg,.png', image = '.jpg,.jpeg,.png,.svg',
jar = '.jar', jar = '.jar',
} }

View File

@ -1,12 +1,12 @@
<template> <template>
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }"> <a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
<div v-if="navbar" class="layout-navbar z-[100]"> <div v-if="navbar" class="layout-navbar z-[100]">
<NavBar /> <NavBar :is-preview="innerProps.isPreview" :logo="innerLogo" :name="innerName" />
</div> </div>
<a-layout> <a-layout>
<a-layout> <a-layout>
<a-layout-sider <a-layout-sider
v-if="renderMenu" v-if="renderMenu && !innerProps.isPreview"
v-show="!hideMenu" v-show="!hideMenu"
class="layout-sider z-[99]" class="layout-sider z-[99]"
breakpoint="xl" breakpoint="xl"
@ -43,7 +43,8 @@
> >
<MsBreadCrumb /> <MsBreadCrumb />
<a-layout-content> <a-layout-content>
<PageLayout /> <PageLayout v-if="!props.isPreview" />
<slot></slot>
</a-layout-content> </a-layout-content>
<Footer v-if="footer" /> <Footer v-if="footer" />
</a-scrollbar> </a-scrollbar>
@ -65,12 +66,39 @@
import PageLayout from './page-layout.vue'; import PageLayout from './page-layout.vue';
import MsBreadCrumb from '@/components/bussiness/ms-breadcrumb/index.vue'; import MsBreadCrumb from '@/components/bussiness/ms-breadcrumb/index.vue';
interface Props {
isPreview?: boolean;
logo?: string;
name?: string;
}
const props = defineProps<Props>();
const innerProps = ref<Props>(props);
watch(
() => props.logo,
() => {
innerProps.value = { ...props };
}
);
watch(
() => props.name,
() => {
innerProps.value = { ...props };
}
);
const isInit = ref(false); const isInit = ref(false);
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const permission = usePermission(); const permission = usePermission();
const innerLogo = computed(() => innerProps.value.logo || appStore.pageConfig.logoPlatform[0]?.url);
const innerName = computed(() => innerProps.value.name || appStore.pageConfig.platformName);
const navbarHeight = `56px`; const navbarHeight = `56px`;
const navbar = computed(() => appStore.navbar); const navbar = computed(() => appStore.navbar);
const renderMenu = computed(() => appStore.menu); const renderMenu = computed(() => appStore.menu);

View File

@ -1,3 +1,6 @@
import { Recordable } from '#/global';
import { FileItem } from '@arco-design/web-vue';
// 基础信息配置 // 基础信息配置
export interface BaseConfig { export interface BaseConfig {
url: string; url: string;
@ -36,3 +39,47 @@ export interface TestEmailParams {
'smtp.tsl': string; 'smtp.tsl': string;
'smtp.recipient': string; 'smtp.recipient': string;
} }
// 界面配置入参
export interface SavePageConfigParams {
fileList: (File | undefined)[];
request: Recordable[];
}
interface FileParamItem extends ParamItem {
file: string;
fileName: string;
}
// 页面配置返回参数
export type PageConfigReturns = FileParamItem[];
// 主题配置对象
export interface ThemeConfig {
style: string;
customStyle: string;
theme: string;
customTheme: string;
}
// 登录页配置对象
export interface LoginConfig {
title: string;
icon: (FileItem | never)[];
loginLogo: (FileItem | never)[];
loginImage: (FileItem | never)[];
slogan: string;
}
// 平台配置对象
export interface PlatformConfig {
logoPlatform: (FileItem | never)[];
platformName: string;
helpDoc: string;
}
// 界面配置对象
export interface PageConfig extends ThemeConfig, LoginConfig, PlatformConfig {}
export type PageConfigKeys = keyof PageConfig;

View File

@ -23,7 +23,6 @@ export interface TestResourceDTO {
concurrentNumber: number; // k8s 最大并发数 concurrentNumber: number; // k8s 最大并发数
podThreads: number; // k8s 单pod最大线程数 podThreads: number; // k8s 单pod最大线程数
jobDefinition: string; // k8s job自定义模板 jobDefinition: string; // k8s job自定义模板
apiTestImage: string; // k8s api测试镜像
deployName: string; // k8s api测试部署名称 deployName: string; // k8s api测试部署名称
uiGrid: string; // ui测试selenium-grid uiGrid: string; // ui测试selenium-grid
girdConcurrentNumber: number; // ui测试selenium-grid最大并发数 girdConcurrentNumber: number; // ui测试selenium-grid最大并发数

View File

@ -2,13 +2,36 @@ import { defineStore } from 'pinia';
import { Notification } from '@arco-design/web-vue'; import { Notification } from '@arco-design/web-vue';
import defaultSettings from '@/config/settings.json'; import defaultSettings from '@/config/settings.json';
import { getMenuList } from '@/api/modules/user'; import { getMenuList } from '@/api/modules/user';
import { getSystemVersion } from '@/api/modules/system';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { getPageConfig } from '@/api/modules/setting/config';
import { setFavicon } from '@/utils';
import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface'; import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface';
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router'; import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
import type { AppState } from './types'; import type { AppState } from './types';
import type { BreadcrumbItem } from '@/components/bussiness/ms-breadcrumb/types'; import type { BreadcrumbItem } from '@/components/bussiness/ms-breadcrumb/types';
import type { PageConfig, PageConfigKeys } from '@/models/setting/config';
const defaultThemeConfig = {
style: 'default',
customStyle: '',
theme: 'default',
customTheme: '',
};
const defaultLoginConfig = {
title: 'MeterSphere',
icon: [],
loginLogo: [],
loginImage: [],
slogan: '一站式开源持续测试平台',
};
const defaultPlatformConfig = {
logoPlatform: [],
platformName: 'MeterSphere',
helpDoc: '',
};
const useAppStore = defineStore('app', { const useAppStore = defineStore('app', {
state: (): AppState => ({ state: (): AppState => ({
@ -20,6 +43,15 @@ const useAppStore = defineStore('app', {
breadcrumbList: [] as BreadcrumbItem[], breadcrumbList: [] as BreadcrumbItem[],
currentOrgId: '', currentOrgId: '',
currentProjectId: '', currentProjectId: '',
version: '',
defaultThemeConfig,
defaultLoginConfig,
defaultPlatformConfig,
pageConfig: {
...defaultThemeConfig,
...defaultLoginConfig,
...defaultPlatformConfig,
},
}), }),
getters: { getters: {
@ -32,9 +64,6 @@ const useAppStore = defineStore('app', {
appAsyncMenus(state: AppState): RouteRecordNormalized[] { appAsyncMenus(state: AppState): RouteRecordNormalized[] {
return state.serverMenu as unknown as RouteRecordNormalized[]; return state.serverMenu as unknown as RouteRecordNormalized[];
}, },
getCustomTheme(state: AppState): string {
return state.customTheme as string;
},
getLoadingStatus(state: AppState): boolean { getLoadingStatus(state: AppState): boolean {
return state.loading; return state.loading;
}, },
@ -53,6 +82,13 @@ const useAppStore = defineStore('app', {
getCurrentProjectId(state: AppState): string { getCurrentProjectId(state: AppState): string {
return state.currentProjectId; return state.currentProjectId;
}, },
getDefaulPageConfig(state: AppState): PageConfig {
return {
...state.defaultThemeConfig,
...state.defaultLoginConfig,
...state.defaultPlatformConfig,
};
},
}, },
actions: { actions: {
@ -158,9 +194,59 @@ const useAppStore = defineStore('app', {
setCurrentProjectId(id: string) { setCurrentProjectId(id: string) {
this.currentProjectId = id; this.currentProjectId = id;
}, },
/**
*
*/
async initSystemversion() {
try {
this.version = await getSystemVersion();
} catch (error) {
console.log(error);
}
},
/**
*
*/
async initPageConfig() {
try {
const res = await getPageConfig();
if (Array.isArray(res) && res.length > 0) {
res.forEach((e) => {
const key = e.paramKey.split('ui.')[1] as PageConfigKeys; // 参数名前缀ui.去掉
if (['icon', 'loginLogo', 'loginImage', 'logoPlatform'].includes(key)) {
// 四个属性值为文件类型,单独处理
this.pageConfig[key] = [
{
url: e.paramValue,
name: e.fileName,
},
] as any;
} else {
this.pageConfig[key] = e.paramValue as any;
}
});
if (this.pageConfig.theme !== 'default') {
// 判断是否选择了自定义主题色
this.pageConfig.customTheme = this.pageConfig.theme;
this.pageConfig.theme = 'custom';
}
if (!['default', 'follow'].includes(this.pageConfig.style)) {
// 判断是否选择了自定义平台风格
this.pageConfig.customStyle = this.pageConfig.style;
this.pageConfig.style = 'custom';
}
if (this.pageConfig.icon[0]?.url) {
// 设置网站 favicon
setFavicon(this.pageConfig.icon[0].url);
}
}
} catch (error) {
console.log(error);
}
},
}, },
persist: { persist: {
paths: ['currentOrgId', 'currentProjectId'], paths: ['currentOrgId', 'currentProjectId', 'pageConfig'],
}, },
}); });

View File

@ -1,15 +1,14 @@
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router'; import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
import type { BreadcrumbItem } from '@/components/bussiness/ms-breadcrumb/types'; import type { BreadcrumbItem } from '@/components/bussiness/ms-breadcrumb/types';
import type { PageConfig, ThemeConfig, LoginConfig, PlatformConfig } from '@/models/setting/config';
export interface AppState { export interface AppState {
theme: string;
colorWeak: boolean; colorWeak: boolean;
navbar: boolean; navbar: boolean;
menu: boolean; menu: boolean;
hideMenu: boolean; hideMenu: boolean;
menuCollapse: boolean; menuCollapse: boolean;
footer: boolean; footer: boolean;
themeColor: string;
menuWidth: number; menuWidth: number;
globalSettings: boolean; globalSettings: boolean;
device: string; device: string;
@ -27,7 +26,11 @@ export interface AppState {
showTotal: boolean; showTotal: boolean;
showJumper: boolean; showJumper: boolean;
hideOnSinglePage: boolean; hideOnSinglePage: boolean;
[key: string]: unknown; version: string;
defaultThemeConfig: ThemeConfig;
defaultLoginConfig: LoginConfig;
defaultPlatformConfig: PlatformConfig;
pageConfig: PageConfig;
} }
export type CustomTheme = 'theme-default' | 'theme-green'; export type CustomTheme = 'theme-default' | 'theme-green';

View File

@ -4,7 +4,7 @@ export interface ScrollToViewOptions {
inline?: 'start' | 'center' | 'end' | 'nearest'; inline?: 'start' | 'center' | 'end' | 'nearest';
} }
export function scrollIntoView(targetRef: HTMLElement | null, options: ScrollToViewOptions = {}) { export function scrollIntoView(targetRef: HTMLElement | Element | null, options: ScrollToViewOptions = {}) {
const scrollOptions: ScrollToViewOptions = { const scrollOptions: ScrollToViewOptions = {
behavior: options.behavior || 'smooth', behavior: options.behavior || 'smooth',
block: options.block || 'start', block: options.block || 'start',

View File

@ -133,3 +133,21 @@ export function desensitize(str: string): string {
return str.replace(/./g, '*'); return str.replace(/./g, '*');
} }
// 动态设置 favicon
export function setFavicon(url: string) {
const head = document.querySelector('head');
const link = document.createElement('link');
link.rel = 'shortcut icon';
link.href = url;
link.type = 'image/x-icon';
// 移除之前的 favicon
const oldFavicon = document.querySelector('link[rel="shortcut icon"]');
if (oldFavicon) {
head?.removeChild(oldFavicon);
}
// 添加新的 favicon
head?.appendChild(link);
}

View File

@ -1,19 +1,32 @@
<template> <template>
<div class="banner-wrap"> <div class="banner-wrap">
<img class="img w-567px m-auto block" :src="bannerImage" /> <img class="img" :style="props.isPreview ? 'height: 100%;' : 'height: 100vh'" :src="innerBanner" />
</div> </div>
</template> </template>
<script setup> <script lang="ts" setup>
import bannerImage from '@/assets/images/login-banner.png'; import { computed } from 'vue';
import useAppStore from '@/store/modules/app';
import defaultBanner from '@/assets/images/login-banner.jpg';
const props = defineProps<{
isPreview?: boolean;
banner?: string;
}>();
const appStore = useAppStore();
const innerBanner = computed(() => {
return props.banner || appStore.pageConfig.loginImage[0]?.url || defaultBanner;
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.banner-wrap { .banner-wrap {
padding-top: 160px; width: 55%;
height: 760px;
.img { .img {
height: 365px; width: 100%;
object-fit: cover;
} }
} }
</style> </style>

View File

@ -1,39 +1,35 @@
<template> <template>
<div class="login-form flex flex-col items-center"> <div class="login-form" :style="props.isPreview ? 'height: inherit' : 'height: 100vh'">
<div class="title"> <div class="title">
<div class="mt-40 flex justify-center"> <div class="flex justify-center">
<svg-icon width="290px" height="60px" name="login-logo" /> <img v-if="innerLogo" :src="innerLogo" class="h-[60px] w-[290px]" />
<svg-icon v-else width="290px" height="60px" name="login-logo" />
</div> </div>
<div class="title-0 flex justify-center"> <div class="title-0 mt-[16px] flex justify-center">
<span class="title-welcome">{{ $t('login.form.title') }}</span> <span class="title-welcome">{{ innerSlogan || $t('login.form.title') }}</span>
</div> </div>
</div> </div>
<div class="form mt-20"> <div class="form mt-[32px]">
<a-form ref="formRef" :model="userInfo" @submit="handleSubmit"> <a-form ref="formRef" :model="userInfo" @submit="handleSubmit">
<a-form-item field="radio" hide-label> <a-form-item class="login-form-item" field="radio" hide-label>
<a-radio-group v-model="userInfo.authenticate"> <a-radio-group v-model="userInfo.authenticate" type="button">
<a-radio value="LDAP">LDAP</a-radio>
<a-radio value="LOCAL">普通登陆</a-radio> <a-radio value="LOCAL">普通登陆</a-radio>
<a-radio value="LDAP">LDAP</a-radio>
<a-radio value="OAuth2">OAuth2 测试</a-radio>
<a-radio value="OIDC 90">OIDC 90</a-radio> <a-radio value="OIDC 90">OIDC 90</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
class="login-form-item"
field="username" field="username"
:rules="[{ required: true, message: $t('login.form.userName.errMsg') }]" :rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
:validate-trigger="['change', 'blur']" :validate-trigger="['change', 'blur']"
hide-label hide-label
> >
<a-input <a-input v-model="userInfo.username" :placeholder="$t('login.form.userName.placeholder')" />
v-model="userInfo.username"
:placeholder="$t('login.form.userName.placeholder')"
style="border-radius: 1.5rem"
>
<template #prefix>
<icon-user />
</template>
</a-input>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
class="login-form-item"
field="password" field="password"
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]" :rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
:validate-trigger="['change', 'blur']" :validate-trigger="['change', 'blur']"
@ -43,40 +39,52 @@
v-model="userInfo.password" v-model="userInfo.password"
:placeholder="$t('login.form.password.placeholder')" :placeholder="$t('login.form.password.placeholder')"
allow-clear allow-clear
style="border-radius: 1.5rem" />
>
<template #prefix>
<icon-lock />
</template>
</a-input-password>
</a-form-item> </a-form-item>
<div class="mt-4"> <div class="mt-[12px]">
<a-button style="border-radius: 1.5rem" type="primary" html-type="submit" long :loading="loading"> <a-button type="primary" html-type="submit" long :loading="loading">
{{ $t('login.form.login') }} {{ $t('login.form.login') }}
</a-button> </a-button>
</div> </div>
</a-form> </a-form>
<div v-if="props.isPreview" class="mask"></div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive } from 'vue'; import { ref, reactive, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { ValidatedError } from '@arco-design/web-vue/es/form/interface'; import { ValidatedError } from '@arco-design/web-vue/es/form/interface';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useStorage } from '@vueuse/core'; import { useStorage } from '@vueuse/core';
import { useUserStore } from '@/store'; import { useUserStore, useAppStore } from '@/store';
import useLoading from '@/hooks/useLoading'; import useLoading from '@/hooks/useLoading';
import type { LoginData } from '@/models/user'; import type { LoginData } from '@/models/user';
import { setLoginExpires } from '@/utils/auth'; import { setLoginExpires } from '@/utils/auth';
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const userStore = useUserStore();
const appStore = useAppStore();
const props = defineProps<{
isPreview?: boolean;
slogan?: string;
logo?: string;
}>();
const innerLogo = computed(() => {
return props.logo || appStore.pageConfig.loginLogo[0]?.url;
});
const innerSlogan = computed(() => {
return props.slogan || appStore.pageConfig.slogan;
});
const errorMessage = ref(''); const errorMessage = ref('');
const { loading, setLoading } = useLoading(); const { loading, setLoading } = useLoading();
const userStore = useUserStore();
const loginConfig = useStorage('login-config', { const loginConfig = useStorage('login-config', {
rememberPassword: true, rememberPassword: true,
@ -127,12 +135,31 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
/* stylelint-disable color-function-notation */
.login-form { .login-form {
@apply flex flex-1 flex-col items-center justify-center;
background: linear-gradient(
26.72deg,
rgba(var(--primary-5), 0.02) 0%,
rgba(var(--primary-5), 0.1) 51.67%,
var(--color-text-fff) 100%
);
.title-welcome { .title-welcome {
color: #783887; color: rgb(var(--primary-5));
} }
.form { .form {
width: 443px; @apply relative bg-white;
padding: 40px;
border-radius: var(--border-radius-large);
box-shadow: 0 8px 10px 0 #3232330d, 0 16px 24px 0 #3232330d, 0 6px 30px 0 #3232330d;
.login-form-item {
margin-bottom: 28px;
}
.mask {
@apply absolute left-0 top-0 h-full w-full;
}
} }
} }
</style> </style>

View File

@ -1,25 +1,29 @@
<template> <template>
<div class="my-container h-[100vh] w-[100vw] min-w-[1440px]"> <a-scrollbar
<a-row class="flex h-[730px] flex-row"> :style="{
<a-col :span="11"> <loginForm /></a-col> overflow: 'auto',
<a-divider direction="vertical" /> width: props.isPreview ? '100%' : '100vw',
<a-col :span="11"> <banner /></a-col> height: props.isPreview ? '100%' : '100vh',
</a-row> }"
</div> >
<div class="login-page" :style="props.isPreview ? '' : 'min-width: 1200px;'">
<banner />
<loginForm :is-preview="props.isPreview" />
</div>
</a-scrollbar>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import loginForm from './components/login-form.vue'; import loginForm from './components/login-form.vue';
import banner from './components/banner.vue'; import banner from './components/banner.vue';
const props = defineProps<{
isPreview?: boolean;
}>();
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.my-container { .login-page {
.login-box { @apply flex items-center;
margin-top: calc(50vh - 400px);
}
.arco-divider-vertical {
height: 52em;
}
} }
</style> </style>

View File

@ -0,0 +1,566 @@
<template>
<div class="relative">
<!-- 风格主题色配置 -->
<MsCard class="mb-[16px]" :loading="pageloading" simple auto-height>
<div class="config-title">
{{ t('system.config.page.style') }}
<a-tooltip :content="t('system.config.page.styleTip')" position="tl" mini>
<icon-question-circle class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-6))]" />
</a-tooltip>
</div>
<a-radio-group v-model:model-value="pageConfig.style" type="button" class="mb-[4px]">
<a-radio v-for="item of styleList" :key="item.value" :value="item.value">
{{ item.label }}
</a-radio>
</a-radio-group>
<div v-if="pageConfig.style === 'custom'" class="ml-[4px]">
<MsColorSelect v-model:pure-color="pageConfig.customStyle" />
</div>
<div class="config-title mt-[16px]">
{{ t('system.config.page.theme') }}
<a-tooltip :content="t('system.config.page.themeTip')" position="tl" mini>
<icon-question-circle class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-6))]" />
</a-tooltip>
</div>
<a-radio-group v-model:model-value="pageConfig.theme" type="button" class="mb-[4px]">
<a-radio v-for="item of themeList" :key="item.value" :value="item.value">
{{ item.label }}
</a-radio>
</a-radio-group>
<div v-if="pageConfig.theme === 'custom'" class="mb-[4px] ml-[4px]">
<MsColorSelect v-model:pure-color="pageConfig.customTheme" />
</div>
</MsCard>
<!-- 登录页配置 -->
<MsCard class="mb-[16px]" :loading="pageloading" simple auto-height>
<div class="config-title">
{{ t('system.config.page.loginPageConfig') }}
</div>
<div class="config-content">
<div class="config-title !mb-[8px] flex items-center justify-between">
{{ t('system.config.page.pagePreview') }}
<MsButton class="!leading-none" @click="resetLoginPageConfig">{{ t('system.config.page.reset') }}</MsButton>
</div>
<!-- 登录页预览盒子 -->
<div class="config-preview">
<div ref="loginPageFullRef" class="login-preview">
<div :class="['config-preview-head', isLoginPageFullscreen ? 'full-preview-head' : '']">
<div class="flex items-center justify-between">
<img v-if="pageConfig.icon[0]?.url" :src="pageConfig.icon[0].url" class="h-[18px] w-[18px]" />
<svg-icon v-else name="logo" class="h-[18px] w-[18px]"></svg-icon>
<div class="ml-[4px] text-[10px]">{{ pageConfig.title }}</div>
</div>
<div
class="w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
@click="loginFullscreenToggle"
>
<MsIcon v-if="isLoginPageFullscreen" type="icon-icon_off_screen" />
<MsIcon v-else type="icon-icon_full_screen_one" />
</div>
</div>
<!-- 登录页预览实际渲染 DOM按三种屏幕尺寸缩放 -->
<div :class="['page-preview', isLoginPageFullscreen ? 'full-preview' : 'normal-preview']">
<banner :banner="pageConfig.loginImage[0]?.url" is-preview />
<loginForm :slogan="pageConfig.slogan" :logo="pageConfig.loginLogo[0]?.url" is-preview />
</div>
</div>
<div class="config-form">
<div class="config-form-card">
<div class="mb-[8px] flex items-center justify-between">
<div class="flex items-center">
{{ t('system.config.page.icon') }}
<a-tag
v-show="pageConfig.icon[0]?.file"
type="warn"
color="rgb(var(--warning-2))"
class="ml-[4px] !text-[rgb(var(--warning-6))]"
size="small"
>
{{ t('system.config.page.unsave') }}
</a-tag>
</div>
<MsUpload
v-model:file-list="pageConfig.icon"
accept="image"
:max-size="200"
size-unit="KB"
:auto-upload="false"
>
<a-button type="outline" class="arco-btn-outline--secondary" size="mini">
{{ t('system.config.page.replace') }}
</a-button>
</MsUpload>
</div>
<p class="text-[12px] text-[var(--color-text-4)]">{{ t('system.config.page.iconTip') }}</p>
</div>
<div class="config-form-card">
<div class="mb-[8px] flex items-center justify-between">
<div class="flex items-center">
{{ t('system.config.page.loginLogo') }}
<a-tag
v-show="pageConfig.loginLogo[0]?.file"
type="warn"
color="rgb(var(--warning-2))"
class="ml-[4px] !text-[rgb(var(--warning-6))]"
size="small"
>
{{ t('system.config.page.unsave') }}
</a-tag>
</div>
<MsUpload
v-model:file-list="pageConfig.loginLogo"
accept="image"
:max-size="200"
size-unit="KB"
:auto-upload="false"
>
<a-button type="outline" class="arco-btn-outline--secondary" size="mini">
{{ t('system.config.page.replace') }}
</a-button>
</MsUpload>
</div>
<p class="text-[12px] text-[var(--color-text-4)]">{{ t('system.config.page.loginLogoTip') }}</p>
</div>
<div class="config-form-card">
<div class="mb-[8px] flex items-center justify-between">
<div class="flex items-center">
{{ t('system.config.page.loginBg') }}
<a-tag
v-show="pageConfig.loginImage[0]?.file"
type="warn"
color="rgb(var(--warning-2))"
class="ml-[4px] !text-[rgb(var(--warning-6))]"
size="small"
>
{{ t('system.config.page.unsave') }}
</a-tag>
</div>
<MsUpload
v-model:file-list="pageConfig.loginImage"
accept="image"
:max-size="1"
size-unit="MB"
:auto-upload="false"
>
<a-button type="outline" class="arco-btn-outline--secondary" size="mini">
{{ t('system.config.page.replace') }}
</a-button>
</MsUpload>
</div>
<p class="text-[12px] text-[var(--color-text-4)]">{{ t('system.config.page.loginBgTip') }}</p>
</div>
<a-form
ref="loginConfigFormRef"
:model="pageConfig"
layout="vertical"
:rules="{ slogan: [{ required: true, message: t('system.config.page.sloganRquired') }] }"
>
<a-form-item
:label="t('system.config.page.slogan')"
field="slogan"
asterisk-position="end"
class="mb-[12px]"
required
>
<a-input
v-model:model-value="pageConfig.slogan"
:placeholder="t('system.config.page.sloganPlaceholder')"
:max-length="250"
></a-input>
<MsFormItemSub :text="t('system.config.page.sloganTip')" :show-fill-icon="false" />
</a-form-item>
<a-form-item :label="t('system.config.page.title')" field="title">
<a-input
v-model:model-value="pageConfig.title"
:placeholder="t('system.config.page.titlePlaceholder')"
:max-length="250"
></a-input>
<MsFormItemSub :text="t('system.config.page.titleTip')" :show-fill-icon="false" />
</a-form-item>
</a-form>
</div>
</div>
<div class="mt-[8px] text-[var(--color-text-4)]">{{ t('system.config.page.loginPreviewTip') }}</div>
</div>
</MsCard>
<!-- 平台主页面配置 -->
<MsCard class="mb-[96px]" :loading="pageloading" simple auto-height>
<div class="config-title">
{{ t('system.config.page.platformConfig') }}
</div>
<div class="config-content border border-solid border-[var(--color-text-n8)] !bg-white">
<div class="config-title !mb-[8px] flex items-center justify-between">
{{ t('system.config.page.pagePreview') }}
<MsButton class="!leading-none" @click="resetPlatformConfig">{{ t('system.config.page.reset') }}</MsButton>
</div>
<!-- 平台主页预览盒子 -->
<div class="config-preview !h-[290px]">
<div ref="loginPageFullRef" class="login-preview">
<div
class="absolute right-[18px] top-[16px] z-10 w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
@click="loginFullscreenToggle"
>
<MsIcon v-if="isLoginPageFullscreen" type="icon-icon_off_screen" />
<MsIcon v-else type="icon-icon_full_screen_one" />
</div>
<!-- 平台主页预览实际渲染 DOM按三种屏幕尺寸缩放 -->
<div
:class="[
'page-preview',
'platform-preview',
'!h-[550px]',
isLoginPageFullscreen ? 'full-preview' : 'normal-preview',
]"
>
<defaultLayout
:logo="pageConfig.logoPlatform[0]?.url"
:name="pageConfig.platformName"
class="overflow-hidden"
is-preview
>
<div class="absolute w-full bg-white" style="height: calc(100% - 28px)"></div>
</defaultLayout>
</div>
</div>
<div class="config-form">
<div class="config-form-card">
<div class="mb-[8px] flex items-center justify-between">
<div class="flex items-center">
{{ t('system.config.page.platformLogo') }}
<a-tag
v-show="pageConfig.logoPlatform[0]?.file"
type="warn"
color="rgb(var(--warning-2))"
class="ml-[4px] !text-[rgb(var(--warning-6))]"
size="small"
>
{{ t('system.config.page.unsave') }}
</a-tag>
</div>
<MsUpload
v-model:file-list="pageConfig.logoPlatform"
accept="image"
:max-size="1"
size-unit="MB"
:auto-upload="false"
>
<a-button type="outline" class="arco-btn-outline--secondary" size="mini">
{{ t('system.config.page.replace') }}
</a-button>
</MsUpload>
</div>
<p class="text-[12px] text-[var(--color-text-4)]">{{ t('system.config.page.platformLogoTip') }}</p>
</div>
<a-form
ref="platformConfigFormRef"
:model="pageConfig"
layout="vertical"
:rules="{ platformName: [{ required: true, message: t('system.config.page.platformNameRquired') }] }"
>
<a-form-item
:label="t('system.config.page.platformName')"
field="platformName"
asterisk-position="end"
class="mb-[12px]"
required
>
<a-input
v-model:model-value="pageConfig.platformName"
:placeholder="t('system.config.page.platformNamePlaceholder')"
:max-length="250"
></a-input>
<MsFormItemSub :text="t('system.config.page.platformNameTip')" :show-fill-icon="false" />
</a-form-item>
<a-form-item :label="t('system.config.page.helpDoc')" field="helpDoc" class="mb-[12px]">
<a-input
v-model:model-value="pageConfig.helpDoc"
:placeholder="t('system.config.page.helpDocPlaceholder')"
:max-length="250"
></a-input>
<MsFormItemSub :text="t('system.config.page.helpDocTip')" :show-fill-icon="false" />
</a-form-item>
</a-form>
</div>
</div>
<div class="mt-[8px] text-[var(--color-text-4)]">{{ t('system.config.page.platformConfigTip') }}</div>
</div>
</MsCard>
<div
class="fixed bottom-0 right-[16px] z-[999] flex justify-between bg-white p-[24px] shadow-[0_-1px_4px_rgba(2,2,2,0.1)]"
:style="{ width: `calc(100% - ${menuWidth + 16}px)` }"
>
<a-button type="secondary" @click="resetAll">{{ t('system.config.page.resetAll') }}</a-button>
<a-button type="primary" @click="beforeSave">
{{ t('system.config.page.save') }}
</a-button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useFullscreen } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsColorSelect from '@/components/pure/ms-color-select/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import loginForm from '@/views/login/components/login-form.vue';
import banner from '@/views/login/components/banner.vue';
import MsFormItemSub from '@/components/bussiness/ms-form-item-sub/index.vue';
import useAppStore from '@/store/modules/app';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import defaultLayout from '@/layout/default-layout.vue';
import { scrollIntoView } from '@/utils/dom';
import { savePageConfig } from '@/api/modules/setting/config';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
const { t } = useI18n();
const appStore = useAppStore();
const collapsedWidth = 86;
const menuWidth = computed(() => {
return appStore.menuCollapse ? collapsedWidth : appStore.menuWidth;
});
const pageloading = ref(false);
const pageConfig = ref({ ...appStore.pageConfig });
const loginPageFullRef = ref<HTMLElement | null>(null);
const { isFullscreen: isLoginPageFullscreen, toggle: loginFullscreenToggle } = useFullscreen(loginPageFullRef);
const loginConfigFormRef = ref<FormInstance>();
const platformConfigFormRef = ref<FormInstance>();
const styleList = [
{
label: t('system.config.page.default'),
value: 'default',
},
{
label: t('system.config.page.follow'),
value: 'follow',
},
{
label: t('system.config.page.custom'),
value: 'custom',
},
];
const themeList = [
{
label: t('system.config.page.default'),
value: 'default',
},
{
label: t('system.config.page.custom'),
value: 'custom',
},
];
function resetLoginPageConfig() {
pageConfig.value = {
...pageConfig.value,
...appStore.defaultLoginConfig,
};
}
function resetPlatformConfig() {
pageConfig.value = {
...pageConfig.value,
...appStore.defaultPlatformConfig,
};
}
/**
* 全部重置
*/
function resetAll() {
pageConfig.value = { ...appStore.getDefaulPageConfig };
}
function makeParams() {
const request = [
{
paramKey: 'ui.icon',
paramValue: pageConfig.value.icon[0]?.url,
type: 'file',
fileName: pageConfig.value.icon[0]?.name,
},
{
paramKey: 'ui.loginLogo',
paramValue: pageConfig.value.loginLogo[0]?.url,
type: 'file',
fileName: pageConfig.value.loginLogo[0]?.name,
},
{
paramKey: 'ui.loginImage',
paramValue: pageConfig.value.loginImage[0]?.url,
type: 'file',
fileName: pageConfig.value.loginImage[0]?.name,
},
{
paramKey: 'ui.logoPlatform',
paramValue: pageConfig.value.logoPlatform[0]?.url,
type: 'file',
fileName: pageConfig.value.logoPlatform[0]?.name,
},
{ paramKey: 'ui.slogan', paramValue: pageConfig.value.slogan, type: 'text' },
{ paramKey: 'ui.title', paramValue: pageConfig.value.title, type: 'text' },
{ paramKey: 'ui.style', paramValue: pageConfig.value.customStyle || pageConfig.value.style, type: 'text' },
{ paramKey: 'ui.theme', paramValue: pageConfig.value.customTheme || pageConfig.value.theme, type: 'text' },
{ paramKey: 'ui.helpDoc', paramValue: pageConfig.value.helpDoc, type: 'text' },
{ paramKey: 'ui.platformName', paramValue: pageConfig.value.platformName, type: 'text' },
];
const fileList = [
pageConfig.value.icon[0].file
? new File([pageConfig.value.icon[0].file as File], `ui.icon,${pageConfig.value.icon[0].file?.name}`)
: undefined,
pageConfig.value.loginLogo[0].file
? new File(
[pageConfig.value.loginLogo[0].file as File],
`ui.loginLogo,${pageConfig.value.loginLogo[0].file?.name}`
)
: undefined,
pageConfig.value.loginImage[0].file
? new File(
[pageConfig.value.loginImage[0].file as File],
`ui.loginImage,${pageConfig.value.loginImage[0].file?.name}`
)
: undefined,
pageConfig.value.logoPlatform[0].file
? new File(
[pageConfig.value.logoPlatform[0].file as File],
`ui.logoPlatform,${pageConfig.value.logoPlatform[0].file?.name}`
)
: undefined,
].filter((e) => e !== undefined);
return { request, fileList };
}
/**
* 保存并应用
*/
async function save() {
try {
pageloading.value = true;
await savePageConfig(makeParams());
Message.success(t('system.config.page.saveSuccess'));
appStore.initPageConfig(); //
} catch (error) {
console.log(error);
} finally {
pageloading.value = false;
}
}
/**
* 保存前校验
*/
function beforeSave() {
try {
loginConfigFormRef.value?.validate((errors: Record<string, ValidatedError> | undefined) => {
if (errors) {
throw new Error('登录页表单校验不通过');
}
});
platformConfigFormRef.value?.validate((errors: Record<string, ValidatedError> | undefined) => {
if (errors) {
throw new Error('平台页表单校验不通过');
}
});
save();
} catch (error) {
console.log(error);
}
const errDom = document.querySelector('.arco-input-error');
scrollIntoView(errDom, { block: 'center' });
}
</script>
<style lang="less" scoped>
.config-title {
@apply flex items-center font-medium;
margin-bottom: 16px;
}
.config-content {
padding: 16px;
min-width: 1150px;
border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
.config-content-head {
@apply flex items-center justify-between;
}
.config-preview {
@apply relative flex items-start overflow-hidden;
height: 495px;
@media screen and (min-width: 1600px) {
height: 550px;
}
@media screen and (min-width: 1800px) {
height: auto;
}
.config-preview-head {
@apply flex items-center justify-between bg-white;
padding: 8px;
width: 735px;
@media screen and (min-width: 1600px) {
width: 100%;
}
}
.full-preview-head {
width: 100vw;
}
.login-preview {
position: relative;
width: 740px;
@media screen and (min-width: 1600px) {
width: 882px;
}
@media screen and (min-width: 1800px) {
width: 100%;
}
.normal-preview {
transform: translate(-25%, -25%) scale(0.5);
@media screen and (min-width: 1600px) {
transform: translate(-20%, -20%) scale(0.6);
}
@media screen and (min-width: 1800px) {
transform: none;
}
}
.page-preview {
@apply relative flex flex-1;
width: 1470px;
height: 916px;
transform-origin: center;
@media screen and (min-width: 1800px) {
width: 100%;
height: 632px;
}
}
.platform-preview {
@media screen and (min-width: 1800px) {
transform: translate(0, 0) scale(1);
}
}
.full-preview {
width: 100vw;
transform: none;
}
}
.config-form {
margin-left: 12px;
.config-form-card {
@apply bg-white;
margin-bottom: 8px;
padding: 12px;
border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-small);
}
}
}
}
</style>

View File

@ -14,6 +14,7 @@
</a-tabs> </a-tabs>
</MsCard> </MsCard>
<baseConfig v-show="activeTab === 'baseConfig'" /> <baseConfig v-show="activeTab === 'baseConfig'" />
<pageConfig v-show="activeTab === 'pageConfig'" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -21,10 +22,11 @@
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import baseConfig from './components/baseConfig.vue'; import baseConfig from './components/baseConfig.vue';
import pageConfig from './components/pageConfig.vue';
const { t } = useI18n(); const { t } = useI18n();
const activeTab = ref('baseConfig'); const activeTab = ref('pageConfig');
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -45,4 +45,49 @@ export default {
'system.config.email.emailErrTip': '邮箱格式错误,请重新输入', 'system.config.email.emailErrTip': '邮箱格式错误,请重新输入',
'system.config.email.updateSuccess': '更新成功', 'system.config.email.updateSuccess': '更新成功',
'system.config.email.testSuccess': '邮箱连接成功', 'system.config.email.testSuccess': '邮箱连接成功',
'system.config.page.style': '平台风格',
'system.config.page.styleTip': '平台风格是指页面背景色风格',
'system.config.page.theme': '平台主题色',
'system.config.page.themeTip': '平台主题色是指除了页面背景色之外的 MeterSphere 紫色系颜色',
'system.config.page.default': '默认',
'system.config.page.follow': '跟随主题色',
'system.config.page.custom': '自定义',
'system.config.page.loginPageConfig': '平台登录页设置',
'system.config.page.pagePreview': '页面预览',
'system.config.page.reset': '恢复默认',
'system.config.page.icon': '网站 icon',
'system.config.page.replace': '替换图片',
'system.config.page.iconTip':
'顶部网站显示的 icon建议使用 SVG 或 PNG 格式透明背景图片,宽高 18px图片大小仅支持 200 KB 以内',
'system.config.page.loginLogo': '登录 logo',
'system.config.page.loginLogoTip':
'登录页面右侧 logo建议使用 SVG 或 PNG 格式透明背景图片,高度不小于 48px图片大小仅支持 200 KB 以内',
'system.config.page.loginBg': '登录背景图',
'system.config.page.loginBgTip':
'背景图建议使用 SVG 格式;矢量图建议尺寸 800*900位图建议尺寸 800*900图片大小仅支持 1 MB 以内',
'system.config.page.slogan': 'Slogan',
'system.config.page.sloganPlaceholder': '请输入 Slogan',
'system.config.page.sloganRquired': 'Slogan不能为空',
'system.config.page.sloganTip': '产品logo下的 slogan',
'system.config.page.title': '网站名称',
'system.config.page.titlePlaceholder': '请输入网站名称',
'system.config.page.titleTip': '显示在网页tab的平台名称',
'system.config.page.loginPreviewTip': 'tips:默认为 MeterSphere 系统界面,支持自定义平台界面设置',
'system.config.page.platformConfig': '平台设置',
'system.config.page.platformLogo': '平台 Logo',
'system.config.page.platformLogoTip':
'平台页面顶部显示的 logo建议使用 SVG 或 PNG 格式透明背景图片,高度不小于 32px图片大小仅支持 200 KB 以内',
'system.config.page.platformName': '平台名称',
'system.config.page.platformNamePlaceholder': '请输入平台名称',
'system.config.page.platformNameRquired': '平台名称不能为空',
'system.config.page.platformNameTip': '全站通用产品名称,建议字数 8',
'system.config.page.helpDoc': '帮助文档',
'system.config.page.helpDocPlaceholder': '请输入帮助文档地址',
'system.config.page.helpDocTip': '可设置帮助文档跳转链接,默认为官方帮助文档',
'system.config.page.platformConfigTip': 'tips:默认为 MeterSphere 系统界面,支持自定义平台界面设置',
'system.config.page.resetAll': '恢复默认',
'system.config.page.cancel': '放弃更新',
'system.config.page.save': '保存并应用',
'system.config.page.unsave': '未保存',
'system.config.page.saveSuccess': '保存成功',
}; };

View File

@ -254,17 +254,6 @@
</span> </span>
</a-tooltip> </a-tooltip>
</a-form-item> </a-form-item>
<a-form-item
:label="t('system.resourcePool.testResourceDTO.apiTestImage')"
field="testResourceDTO.apiTestImage"
class="form-item"
>
<a-input
v-model:model-value="form.testResourceDTO.apiTestImage"
:placeholder="t('system.resourcePool.testResourceDTO.apiTestImagePlaceholder')"
:max-length="250"
></a-input>
</a-form-item>
<a-form-item <a-form-item
:label="t('system.resourcePool.testResourceDTO.deployName')" :label="t('system.resourcePool.testResourceDTO.deployName')"
field="testResourceDTO.deployName" field="testResourceDTO.deployName"
@ -352,6 +341,7 @@
import { scrollIntoView } from '@/utils/dom'; import { scrollIntoView } from '@/utils/dom';
import { addPool, getPoolInfo, updatePoolInfo } from '@/api/modules/setting/resourcePool'; import { addPool, getPoolInfo, updatePoolInfo } from '@/api/modules/setting/resourcePool';
import { getAllOrgList } from '@/api/modules/setting/orgnization'; import { getAllOrgList } from '@/api/modules/setting/orgnization';
import useAppStore from '@/store/modules/app';
import type { MsBatchFormInstance, FormItemModel } from '@/components/bussiness/ms-batch-form/types'; import type { MsBatchFormInstance, FormItemModel } from '@/components/bussiness/ms-batch-form/types';
import type { UpdateResourcePoolParams, NodesListItem } from '@/models/setting/resourcePool'; import type { UpdateResourcePoolParams, NodesListItem } from '@/models/setting/resourcePool';
@ -359,6 +349,8 @@
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore();
const title = ref(''); const title = ref('');
const loading = ref(false); const loading = ref(false);
const defaultForm = { const defaultForm = {
@ -383,7 +375,6 @@
token: '', token: '',
nameSpaces: '', nameSpaces: '',
jobDefinition: job, jobDefinition: job,
apiTestImage: '',
deployName: '', deployName: '',
orgIds: [] as string[], orgIds: [] as string[],
}, },
@ -626,7 +617,6 @@
} }
} }
const apiImageTag = ref('dev');
/** /**
* 下载 yaml 文件 * 下载 yaml 文件
* @param type 文件类型 * @param type 文件类型
@ -634,11 +624,8 @@
function downloadYaml(type: YamlType) { function downloadYaml(type: YamlType) {
let name = ''; let name = '';
let yamlStr = ''; let yamlStr = '';
const { nameSpaces, deployName, apiTestImage } = form.value.testResourceDTO; const { nameSpaces, deployName } = form.value.testResourceDTO;
let apiImage = `registry.cn-qingdao.aliyuncs.com/metersphere/node-controller:${apiImageTag.value}`; const apiImage = `registry.cn-qingdao.aliyuncs.com/metersphere/node-controller:${appStore.version}`;
if (apiTestImage) {
apiImage = apiTestImage;
}
switch (type) { switch (type) {
case 'role': case 'role':
name = 'Role.yml'; name = 'Role.yml';
@ -680,7 +667,6 @@
concurrentNumber, // k8s concurrentNumber, // k8s
podThreads, // k8s pod线 podThreads, // k8s pod线
jobDefinition, // k8s job jobDefinition, // k8s job
apiTestImage, // k8s api
deployName, // k8s api deployName, // k8s api
nodesList, nodesList,
loadTestImage, loadTestImage,
@ -701,7 +687,6 @@
nameSpaces, nameSpaces,
concurrentNumber, concurrentNumber,
podThreads, podThreads,
apiTestImage,
deployName, deployName,
} }
: {}; : {};

View File

@ -298,7 +298,6 @@
nameSpaces, // k8s nameSpaces, // k8s
concurrentNumber, // k8s concurrentNumber, // k8s
podThreads, // k8s pod线 podThreads, // k8s pod线
apiTestImage, // k8s api
deployName, // k8s api deployName, // k8s api
girdConcurrentNumber, girdConcurrentNumber,
nodesList, nodesList,
@ -337,10 +336,6 @@
label: t('system.resourcePool.testResourceDTO.deployName'), label: t('system.resourcePool.testResourceDTO.deployName'),
value: deployName, value: deployName,
}, },
{
label: t('system.resourcePool.testResourceDTO.apiTestImage'),
value: apiTestImage,
},
{ {
label: t('system.resourcePool.testResourceDTO.concurrentNumber'), label: t('system.resourcePool.testResourceDTO.concurrentNumber'),
value: concurrentNumber, value: concurrentNumber,