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 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: [
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
|
|
@ -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 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';
|
||||||
|
|
|
@ -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 {
|
.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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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"
|
@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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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最大并发数
|
||||||
|
|
|
@ -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'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
</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>
|
||||||
|
|
|
@ -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': '保存成功',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue