feat(系统设置): 参数设置-基础设置&界面设置、部分组件调整、布局&登录增加预览模式
This commit is contained in:
parent
e596e26051
commit
3e759804dc
|
@ -7,7 +7,6 @@ import configArcoStyleImportPlugin from './plugin/arcoStyleImport';
|
|||
import configArcoResolverPlugin from './plugin/arcoResolver';
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
import vueSetupExtend from 'vite-plugin-vue-setup-extend';
|
||||
import monacoEditorPlugin from 'vite-plugin-monaco-editor';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
@ -23,7 +22,6 @@ export default defineConfig({
|
|||
// 指定symbolId格式
|
||||
symbolId: 'icon-[name]',
|
||||
}),
|
||||
monacoEditorPlugin({}),
|
||||
],
|
||||
resolve: {
|
||||
alias: [
|
||||
|
|
|
@ -36,10 +36,10 @@
|
|||
"dependencies": {
|
||||
"@7polo/kity": "2.0.8",
|
||||
"@7polo/kityminder-core": "1.4.53",
|
||||
"@arco-design/web-vue": "^2.48.0",
|
||||
"@arco-themes/vue-ms-theme-default": "^0.0.21",
|
||||
"@arco-design/web-vue": "^2.49.2",
|
||||
"@arco-themes/vue-ms-theme-default": "^0.0.24",
|
||||
"@form-create/arco-design": "^3.1.21",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"@vueuse/core": "^10.2.1",
|
||||
"ace-builds": "^1.22.0",
|
||||
"axios": "^0.24.0",
|
||||
"dayjs": "^1.11.8",
|
||||
|
@ -60,6 +60,7 @@
|
|||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.2.2",
|
||||
"vue3-ace-editor": "^2.2.2",
|
||||
"vue3-colorpicker": "^2.1.6",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -13,6 +13,9 @@
|
|||
import useLocale from '@/locale/useLocale';
|
||||
import { saveBaseInfo, getBaseInfo } from '@/api/modules/setting/config';
|
||||
import { getLocalStorage, setLocalStorage } from '@/utils/local-storage';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { currentLocale } = useLocale();
|
||||
const locale = computed(() => {
|
||||
|
@ -26,9 +29,11 @@
|
|||
}
|
||||
});
|
||||
|
||||
// 项目初始化时需要获取基础设置信息,看当前站点 url是否为系统内置默认地址,如果是需要替换为当前项目部署的 url 地址
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
appStore.initSystemversion(); // 初始化系统版本
|
||||
appStore.initPageConfig(); // 初始化页面配置
|
||||
// 项目初始化时需要获取基础设置信息,看当前站点 url是否为系统内置默认地址,如果是需要替换为当前项目部署的 url 地址
|
||||
const isInitUrl = getLocalStorage('isInitUrl'); // 是否已经初始化过 url
|
||||
if (isInitUrl === 'true') return;
|
||||
const res = await getBaseInfo();
|
||||
|
|
|
@ -81,16 +81,22 @@ export class MSAxios {
|
|||
/**
|
||||
* @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 fileName = params.fileList.length === 1 ? 'file' : 'files';
|
||||
|
||||
params.fileList.forEach((file: File) => {
|
||||
formData.append(fileName, file);
|
||||
});
|
||||
if (customFileKey !== '') {
|
||||
params.fileList.forEach((file: File) => {
|
||||
formData.append(customFileKey, file);
|
||||
});
|
||||
} else {
|
||||
params.fileList.forEach((file: File) => {
|
||||
formData.append(fileName, file);
|
||||
});
|
||||
}
|
||||
if (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) => {
|
||||
this.axiosInstance
|
||||
|
|
|
@ -5,9 +5,18 @@ import {
|
|||
SaveEmailInfoUrl,
|
||||
GetBaseInfoUrl,
|
||||
GetEmailInfoUrl,
|
||||
SavePageConfigUrl,
|
||||
GetPageConfigUrl,
|
||||
} 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) {
|
||||
|
@ -33,3 +42,13 @@ export function saveEmailInfo(data: SaveInfoParams) {
|
|||
export function getEmailInfo() {
|
||||
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 });
|
||||
}
|
||||
|
|
|
@ -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 };
|
|
@ -3,3 +3,5 @@ export const SaveBaseInfoUrl = '/system/parameter/save/base-info';
|
|||
export const SaveEmailInfoUrl = '/system/parameter/edit/email-info';
|
||||
export const GetEmailInfoUrl = '/system/parameter/get/email-info';
|
||||
export const GetBaseInfoUrl = '/system/parameter/get/base-info';
|
||||
export const SavePageConfigUrl = '/display/save';
|
||||
export const GetPageConfigUrl = '/display/info';
|
||||
|
|
|
@ -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 |
|
@ -31,6 +31,9 @@
|
|||
.arco-tabs-nav-add-btn {
|
||||
font-size: var(--font-size-body-3);
|
||||
}
|
||||
.arco-tabs-tab {
|
||||
padding: 13px 0 !important;
|
||||
}
|
||||
|
||||
/** Modal对话框 **/
|
||||
.arco-modal {
|
||||
|
|
|
@ -78,3 +78,6 @@ body {
|
|||
font-size: 12px;
|
||||
color: rgb(var(--color-text-4));
|
||||
}
|
||||
.one-line-text {
|
||||
@apply overflow-hidden overflow-ellipsis whitespace-nowrap;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
>
|
||||
<div v-if="!props.simple" class="card-header">
|
||||
<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>
|
||||
<a-divider v-if="!props.simple" class="mb-[16px]" />
|
||||
<div class="ms-card-container">
|
||||
|
|
|
@ -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>
|
|
@ -8,31 +8,33 @@
|
|||
@before-upload="beforeUpload"
|
||||
>
|
||||
<template #upload-button>
|
||||
<div class="ms-upload-area">
|
||||
<div class="ms-upload-icon-box">
|
||||
<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>
|
||||
<slot>
|
||||
<div class="ms-upload-area">
|
||||
<div class="ms-upload-icon-box">
|
||||
<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>
|
||||
<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>
|
||||
</slot>
|
||||
</template>
|
||||
</a-upload>
|
||||
</template>
|
||||
|
@ -60,7 +62,7 @@
|
|||
disabled: boolean;
|
||||
iconType: string;
|
||||
maxSize: number; // 文件大小限制,单位 MB
|
||||
[key: string]: any;
|
||||
sizeUnit: 'MB' | 'KB'; // 文件大小单位
|
||||
}> & {
|
||||
accept: UploadType;
|
||||
fileList: FileItem[];
|
||||
|
@ -107,7 +109,8 @@
|
|||
fileList.value = [];
|
||||
}
|
||||
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'));
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,16 @@
|
|||
<div class="navbar">
|
||||
<div class="left-side">
|
||||
<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>
|
||||
</div>
|
||||
<div class="center-side">
|
||||
<div v-if="!props.isPreview" class="center-side">
|
||||
<template v-if="showProjectSelect">
|
||||
<a-divider direction="vertical" class="ml-0" />
|
||||
<a-select
|
||||
|
@ -29,7 +35,7 @@
|
|||
</template>
|
||||
<TopMenu />
|
||||
</div>
|
||||
<ul class="right-side">
|
||||
<ul v-if="!props.isPreview" class="right-side">
|
||||
<li>
|
||||
<a-tooltip :content="t('settings.navbar.search')">
|
||||
<a-button type="secondary">
|
||||
|
@ -190,6 +196,12 @@
|
|||
|
||||
import type { ProjectListItem } from '@/models/setting/project';
|
||||
|
||||
const props = defineProps<{
|
||||
isPreview?: boolean;
|
||||
logo?: string;
|
||||
name?: string;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
// const { logout } = useUser();
|
||||
const route = useRoute();
|
||||
|
|
|
@ -8,7 +8,7 @@ export enum UploadAcceptEnum {
|
|||
csv = '.csv',
|
||||
zip = '.zip',
|
||||
xmind = '.xmind',
|
||||
image = '.jpg,.png',
|
||||
image = '.jpg,.jpeg,.png,.svg',
|
||||
jar = '.jar',
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||
<div v-if="navbar" class="layout-navbar z-[100]">
|
||||
<NavBar />
|
||||
<NavBar :is-preview="innerProps.isPreview" :logo="innerLogo" :name="innerName" />
|
||||
</div>
|
||||
<a-layout>
|
||||
<a-layout>
|
||||
<a-layout-sider
|
||||
v-if="renderMenu"
|
||||
v-if="renderMenu && !innerProps.isPreview"
|
||||
v-show="!hideMenu"
|
||||
class="layout-sider z-[99]"
|
||||
breakpoint="xl"
|
||||
|
@ -43,7 +43,8 @@
|
|||
>
|
||||
<MsBreadCrumb />
|
||||
<a-layout-content>
|
||||
<PageLayout />
|
||||
<PageLayout v-if="!props.isPreview" />
|
||||
<slot></slot>
|
||||
</a-layout-content>
|
||||
<Footer v-if="footer" />
|
||||
</a-scrollbar>
|
||||
|
@ -65,12 +66,39 @@
|
|||
import PageLayout from './page-layout.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 appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
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 navbar = computed(() => appStore.navbar);
|
||||
const renderMenu = computed(() => appStore.menu);
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { Recordable } from '#/global';
|
||||
import { FileItem } from '@arco-design/web-vue';
|
||||
|
||||
// 基础信息配置
|
||||
export interface BaseConfig {
|
||||
url: string;
|
||||
|
@ -36,3 +39,47 @@ export interface TestEmailParams {
|
|||
'smtp.tsl': 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;
|
||||
|
|
|
@ -23,7 +23,6 @@ export interface TestResourceDTO {
|
|||
concurrentNumber: number; // k8s 最大并发数
|
||||
podThreads: number; // k8s 单pod最大线程数
|
||||
jobDefinition: string; // k8s job自定义模板
|
||||
apiTestImage: string; // k8s api测试镜像
|
||||
deployName: string; // k8s api测试部署名称
|
||||
uiGrid: string; // ui测试selenium-grid
|
||||
girdConcurrentNumber: number; // ui测试selenium-grid最大并发数
|
||||
|
|
|
@ -2,13 +2,36 @@ import { defineStore } from 'pinia';
|
|||
import { Notification } from '@arco-design/web-vue';
|
||||
import defaultSettings from '@/config/settings.json';
|
||||
import { getMenuList } from '@/api/modules/user';
|
||||
import { getSystemVersion } from '@/api/modules/system';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
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 { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
||||
import type { AppState } from './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', {
|
||||
state: (): AppState => ({
|
||||
|
@ -20,6 +43,15 @@ const useAppStore = defineStore('app', {
|
|||
breadcrumbList: [] as BreadcrumbItem[],
|
||||
currentOrgId: '',
|
||||
currentProjectId: '',
|
||||
version: '',
|
||||
defaultThemeConfig,
|
||||
defaultLoginConfig,
|
||||
defaultPlatformConfig,
|
||||
pageConfig: {
|
||||
...defaultThemeConfig,
|
||||
...defaultLoginConfig,
|
||||
...defaultPlatformConfig,
|
||||
},
|
||||
}),
|
||||
|
||||
getters: {
|
||||
|
@ -32,9 +64,6 @@ const useAppStore = defineStore('app', {
|
|||
appAsyncMenus(state: AppState): RouteRecordNormalized[] {
|
||||
return state.serverMenu as unknown as RouteRecordNormalized[];
|
||||
},
|
||||
getCustomTheme(state: AppState): string {
|
||||
return state.customTheme as string;
|
||||
},
|
||||
getLoadingStatus(state: AppState): boolean {
|
||||
return state.loading;
|
||||
},
|
||||
|
@ -53,6 +82,13 @@ const useAppStore = defineStore('app', {
|
|||
getCurrentProjectId(state: AppState): string {
|
||||
return state.currentProjectId;
|
||||
},
|
||||
getDefaulPageConfig(state: AppState): PageConfig {
|
||||
return {
|
||||
...state.defaultThemeConfig,
|
||||
...state.defaultLoginConfig,
|
||||
...state.defaultPlatformConfig,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
@ -158,9 +194,59 @@ const useAppStore = defineStore('app', {
|
|||
setCurrentProjectId(id: string) {
|
||||
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: {
|
||||
paths: ['currentOrgId', 'currentProjectId'],
|
||||
paths: ['currentOrgId', 'currentProjectId', 'pageConfig'],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
||||
import type { BreadcrumbItem } from '@/components/bussiness/ms-breadcrumb/types';
|
||||
import type { PageConfig, ThemeConfig, LoginConfig, PlatformConfig } from '@/models/setting/config';
|
||||
|
||||
export interface AppState {
|
||||
theme: string;
|
||||
colorWeak: boolean;
|
||||
navbar: boolean;
|
||||
menu: boolean;
|
||||
hideMenu: boolean;
|
||||
menuCollapse: boolean;
|
||||
footer: boolean;
|
||||
themeColor: string;
|
||||
menuWidth: number;
|
||||
globalSettings: boolean;
|
||||
device: string;
|
||||
|
@ -27,7 +26,11 @@ export interface AppState {
|
|||
showTotal: boolean;
|
||||
showJumper: boolean;
|
||||
hideOnSinglePage: boolean;
|
||||
[key: string]: unknown;
|
||||
version: string;
|
||||
defaultThemeConfig: ThemeConfig;
|
||||
defaultLoginConfig: LoginConfig;
|
||||
defaultPlatformConfig: PlatformConfig;
|
||||
pageConfig: PageConfig;
|
||||
}
|
||||
|
||||
export type CustomTheme = 'theme-default' | 'theme-green';
|
||||
|
|
|
@ -4,7 +4,7 @@ export interface ScrollToViewOptions {
|
|||
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 = {
|
||||
behavior: options.behavior || 'smooth',
|
||||
block: options.block || 'start',
|
||||
|
|
|
@ -133,3 +133,21 @@ export function desensitize(str: string): string {
|
|||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,19 +1,32 @@
|
|||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import bannerImage from '@/assets/images/login-banner.png';
|
||||
<script lang="ts" setup>
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.banner-wrap {
|
||||
padding-top: 160px;
|
||||
height: 760px;
|
||||
width: 55%;
|
||||
.img {
|
||||
height: 365px;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,39 +1,35 @@
|
|||
<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="mt-40 flex justify-center">
|
||||
<svg-icon width="290px" height="60px" name="login-logo" />
|
||||
<div class="flex justify-center">
|
||||
<img v-if="innerLogo" :src="innerLogo" class="h-[60px] w-[290px]" />
|
||||
<svg-icon v-else width="290px" height="60px" name="login-logo" />
|
||||
</div>
|
||||
<div class="title-0 flex justify-center">
|
||||
<span class="title-welcome">{{ $t('login.form.title') }}</span>
|
||||
<div class="title-0 mt-[16px] flex justify-center">
|
||||
<span class="title-welcome">{{ innerSlogan || $t('login.form.title') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form mt-20">
|
||||
<div class="form mt-[32px]">
|
||||
<a-form ref="formRef" :model="userInfo" @submit="handleSubmit">
|
||||
<a-form-item field="radio" hide-label>
|
||||
<a-radio-group v-model="userInfo.authenticate">
|
||||
<a-radio value="LDAP">LDAP</a-radio>
|
||||
<a-form-item class="login-form-item" field="radio" hide-label>
|
||||
<a-radio-group v-model="userInfo.authenticate" type="button">
|
||||
<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-group>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
class="login-form-item"
|
||||
field="username"
|
||||
:rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
hide-label
|
||||
>
|
||||
<a-input
|
||||
v-model="userInfo.username"
|
||||
:placeholder="$t('login.form.userName.placeholder')"
|
||||
style="border-radius: 1.5rem"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-user />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-input v-model="userInfo.username" :placeholder="$t('login.form.userName.placeholder')" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
class="login-form-item"
|
||||
field="password"
|
||||
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
|
@ -43,40 +39,52 @@
|
|||
v-model="userInfo.password"
|
||||
:placeholder="$t('login.form.password.placeholder')"
|
||||
allow-clear
|
||||
style="border-radius: 1.5rem"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-lock />
|
||||
</template>
|
||||
</a-input-password>
|
||||
/>
|
||||
</a-form-item>
|
||||
<div class="mt-4">
|
||||
<a-button style="border-radius: 1.5rem" type="primary" html-type="submit" long :loading="loading">
|
||||
<div class="mt-[12px]">
|
||||
<a-button type="primary" html-type="submit" long :loading="loading">
|
||||
{{ $t('login.form.login') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
<div v-if="props.isPreview" class="mask"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { ValidatedError } from '@arco-design/web-vue/es/form/interface';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useUserStore } from '@/store';
|
||||
import { useUserStore, useAppStore } from '@/store';
|
||||
import useLoading from '@/hooks/useLoading';
|
||||
import type { LoginData } from '@/models/user';
|
||||
import { setLoginExpires } from '@/utils/auth';
|
||||
|
||||
const router = useRouter();
|
||||
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 { loading, setLoading } = useLoading();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
|
@ -127,12 +135,31 @@
|
|||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/* stylelint-disable color-function-notation */
|
||||
.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 {
|
||||
color: #783887;
|
||||
color: rgb(var(--primary-5));
|
||||
}
|
||||
.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>
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
<template>
|
||||
<div class="my-container h-[100vh] w-[100vw] min-w-[1440px]">
|
||||
<a-row class="flex h-[730px] flex-row">
|
||||
<a-col :span="11"> <loginForm /></a-col>
|
||||
<a-divider direction="vertical" />
|
||||
<a-col :span="11"> <banner /></a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<a-scrollbar
|
||||
:style="{
|
||||
overflow: 'auto',
|
||||
width: props.isPreview ? '100%' : '100vw',
|
||||
height: props.isPreview ? '100%' : '100vh',
|
||||
}"
|
||||
>
|
||||
<div class="login-page" :style="props.isPreview ? '' : 'min-width: 1200px;'">
|
||||
<banner />
|
||||
<loginForm :is-preview="props.isPreview" />
|
||||
</div>
|
||||
</a-scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import loginForm from './components/login-form.vue';
|
||||
import banner from './components/banner.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isPreview?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.my-container {
|
||||
.login-box {
|
||||
margin-top: calc(50vh - 400px);
|
||||
}
|
||||
.arco-divider-vertical {
|
||||
height: 52em;
|
||||
}
|
||||
.login-page {
|
||||
@apply flex items-center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
|
@ -14,6 +14,7 @@
|
|||
</a-tabs>
|
||||
</MsCard>
|
||||
<baseConfig v-show="activeTab === 'baseConfig'" />
|
||||
<pageConfig v-show="activeTab === 'pageConfig'" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -21,10 +22,11 @@
|
|||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import baseConfig from './components/baseConfig.vue';
|
||||
import pageConfig from './components/pageConfig.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const activeTab = ref('baseConfig');
|
||||
const activeTab = ref('pageConfig');
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -45,4 +45,49 @@ export default {
|
|||
'system.config.email.emailErrTip': '邮箱格式错误,请重新输入',
|
||||
'system.config.email.updateSuccess': '更新成功',
|
||||
'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': '保存成功',
|
||||
};
|
||||
|
|
|
@ -254,17 +254,6 @@
|
|||
</span>
|
||||
</a-tooltip>
|
||||
</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
|
||||
:label="t('system.resourcePool.testResourceDTO.deployName')"
|
||||
field="testResourceDTO.deployName"
|
||||
|
@ -352,6 +341,7 @@
|
|||
import { scrollIntoView } from '@/utils/dom';
|
||||
import { addPool, getPoolInfo, updatePoolInfo } from '@/api/modules/setting/resourcePool';
|
||||
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 { UpdateResourcePoolParams, NodesListItem } from '@/models/setting/resourcePool';
|
||||
|
@ -359,6 +349,8 @@
|
|||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const title = ref('');
|
||||
const loading = ref(false);
|
||||
const defaultForm = {
|
||||
|
@ -383,7 +375,6 @@
|
|||
token: '',
|
||||
nameSpaces: '',
|
||||
jobDefinition: job,
|
||||
apiTestImage: '',
|
||||
deployName: '',
|
||||
orgIds: [] as string[],
|
||||
},
|
||||
|
@ -626,7 +617,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
const apiImageTag = ref('dev');
|
||||
/**
|
||||
* 下载 yaml 文件
|
||||
* @param type 文件类型
|
||||
|
@ -634,11 +624,8 @@
|
|||
function downloadYaml(type: YamlType) {
|
||||
let name = '';
|
||||
let yamlStr = '';
|
||||
const { nameSpaces, deployName, apiTestImage } = form.value.testResourceDTO;
|
||||
let apiImage = `registry.cn-qingdao.aliyuncs.com/metersphere/node-controller:${apiImageTag.value}`;
|
||||
if (apiTestImage) {
|
||||
apiImage = apiTestImage;
|
||||
}
|
||||
const { nameSpaces, deployName } = form.value.testResourceDTO;
|
||||
const apiImage = `registry.cn-qingdao.aliyuncs.com/metersphere/node-controller:${appStore.version}`;
|
||||
switch (type) {
|
||||
case 'role':
|
||||
name = 'Role.yml';
|
||||
|
@ -680,7 +667,6 @@
|
|||
concurrentNumber, // k8s 最大并发数
|
||||
podThreads, // k8s 单pod最大线程数
|
||||
jobDefinition, // k8s job自定义模板
|
||||
apiTestImage, // k8s api测试镜像
|
||||
deployName, // k8s api测试部署名称
|
||||
nodesList,
|
||||
loadTestImage,
|
||||
|
@ -701,7 +687,6 @@
|
|||
nameSpaces,
|
||||
concurrentNumber,
|
||||
podThreads,
|
||||
apiTestImage,
|
||||
deployName,
|
||||
}
|
||||
: {};
|
||||
|
|
|
@ -298,7 +298,6 @@
|
|||
nameSpaces, // k8s 命名空间
|
||||
concurrentNumber, // k8s 最大并发数
|
||||
podThreads, // k8s 单pod最大线程数
|
||||
apiTestImage, // k8s api测试镜像
|
||||
deployName, // k8s api测试部署名称
|
||||
girdConcurrentNumber,
|
||||
nodesList,
|
||||
|
@ -337,10 +336,6 @@
|
|||
label: t('system.resourcePool.testResourceDTO.deployName'),
|
||||
value: deployName,
|
||||
},
|
||||
{
|
||||
label: t('system.resourcePool.testResourceDTO.apiTestImage'),
|
||||
value: apiTestImage,
|
||||
},
|
||||
{
|
||||
label: t('system.resourcePool.testResourceDTO.concurrentNumber'),
|
||||
value: concurrentNumber,
|
||||
|
|
Loading…
Reference in New Issue