feat(系统设置): 参数设置-界面设置-自定义平台风格&主题

This commit is contained in:
baiqi 2023-08-07 13:47:04 +08:00 committed by fit2-zhao
parent a2cb845f82
commit e672c2dfdf
30 changed files with 1362 additions and 75 deletions

View File

@ -18,7 +18,7 @@ export default defineConfig({
configArcoStyleImportPlugin(),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [resolve(process.cwd(), 'src/assets/svg')], // 与本地储存地址一致
iconDirs: [resolve(process.cwd(), 'src/assets/svg'), resolve(process.cwd(), 'public/images')], // 与本地储存地址一致
// 指定symbolId格式
symbolId: 'icon-[name]',
}),

View File

@ -2,11 +2,7 @@
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link
rel="shortcut icon"
type="image/x-icon"
href="/src/assets/favicon.ico"
/>
<link rel="shortcut icon" type="image/x-icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MeterSphere</title>
</head>

View File

@ -40,6 +40,7 @@
"@arco-design/web-vue": "^2.49.2",
"@arco-themes/vue-ms-theme-default": "^0.0.25",
"@form-create/arco-design": "^3.1.21",
"@types/color": "^3.0.3",
"@vueuse/core": "^10.2.1",
"ace-builds": "^1.22.0",
"axios": "^0.24.0",
@ -79,8 +80,11 @@
"@vitest/coverage-c8": "^0.31.4",
"@vue/babel-plugin-jsx": "^1.1.1",
"@vue/test-utils": "^2.3.2",
"@zougt/some-loader-utils": "^1.4.3",
"@zougt/vite-plugin-theme-preprocessor": "^1.4.8",
"autoprefixer": "^10.4.14",
"axios-mock-adapter": "^1.21.5",
"color": "^4.2.3",
"consola": "^2.15.3",
"cross-env": "^7.0.3",
"eslint": "^8.42.0",

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -14,6 +14,7 @@
import { saveBaseInfo, getBaseInfo } from '@/api/modules/setting/config';
import { getLocalStorage, setLocalStorage } from '@/utils/local-storage';
import useAppStore from '@/store/modules/app';
import { watchStyle, watchTheme } from '@/utils/theme';
const appStore = useAppStore();
@ -29,6 +30,11 @@
}
});
//
watchStyle(appStore.pageConfig.style, appStore.pageConfig);
watchTheme(appStore.pageConfig.theme, appStore.pageConfig);
window.document.title = appStore.pageConfig.title;
onBeforeMount(async () => {
try {
appStore.initSystemversion(); //

View File

@ -7,7 +7,12 @@ import {
GetEmailInfoUrl,
SavePageConfigUrl,
GetPageConfigUrl,
GetAuthListUrl,
GetAuthDetailUrl,
UpdateAuthUrl,
AddAuthUrl,
} from '@/api/requrls/setting/config';
import { TableQueryParams } from '@/models/common';
import type {
SaveInfoParams,
@ -16,6 +21,8 @@ import type {
BaseConfig,
SavePageConfigParams,
PageConfigReturns,
AuthItem,
AuthParams,
} from '@/models/setting/config';
// 测试邮箱连接
@ -52,3 +59,23 @@ export function savePageConfig(data: SavePageConfigParams) {
export function getPageConfig() {
return MSR.get<PageConfigReturns>({ url: GetPageConfigUrl });
}
// 获取认证源列表
export function getAuthList(data: TableQueryParams) {
return MSR.post<AuthItem[]>({ url: GetAuthListUrl, data });
}
// 获取认证源详情
export function getAuthDetail(id: string) {
return MSR.get<AuthItem>({ url: GetAuthDetailUrl, params: { id } });
}
// 添加认证源
export function addAuth(data: AuthParams) {
return MSR.post({ url: AddAuthUrl, data });
}
// 更新认证源
export function updateAuth(data: AuthParams) {
return MSR.post({ url: UpdateAuthUrl, data });
}

View File

@ -5,3 +5,7 @@ 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';
export const UpdateAuthUrl = '/system/authsource/update';
export const GetAuthListUrl = '/system/authsource/list';
export const AddAuthUrl = '/system/authsource/add';
export const GetAuthDetailUrl = '/system/authsource/get';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -1,5 +1,5 @@
<template>
<ColorPicker v-model:pureColor="innerPureColor" :z-index="1" is-widget picker-type="chrome" round-history />
<ColorPicker v-model:pureColor="innerPureColor" :z-index="1" picker-type="chrome" is-widget round-history />
</template>
<script setup lang="ts">
@ -19,6 +19,9 @@
() => props.pureColor,
(val) => {
innerPureColor.value = val;
},
{
immediate: true,
}
);
@ -30,4 +33,18 @@
);
</script>
<style lang="less"></style>
<style lang="less">
.color-cube {
overflow: hidden;
border-radius: var(--border-radius-small);
}
.vc-transparent {
background-image: none !important;
}
.color-list {
width: 100% !important;
}
.color-item:not(:last-child) {
margin-right: 2px !important;
}
</style>

View File

@ -37,6 +37,9 @@
<a-button :disabled="props.okLoading" @click="handleCancel">
{{ t(props.cancelText || 'ms.drawer.cancel') }}
</a-button>
<a-button v-if="showContinue" type="secondary" :loading="props.okLoading" @click="handleContinue">
{{ t(props.saveContinueText || 'ms.drawer.saveContinue') }}
</a-button>
<a-button type="primary" :loading="props.okLoading" @click="handleOk">
{{ t(props.okText || 'ms.drawer.ok') }}
</a-button>
@ -66,6 +69,8 @@
okLoading?: boolean;
okText?: string;
cancelText?: string;
saveContinueText?: string;
showContinue?: boolean;
width: number;
}
@ -73,8 +78,9 @@
footer: true,
mask: true,
showSkeleton: false,
showContinue: false,
});
const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
const emit = defineEmits(['update:visible', 'confirm', 'cancel', 'continue']);
const { t } = useI18n();
@ -87,6 +93,10 @@
}
);
const handleContinue = () => {
emit('continue');
};
const handleOk = () => {
emit('confirm');
};

View File

@ -1,4 +1,5 @@
export default {
'ms.drawer.cancel': 'Cancel',
'ms.drawer.ok': 'Confirm',
'ms.drawer.saveContinue': 'Save & Continue',
};

View File

@ -1,4 +1,5 @@
export default {
'ms.drawer.cancel': '取消',
'ms.drawer.ok': '确认',
'ms.drawer.saveContinue': '保存并继续添加',
};

View File

@ -414,6 +414,7 @@ export default defineComponent({
'jumper-prepend': slots['jumper-prepend'],
'jumper-append': slots['jumper-append'],
}}
simple
disabled={props.disabled}
current={computedCurrent.value}
pages={pages.value}

View File

@ -191,7 +191,7 @@
import TopMenu from '@/components/bussiness/ms-top-menu/index.vue';
import MessageBox from '../message-box/index.vue';
import { NOT_SHOW_PROJECT_SELECT_MODULE } from '@/router/constants';
import { getProjectList } from '@/api/modules/setting/project';
// import { getProjectList } from '@/api/modules/setting/project';
import { useI18n } from '@/hooks/useI18n';
import type { ProjectListItem } from '@/models/setting/project';
@ -210,12 +210,12 @@
const projectList: Ref<ProjectListItem[]> = ref([]);
onBeforeMount(async () => {
try {
const res = await getProjectList(appStore.getCurrentOrgId);
projectList.value = res;
} catch (error) {
console.log(error);
}
// try {
// const res = await getProjectList(appStore.getCurrentOrgId);
// projectList.value = res;
// } catch (error) {
// console.log(error);
// }
});
const showProjectSelect = computed(() => {
@ -279,9 +279,7 @@
<style scoped lang="less">
.navbar {
@apply flex h-full justify-between;
background-color: var(--color-bg-3);
@apply flex h-full justify-between bg-transparent;
}
.left-side {
@apply flex items-center;

View File

@ -11,5 +11,6 @@ export enum TableKeyEnum {
USERGROUPUSER = 'userGroupUser',
SYSTEM_USER = 'systemUser',
SYSTEM_RESOURCEPOOL = 'systemResourcePool',
SYSTEM_AUTH = 'systemResourcePool',
ORGANNATIONMEMBER = 'member',
}

View File

@ -96,8 +96,10 @@
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 innerLogo = computed(() =>
props.isPreview ? innerProps.value.logo : appStore.pageConfig.logoPlatform[0]?.url
);
const innerName = computed(() => (props.isPreview ? innerProps.value.name : appStore.pageConfig.platformName));
const navbarHeight = `56px`;
const navbar = computed(() => appStore.navbar);
@ -175,7 +177,7 @@
}
}
:deep(.arco-menu-light) {
background-color: var(--color-bg-3) !important;
background-color: transparent !important;
.arco-menu-item {
:hover {
background-color: var(--color-bg-6);
@ -200,4 +202,7 @@
min-height: 500px;
}
}
.arco-layout-sider-light {
@apply bg-transparent;
}
</style>

View File

@ -43,7 +43,7 @@ export interface TestEmailParams {
// 界面配置入参
export interface SavePageConfigParams {
fileList: (File | undefined)[];
request: Recordable[];
request: (Recordable | undefined)[];
}
interface FileParamItem extends ParamItem {
@ -54,12 +54,17 @@ interface FileParamItem extends ParamItem {
// 页面配置返回参数
export type PageConfigReturns = FileParamItem[];
// 主题配置对象
// 平台风格
export type Style = 'default' | 'custom' | 'follow';
// 主题
export type Theme = 'default' | 'custom';
// 主题配置对象
export interface ThemeConfig {
style: string;
style: Style;
customStyle: string;
theme: string;
theme: Theme;
customTheme: string;
}
@ -83,3 +88,36 @@ export interface PlatformConfig {
export interface PageConfig extends ThemeConfig, LoginConfig, PlatformConfig {}
export type PageConfigKeys = keyof PageConfig;
// 认证源配置列表项对象
export interface AuthItem {
id: string;
enable: boolean;
createTime: number;
updateTime: number;
description: string;
name: string;
type: string;
configuration: string;
}
// 认证源配置对象
export type AuthConfig = Omit<AuthItem, 'id'>;
// 认证源配置表单对象
export interface AuthForm {
id?: string;
enable: boolean;
description: string;
name: string;
type: string;
configuration: Recordable;
}
// 认证源配置接口入参
export type AuthParams = Omit<AuthForm, 'configuration'> & {
configuration: string;
};
// 认证源配置详情对象
export type AuthDetail = AuthForm & Omit<AuthItem, 'configuration'>;

View File

@ -7,18 +7,19 @@ import { useI18n } from '@/hooks/useI18n';
import { cloneDeep } from 'lodash-es';
import { getPageConfig } from '@/api/modules/setting/config';
import { setFavicon } from '@/utils';
import { watchStyle, watchTheme } from '@/utils/theme';
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';
import type { PageConfig, PageConfigKeys, Style, Theme } from '@/models/setting/config';
const defaultThemeConfig = {
style: 'default',
customStyle: '',
theme: 'default',
customTheme: '',
style: 'default' as Style,
customStyle: '#f9f9fe',
theme: 'default' as Theme,
customTheme: '#811fa3',
};
const defaultLoginConfig = {
title: 'MeterSphere',
@ -211,6 +212,8 @@ const useAppStore = defineStore('app', {
try {
const res = await getPageConfig();
if (Array.isArray(res) && res.length > 0) {
let hasStyleChange = false;
let hasThemeChange = false;
res.forEach((e) => {
const key = e.paramKey.split('ui.')[1] as PageConfigKeys; // 参数名前缀ui.去掉
if (['icon', 'loginLogo', 'loginImage', 'logoPlatform'].includes(key)) {
@ -222,6 +225,19 @@ const useAppStore = defineStore('app', {
},
] as any;
} else {
if (key === 'style') {
// 风格是否更改,先判断自定义风格的值是否相等,再判断非自定义的俩值是否相等
hasStyleChange = !['default', 'follow'].includes(e.paramValue)
? this.pageConfig.customStyle !== e.paramValue
: this.pageConfig.style !== e.paramValue;
}
if (key === 'theme') {
// 主题是否更改,先判断自定义主题的值是否相等,再判断非自定义的俩值是否相等
hasThemeChange =
e.paramValue !== 'default'
? this.pageConfig.customTheme !== e.paramValue
: this.pageConfig.theme !== e.paramValue;
}
this.pageConfig[key] = e.paramValue as any;
}
});
@ -229,16 +245,29 @@ const useAppStore = defineStore('app', {
// 判断是否选择了自定义主题色
this.pageConfig.customTheme = this.pageConfig.theme;
this.pageConfig.theme = 'custom';
} else {
// 非自定义则需要重置自定义主题色为空,避免本地缓存与接口配置不一致
this.pageConfig.customTheme = defaultThemeConfig.customTheme;
}
if (!['default', 'follow'].includes(this.pageConfig.style)) {
// 判断是否选择了自定义平台风格
this.pageConfig.customStyle = this.pageConfig.style;
this.pageConfig.style = 'custom';
} else {
// 非自定义则需要重置自定义风格,避免本地缓存与接口配置不一致
this.pageConfig.customStyle = defaultThemeConfig.customStyle;
}
if (this.pageConfig.icon[0]?.url) {
// 设置网站 favicon
setFavicon(this.pageConfig.icon[0].url);
}
// 如果风格和主题有变化,则初始化一下主题和风格;没有变化则不需要在此初始化,在 App.vue 中初始化过了
if (hasStyleChange) {
watchStyle(this.pageConfig.style, this.pageConfig);
}
if (hasThemeChange) {
watchTheme(this.pageConfig.theme, this.pageConfig);
}
}
} catch (error) {
console.log(error);

View File

@ -1,4 +1,2 @@
.theme-defalut {
--primary-6: #811fa3;
--primary-7: #6e1a8b;
}
@primary-6: #811fa3;
@primary-7: #6e1a8b;

View File

@ -4,6 +4,11 @@ export interface ScrollToViewOptions {
inline?: 'start' | 'center' | 'end' | 'nearest';
}
/**
*
* @param targetRef ref DOM
* @param options
*/
export function scrollIntoView(targetRef: HTMLElement | Element | null, options: ScrollToViewOptions = {}) {
const scrollOptions: ScrollToViewOptions = {
behavior: options.behavior || 'smooth',

134
frontend/src/utils/theme.ts Normal file
View File

@ -0,0 +1,134 @@
import Color from 'color';
import type { PageConfig, Style, Theme } from '@/models/setting/config';
/**
* rgb
* @param color Color对象
* @returns
*/
export function getRGBinnerVal(color: Color) {
return color
.rgb()
.toString()
.replace(/rgba?\(|\)/g, '');
}
/**
*
* @param primaryColor
*/
export function setCustomTheme(primaryColor: string) {
const styleTag = document.createElement('style');
styleTag.id = 'MS-CUSTOM-THEME';
const primary = new Color(primaryColor);
const white = Color('#fff');
const P = primary.toString().replace(/rgba?\(|\)/g, '');
const P1 = getRGBinnerVal(primary.mix(white, 0.95));
const P2 = getRGBinnerVal(primary.mix(white, 0.8));
const P3 = getRGBinnerVal(primary.mix(white, 0.7));
const P4 = getRGBinnerVal(primary.mix(white, 0.15));
const P7 = getRGBinnerVal(primary.mix(Color('#000'), 0.15));
const P9 = getRGBinnerVal(primary.mix(white, 0.9));
styleTag.innerHTML = `
body{
--primary-1: ${P1};
--primary-2: ${P2};
--primary-3: ${P3};
--primary-4: ${P4};
--primary-5: ${P};
--primary-6: ${P};
--primary-7: ${P7};
--primary-9: ${P9};
}
`;
// 移除之前的 style 标签(如果有)
const prevStyleTag = document.getElementById('MS-CUSTOM-THEME');
if (prevStyleTag) {
prevStyleTag.remove();
}
document.body.appendChild(styleTag);
}
/**
*
*/
export function resetTheme() {
const prevStyleTag = document.getElementById('MS-CUSTOM-THEME');
if (prevStyleTag) {
prevStyleTag.remove();
}
}
/**
*
* @param color
*/
export function setPlatformColor(color: string, isFollow = false) {
const styleTag = document.createElement('style');
styleTag.id = 'MS-CUSTOM-STYLE';
const white = Color('#fff');
// 跟随主题色设置为P1
const platformColor = isFollow ? new Color(color).mix(white, 0.95) : new Color(color);
styleTag.innerHTML = `
body{
--color-bg-3: ${platformColor};
--color-text-n9: ${platformColor};
}
`;
// 移除之前的 style 标签(如果有)
const prevStyleTag = document.getElementById('MS-CUSTOM-STYLE');
if (prevStyleTag) {
prevStyleTag.remove();
}
document.body.appendChild(styleTag);
}
/**
*
*/
export function resetStyle() {
const prevStyleTag = document.getElementById('MS-CUSTOM-STYLE');
if (prevStyleTag) {
prevStyleTag.remove();
}
}
/**
*
* @param val
* @param pageConfig
*/
export function watchStyle(val: Style, pageConfig: PageConfig) {
if (val === 'default') {
// 默认就是系统自带的颜色
resetStyle();
} else if (val === 'custom') {
// 自定义风格颜色
setPlatformColor(pageConfig.customStyle);
} else {
// 跟随主题色
setPlatformColor(pageConfig.customTheme, true);
}
}
/**
*
* @param val
* @param pageConfig
*/
export function watchTheme(val: Theme, pageConfig: PageConfig) {
if (val === 'default') {
resetTheme();
if (pageConfig.style === 'follow') {
// 若平台风格跟随主题色
resetStyle();
}
} else {
setCustomTheme(pageConfig.customTheme);
if (pageConfig.style === 'follow') {
// 若平台风格跟随主题色
setPlatformColor(pageConfig.customTheme, true);
}
}
}

View File

@ -7,7 +7,6 @@
<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;
@ -16,6 +15,7 @@
const appStore = useAppStore();
const defaultBanner = `${import.meta.env.BASE_URL}images/login-banner.jpg`;
const innerBanner = computed(() => {
return props.banner || appStore.pageConfig.loginImage[0]?.url || defaultBanner;
});

View File

@ -76,11 +76,11 @@
}>();
const innerLogo = computed(() => {
return props.logo || appStore.pageConfig.loginLogo[0]?.url;
return props.isPreview ? props.logo : appStore.pageConfig.loginLogo[0]?.url;
});
const innerSlogan = computed(() => {
return props.slogan || appStore.pageConfig.slogan;
return props.isPreview ? props.slogan : appStore.pageConfig.slogan;
});
const errorMessage = ref('');

View File

@ -0,0 +1,809 @@
<template>
<div>
<MsCard :loading="loading" simple>
<div class="mb-4 flex items-center justify-between">
<a-button type="primary" @click="createAuth">
{{ t('system.config.auth.add') }}
</a-button>
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<template #name="{ record }">
<a-button type="text" @click="openAuthDetail(record)">{{ record.name }}</a-button>
</template>
<template #enable="{ record }">
<div v-if="record.enable" class="flex items-center">
<icon-check-circle-fill class="mr-[2px] text-[rgb(var(--success-6))]" />
{{ t('system.config.auth.enable') }}
</div>
<div v-else class="flex items-center text-[var(--color-text-4)]">
<icon-stop class="mr-[2px]" />
{{ t('system.config.auth.disable') }}
</div>
</template>
<template #action="{ record }">
<MsButton @click="editAuth(record)">{{ t('system.config.auth.edit') }}</MsButton>
<MsButton v-if="record.enable" @click="disabledAuth(record)">
{{ t('system.config.auth.disable') }}
</MsButton>
<MsButton v-else @click="enableAuth(record)">{{ t('system.config.auth.enable') }}</MsButton>
<MsTableMoreAction :list="tableActions" @select="handleSelect($event, record)"></MsTableMoreAction>
</template>
</ms-base-table>
</MsCard>
<MsDrawer
v-model:visible="showDetailDrawer"
:width="480"
:title="activeAuthDetail.name"
:title-tag="activeAuthDetail.enable ? t('system.config.auth.enable') : t('system.config.auth.disable')"
:title-tag-color="activeAuthDetail.enable ? 'green' : 'gray'"
:descriptions="activeAuthDesc"
:footer="false"
:mask="false"
:show-skeleton="detailDrawerLoading"
show-description
>
<template #tbutton>
<a-button type="outline" size="mini" :disabled="detailDrawerLoading" @click="editAuth(activeAuthDetail)">
{{ t('system.config.auth.edit') }}
</a-button>
</template>
</MsDrawer>
<MsDrawer
v-model:visible="showDrawer"
:title="t(isEdit ? 'system.config.auth.updateTitle' : 'system.config.auth.add')"
:ok-text="t(isEdit ? 'system.config.auth.update' : 'system.config.auth.drawerAdd')"
:ok-loading="drawerLoading"
:width="680"
show-continue
@confirm="handleDrawerConfirm"
@continue="handleDrawerConfirm(true)"
@cancel="handleDrawerCancel"
>
<a-form ref="authFormRef" :model="activeAuthForm" layout="vertical">
<a-form-item
:label="t('system.config.auth.name')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.nameRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.name"
:max-length="250"
:placeholder="t('system.config.auth.namePlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item :label="t('system.config.auth.desc')" field="url" asterisk-position="end">
<a-textarea
v-model:model-value="activeAuthForm.description"
:max-length="250"
:placeholder="t('system.config.auth.descPlaceholder')"
allow-clear
></a-textarea>
</a-form-item>
<a-form-item :label="t('system.config.auth.addResource')" field="url" asterisk-position="end">
<a-radio-group v-model:model-value="activeAuthForm.type" type="button">
<a-radio v-for="item of authTypeList" :key="item" :value="item">{{ item }}</a-radio>
</a-radio-group>
</a-form-item>
<template v-if="activeAuthForm.type === 'CAS'">
<a-form-item
:label="t('system.config.auth.serviceUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.serviceUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.serviceUrl"
:max-length="250"
:placeholder="t('system.config.auth.serviceUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.loginUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.loginUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.loginUrl"
:max-length="250"
:placeholder="t('system.config.auth.loginUrlPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.loginUrlTip')" :show-fill-icon="false" />
</a-form-item>
<a-form-item
:label="t('system.config.auth.callbackUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.callbackUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.callbackUrl"
:max-length="250"
:placeholder="t('system.config.auth.callbackUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.verifyUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.verifyUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.verifyUrl"
:max-length="250"
:placeholder="t('system.config.auth.verifyUrlPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.verifyUrlTip')" :show-fill-icon="false" />
</a-form-item>
</template>
<template v-else-if="activeAuthForm.type === 'OIDC'">
<a-form-item
:label="t('system.config.auth.authUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.authUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.authUrl"
:max-length="250"
:placeholder="t('system.config.auth.authUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.tokenUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.tokenUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.tokenUrl"
:max-length="250"
:placeholder="t('system.config.auth.tokenUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.userInfoUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.userInfoUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.userInfoUrl"
:max-length="250"
:placeholder="t('system.config.auth.userInfoUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.callbackUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.callbackUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.callbackUrl"
:max-length="250"
:placeholder="t('system.config.auth.OIDCCallbackUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.clientId')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.clientIdRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.clientId"
:max-length="250"
:placeholder="t('system.config.auth.clientIdPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.clientSecret')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.clientSecretRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.clientSecret"
:max-length="250"
:placeholder="t('system.config.auth.clientSecretPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.logoutSessionUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.logoutSessionUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.logoutSessionUrl"
:max-length="250"
:placeholder="t('system.config.auth.logoutSessionUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item :label="t('system.config.auth.loginUrl')" field="url" asterisk-position="end">
<a-input
v-model:model-value="activeAuthForm.configuration.loginUrl"
:max-length="250"
:placeholder="t('system.config.auth.loginUrlPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.loginUrlTip')" :show-fill-icon="false" />
</a-form-item>
</template>
<template v-else-if="activeAuthForm.type === 'OAuth2'">
<a-form-item
:label="t('system.config.auth.authUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.authUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.authUrl"
:max-length="250"
:placeholder="t('system.config.auth.authUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.tokenUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.tokenUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.tokenUrl"
:max-length="250"
:placeholder="t('system.config.auth.tokenUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.userInfoUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.userInfoUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.userInfoUrl"
:max-length="250"
:placeholder="t('system.config.auth.userInfoUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.callbackUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.callbackUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.callbackUrl"
:max-length="250"
:placeholder="t('system.config.auth.OIDCCallbackUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.clientId')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.clientIdRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.clientId"
:max-length="250"
:placeholder="t('system.config.auth.clientIdPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.clientSecret')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.clientSecretRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.clientSecret"
:max-length="250"
:placeholder="t('system.config.auth.clientSecretPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.propertyMap')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.propertyMapRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.propertyMap"
:max-length="250"
:placeholder="t('system.config.auth.propertyMapPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item :label="t('system.config.auth.logoutSessionUrl')" field="url" asterisk-position="end">
<a-input
v-model:model-value="activeAuthForm.configuration.logoutSessionUrl"
:max-length="250"
:placeholder="t('system.config.auth.logoutSessionUrlPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item :label="t('system.config.auth.linkRange')" field="url" asterisk-position="end">
<a-input
v-model:model-value="activeAuthForm.configuration.linkRange"
:max-length="250"
:placeholder="t('system.config.auth.linkRangePlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item :label="t('system.config.auth.loginUrl')" field="url" asterisk-position="end">
<a-input
v-model:model-value="activeAuthForm.configuration.loginUrl"
:max-length="250"
:placeholder="t('system.config.auth.loginUrlPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.loginUrlTip')" :show-fill-icon="false" />
</a-form-item>
</template>
<template v-else-if="activeAuthForm.type === 'LDAP'">
<a-form-item
:label="t('system.config.auth.LDAPUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.LDAPUrlRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.LDAPUrl"
:max-length="250"
:placeholder="t('system.config.auth.LDAPUrlPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.LDAPUrlTip')" :show-fill-icon="false" />
</a-form-item>
<a-form-item
:label="t('system.config.auth.DN')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.DNRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.DN"
:max-length="250"
:placeholder="t('system.config.auth.DNPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.password')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.passwordRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.password"
:max-length="250"
:placeholder="t('system.config.auth.passwordPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.auth.OU')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.OURequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.OU"
:max-length="250"
:placeholder="t('system.config.auth.OUPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.OUTip')" :show-fill-icon="false" />
</a-form-item>
<a-form-item
:label="t('system.config.auth.userFilter')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.userFilterRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.userFilter"
:max-length="250"
:placeholder="t('system.config.auth.userFilterPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.userFilterTip')" :show-fill-icon="false" />
</a-form-item>
<a-form-item
:label="t('system.config.auth.LDAPPropertyMap')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.auth.LDAPPropertyMapRequired') }]"
required
>
<a-input
v-model:model-value="activeAuthForm.configuration.LDAPPropertyMap"
:max-length="250"
:placeholder="t('system.config.auth.LDAPPropertyMapPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.auth.LDAPPropertyMapTip')" :show-fill-icon="false" />
</a-form-item>
<div>
<a-button type="outline" class="mr-[16px]" @click="testLink">
{{ t('system.config.auth.testLink') }}
</a-button>
<a-button type="outline" @click="testLogin">{{ t('system.config.auth.testLogin') }}</a-button>
</div>
</template>
</a-form>
</MsDrawer>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import { useTableStore } from '@/store';
import { TableKeyEnum } from '@/enums/tableEnum';
import useTable from '@/components/pure/ms-table/useTable';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import useModal from '@/hooks/useModal';
import { getAuthList, getAuthDetail, addAuth, updateAuth } from '@/api/modules/setting/config';
import MsFormItemSub from '@/components/bussiness/ms-form-item-sub/index.vue';
import { scrollIntoView } from '@/utils/dom';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import type { AuthDetail, AuthForm, AuthItem } from '@/models/setting/config';
const { t } = useI18n();
const { openModal } = useModal();
const loading = ref(false);
const tableActions: ActionsItem[] = [
{
label: 'system.config.auth.delete',
eventTag: 'delete',
danger: true,
},
];
const columns: MsTableColumn = [
{
title: 'system.config.auth.name',
slotName: 'name',
dataIndex: 'name',
width: 200,
showInTable: true,
},
{
title: 'system.config.auth.status',
slotName: 'enable',
dataIndex: 'enable',
showInTable: true,
},
{
title: 'system.config.auth.desc',
dataIndex: 'description',
showInTable: true,
},
{
title: 'system.config.auth.createTime',
dataIndex: 'createTime',
showInTable: true,
},
{
title: 'system.config.auth.updateTime',
dataIndex: 'updateTime',
showInTable: true,
},
{
title: 'system.config.auth.action',
slotName: 'action',
fixed: 'right',
width: 120,
showInTable: true,
},
];
const tableStore = useTableStore();
tableStore.initColumn(TableKeyEnum.SYSTEM_AUTH, columns, 'drawer');
const { propsRes, propsEvent, loadList } = useTable(getAuthList, {
tableKey: TableKeyEnum.SYSTEM_AUTH,
columns,
scroll: { y: 'auto' },
selectable: false,
showSelectAll: false,
});
onBeforeMount(() => {
loadList();
});
const enableLoading = ref(false);
/**
* 启用认证源
*/
async function enableAuth(record: any) {
openModal({
type: 'warning',
title: t('system.config.auth.enableTipTitle', { name: record.name }),
content: t('system.config.auth.enableTipContent'),
okText: t('system.config.auth.enableConfirm'),
cancelText: t('system.config.auth.cancel'),
cancelButtonProps: {
disabled: enableLoading.value,
},
okLoading: enableLoading.value,
maskClosable: false,
onBeforeOk: async () => {
try {
enableLoading.value = true;
// await togglePoolStatus(record.id);
Message.success(t('system.config.auth.enableSuccess'));
loadList();
return true;
} catch (error) {
console.log(error);
return false;
} finally {
enableLoading.value = false;
}
},
hideCancel: false,
});
}
const disableLoading = ref(false);
/**
* 禁用认证源
*/
function disabledAuth(record: any) {
openModal({
type: 'warning',
title: t('system.config.auth.disableTipTitle', { name: record.name }),
content: t('system.config.auth.disableTipContent'),
okText: t('system.config.auth.disableConfirm'),
cancelText: t('system.config.auth.cancel'),
cancelButtonProps: {
disabled: disableLoading.value,
},
okLoading: disableLoading.value,
maskClosable: false,
onBeforeOk: async () => {
try {
disableLoading.value = true;
// await togglePoolStatus(record.id);
Message.success(t('system.config.auth.disableSuccess'));
loadList();
return true;
} catch (error) {
console.log(error);
return false;
} finally {
disableLoading.value = false;
}
},
hideCancel: false,
});
}
const delLoading = ref(false);
/**
* 删除认证源
*/
function deleteAuth(record: any) {
openModal({
type: 'warning',
title: t('system.config.auth.deleteTipTitle', { name: record.name }),
content: t('system.config.auth.deleteTipContent'),
okText: t('system.config.auth.deleteConfirm'),
cancelText: t('system.config.auth.cancel'),
okButtonProps: {
status: 'danger',
},
cancelButtonProps: {
disabled: delLoading.value,
},
maskClosable: false,
okLoading: delLoading.value,
onBeforeOk: async () => {
try {
delLoading.value = true;
// await delPoolInfo(record.id);
Message.success(t('system.config.auth.deleteSuccess'));
loadList();
return true;
} catch (error) {
console.log(error);
return false;
} finally {
delLoading.value = false;
}
},
hideCancel: false,
});
}
/**
* 处理表格更多按钮事件
* @param item
*/
function handleSelect(item: ActionsItem, record: any) {
switch (item.eventTag) {
case 'delete':
deleteAuth(record);
break;
default:
break;
}
}
const showDetailDrawer = ref(false);
const detailDrawerLoading = ref(false);
const activeAuthDesc = ref([]);
const activeAuthDetail = ref<AuthDetail>({
id: '',
enable: true,
description: '',
name: '',
type: '',
updateTime: 0,
createTime: 0,
configuration: {},
});
/**
* 查看认证源
* @param record 表格项
*/
async function openAuthDetail(record: AuthItem) {
try {
showDetailDrawer.value = true;
detailDrawerLoading.value = true;
const res = await getAuthDetail(record.id);
activeAuthDetail.value = { ...res, configuration: JSON.parse(res.configuration || '{}') };
} catch (error) {
console.log(error);
} finally {
detailDrawerLoading.value = false;
}
}
const drawerTitle = ref('');
const showDrawer = ref(false);
const drawerLoading = ref(false);
const authFormRef = ref<FormInstance>();
const authTypeList = ['CAS', 'OIDC', 'OAuth2', 'LDAP'];
const defaultAuth = {
id: '',
enable: true,
description: '',
name: '',
type: 'CAS',
configuration: {},
};
const activeAuthForm = ref<AuthForm>({
...defaultAuth,
});
const isEdit = computed(() => !!activeAuthForm.value.id);
/**
* 编辑认证源
* @param record 表格项
*/
function editAuth(record: AuthItem | AuthDetail) {
drawerTitle.value = t('system.config.auth.update');
showDrawer.value = true;
activeAuthForm.value = {
...record,
configuration:
typeof record.configuration === 'string' ? JSON.parse(record.configuration || '{}') : record.configuration,
};
}
/**
* 添加认证源
*/
function createAuth() {
drawerTitle.value = t('system.config.auth.add');
showDrawer.value = true;
activeAuthForm.value = { ...defaultAuth };
}
async function testLink() {}
async function testLogin() {}
/**
* 保存认证信息
* @param isContinue 是否继续添加
*/
async function saveAuth(isContinue: boolean) {
try {
drawerLoading.value = true;
const params = {
...activeAuthForm.value,
configuration: JSON.stringify(activeAuthForm.value.configuration),
};
if (isEdit.value) {
await updateAuth(params);
Message.success(t('system.config.auth.updateSuccess'));
} else {
await addAuth(params);
Message.success(t('system.config.auth.addSuccess'));
if (isContinue) {
authFormRef.value?.resetFields();
} else {
showDrawer.value = false;
}
}
} catch (error) {
console.log(error);
} finally {
drawerLoading.value = false;
}
}
function handleDrawerConfirm(isContinue: boolean) {
authFormRef.value?.validate(async (errors: Record<string, ValidatedError> | undefined) => {
if (!errors) {
saveAuth(isContinue);
} else {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
}
});
}
function handleDrawerCancel() {
showDrawer.value = false;
authFormRef.value?.resetFields();
}
</script>
<style lang="less" scoped></style>

View File

@ -3,20 +3,6 @@
<!-- 风格主题色配置 -->
<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))]" />
@ -27,8 +13,22 @@
{{ 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 v-if="pageConfig.theme === 'custom'" class="ml-[4px]">
<MsColorSelect key="customTheme" v-model:pure-color="pageConfig.customTheme" />
</div>
<div class="config-title mt-[16px]">
{{ 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="mb-[4px] ml-[4px]">
<MsColorSelect key="customStyle" v-model:pure-color="pageConfig.customStyle" />
</div>
</MsCard>
<!-- 登录页配置 -->
@ -60,7 +60,7 @@
</div>
<!-- 登录页预览实际渲染 DOM按三种屏幕尺寸缩放 -->
<div :class="['page-preview', isLoginPageFullscreen ? 'full-preview' : 'normal-preview']">
<banner :banner="pageConfig.loginImage[0]?.url" is-preview />
<banner :banner="pageConfig.loginImage[0]?.url || defaultBanner" is-preview />
<loginForm :slogan="pageConfig.slogan" :logo="pageConfig.loginLogo[0]?.url" is-preview />
</div>
</div>
@ -195,12 +195,12 @@
</div>
<!-- 平台主页预览盒子 -->
<div class="config-preview !h-[290px]">
<div ref="loginPageFullRef" class="login-preview">
<div ref="platformPageFullRef" 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"
class="absolute right-[18px] top-[16px] z-[999] w-[96px] cursor-pointer text-right !text-[var(--color-text-4)]"
@click="platformFullscreenToggle"
>
<MsIcon v-if="isLoginPageFullscreen" type="icon-icon_off_screen" />
<MsIcon v-if="isPlatformPageFullscreen" type="icon-icon_off_screen" />
<MsIcon v-else type="icon-icon_full_screen_one" />
</div>
<!-- 平台主页预览实际渲染 DOM按三种屏幕尺寸缩放 -->
@ -209,7 +209,7 @@
'page-preview',
'platform-preview',
'!h-[550px]',
isLoginPageFullscreen ? 'full-preview' : 'normal-preview',
isPlatformPageFullscreen ? 'full-preview' : 'normal-preview',
]"
>
<defaultLayout
@ -298,7 +298,7 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { computed, ref, watch, onBeforeUnmount } from 'vue';
import { useFullscreen } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
@ -312,10 +312,12 @@
import MsUpload from '@/components/pure/ms-upload/index.vue';
import defaultLayout from '@/layout/default-layout.vue';
import { scrollIntoView } from '@/utils/dom';
import { setCustomTheme, setPlatformColor, watchStyle, watchTheme } from '@/utils/theme';
import { savePageConfig } from '@/api/modules/setting/config';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
const defaultBanner = `${import.meta.env.BASE_URL}images/login-banner.jpg`;
const { t } = useI18n();
const appStore = useAppStore();
const collapsedWidth = 86;
@ -325,7 +327,10 @@
const pageloading = ref(false);
const pageConfig = ref({ ...appStore.pageConfig });
const loginPageFullRef = ref<HTMLElement | null>(null);
const platformPageFullRef = ref<HTMLElement | null>(null);
const { isFullscreen: isLoginPageFullscreen, toggle: loginFullscreenToggle } = useFullscreen(loginPageFullRef);
const { isFullscreen: isPlatformPageFullscreen, toggle: platformFullscreenToggle } =
useFullscreen(platformPageFullRef);
const loginConfigFormRef = ref<FormInstance>();
const platformConfigFormRef = ref<FormInstance>();
@ -355,6 +360,42 @@
},
];
watch(
() => pageConfig.value.style,
(val) => {
watchStyle(val, pageConfig.value);
}
);
watch(
() => pageConfig.value.customStyle,
(val) => {
if (val && pageConfig.value.style === 'custom') {
setPlatformColor(val);
}
}
);
watch(
() => pageConfig.value.theme,
(val) => {
watchTheme(val, pageConfig.value);
}
);
watch(
() => pageConfig.value.customTheme,
(val) => {
if (val && pageConfig.value.theme === 'custom') {
setCustomTheme(val);
if (pageConfig.value.style === 'follow') {
//
setPlatformColor(pageConfig.value.customTheme, true);
}
}
}
);
function resetLoginPageConfig() {
pageConfig.value = {
...pageConfig.value,
@ -383,51 +424,72 @@
paramValue: pageConfig.value.icon[0]?.url,
type: 'file',
fileName: pageConfig.value.icon[0]?.name,
isDefault: pageConfig.value.icon.length === 0, //
hasFile: pageConfig.value.icon[0]?.file, //
},
{
paramKey: 'ui.loginLogo',
paramValue: pageConfig.value.loginLogo[0]?.url,
type: 'file',
fileName: pageConfig.value.loginLogo[0]?.name,
isDefault: pageConfig.value.loginLogo.length === 0,
hasFile: pageConfig.value.loginLogo[0]?.file,
},
{
paramKey: 'ui.loginImage',
paramValue: pageConfig.value.loginImage[0]?.url,
type: 'file',
fileName: pageConfig.value.loginImage[0]?.name,
isDefault: pageConfig.value.loginImage.length === 0,
hasFile: pageConfig.value.loginImage[0]?.file,
},
{
paramKey: 'ui.logoPlatform',
paramValue: pageConfig.value.logoPlatform[0]?.url,
type: 'file',
fileName: pageConfig.value.logoPlatform[0]?.name,
isDefault: pageConfig.value.logoPlatform.length === 0,
hasFile: pageConfig.value.logoPlatform[0]?.file,
},
{ 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.style',
paramValue: pageConfig.value.style === 'custom' ? pageConfig.value.customStyle : pageConfig.value.style,
type: 'text',
},
{
paramKey: 'ui.theme',
paramValue: pageConfig.value.theme === 'custom' ? 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' },
];
].filter((e) => {
if (e.type === 'file') {
return e.hasFile || e.isDefault;
}
return true;
});
const fileList = [
pageConfig.value.icon[0].file
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
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
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
pageConfig.value.logoPlatform[0]?.file
? new File(
[pageConfig.value.logoPlatform[0].file as File],
[pageConfig.value.logoPlatform[0]?.file as File],
`ui.logoPlatform,${pageConfig.value.logoPlatform[0].file?.name}`
)
: undefined,
@ -435,6 +497,8 @@
return { request, fileList };
}
const isSave = ref(false); //
/**
* 保存并应用
*/
@ -444,6 +508,7 @@
await savePageConfig(makeParams());
Message.success(t('system.config.page.saveSuccess'));
appStore.initPageConfig(); //
isSave.value = true;
} catch (error) {
console.log(error);
} finally {
@ -470,9 +535,27 @@
} catch (error) {
console.log(error);
}
const errDom = document.querySelector('.arco-input-error');
const errDom = document.querySelector('.arco-form-item-message');
scrollIntoView(errDom, { block: 'center' });
}
onBeforeUnmount(() => {
if (isSave.value === false) {
//
if (
pageConfig.value.style !== appStore.pageConfig.style &&
pageConfig.value.customStyle !== appStore.pageConfig.style
) {
watchStyle(appStore.pageConfig.style, appStore.pageConfig);
}
if (
pageConfig.value.theme !== appStore.pageConfig.theme &&
pageConfig.value.customTheme !== appStore.pageConfig.theme
) {
watchTheme(appStore.pageConfig.theme, appStore.pageConfig);
}
}
});
</script>
<style lang="less" scoped>
@ -512,7 +595,8 @@
width: 100vw;
}
.login-preview {
position: relative;
@apply relative bg-white;
width: 740px;
@media screen and (min-width: 1600px) {
width: 882px;
@ -532,7 +616,7 @@
.page-preview {
@apply relative flex flex-1;
width: 1470px;
width: 1480px;
height: 916px;
transform-origin: center;
@media screen and (min-width: 1800px) {
@ -547,6 +631,7 @@
}
.full-preview {
width: 100vw;
height: 100vh !important;
transform: none;
}
}

View File

@ -14,19 +14,34 @@
</a-tabs>
</MsCard>
<baseConfig v-show="activeTab === 'baseConfig'" />
<pageConfig v-show="activeTab === 'pageConfig'" />
<pageConfig v-if="isInitedPageConfig" v-show="activeTab === 'pageConfig'" />
<authConfig v-if="isInitedAuthConfig" v-show="activeTab === 'authConfig'" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ref, watch } from 'vue';
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';
import authConfig from './components/authConfig.vue';
const { t } = useI18n();
const activeTab = ref('pageConfig');
const activeTab = ref('authConfig');
const isInitedPageConfig = ref(activeTab.value === 'pageConfig');
const isInitedAuthConfig = ref(activeTab.value === 'authConfig');
watch(
() => activeTab.value,
(val) => {
if (val === 'pageConfig' && !isInitedPageConfig.value) {
isInitedPageConfig.value = true;
} else if (val === 'authConfig' && !isInitedAuthConfig.value) {
isInitedAuthConfig.value = true;
}
}
);
</script>
<style lang="less" scoped>

View File

@ -90,4 +90,107 @@ export default {
'system.config.page.save': '保存并应用',
'system.config.page.unsave': '未保存',
'system.config.page.saveSuccess': '保存成功',
'system.config.auth.add': '添加认证',
'system.config.auth.enable': '启用',
'system.config.auth.enableSuccess': '启用成功',
'system.config.auth.enableTipTitle': '启用认证 {name}',
'system.config.auth.enableTipContent': '启用后可通过该认证方式登录',
'system.config.auth.enableConfirm': '确认启用',
'system.config.auth.disable': '禁用',
'system.config.auth.disableSuccess': '禁用成功',
'system.config.auth.disableTipTitle': '禁用认证 {name}',
'system.config.auth.disableTipContent': '禁用后不支持该认证方式登录',
'system.config.auth.disableConfirm': '确认禁用',
'system.config.auth.updateSuccess': '更新成功',
'system.config.auth.addSuccess': '添加成功',
'system.config.auth.cancel': '取消',
'system.config.auth.edit': '编辑',
'system.config.auth.name': '名称',
'system.config.auth.status': '状态',
'system.config.auth.desc': '描述',
'system.config.auth.createTime': '创建时间',
'system.config.auth.updateTime': '更新时间',
'system.config.auth.action': '操作',
'system.config.auth.delete': '删除',
'system.config.auth.deleteTipTitle': '确认删除 {name} 这个认证吗?',
'system.config.auth.deleteTipContent': '删除后不可恢复,请谨慎操作!',
'system.config.auth.deleteConfirm': '确认删除',
'system.config.auth.deleteSuccess': '删除成功',
'system.config.auth.updateTitle': '更新认证',
'system.config.auth.drawerAdd': '添加',
'system.config.auth.update': '更新',
'system.config.auth.nameRequired': '认证源名称不能为空',
'system.config.auth.namePlaceholder': '请输入认证源名称',
'system.config.auth.descPlaceholder': '请对该认证源进行描述',
'system.config.auth.addResource': '添加资源',
'system.config.auth.serviceUrl': '服务端地址',
'system.config.auth.serviceUrlRequired': '服务端地址不能为空',
'system.config.auth.serviceUrlPlaceholder': '例如http://<casurl>',
'system.config.auth.loginUrl': '登录地址',
'system.config.auth.loginUrlRequired': '登录地址不能为空',
'system.config.auth.loginUrlPlaceholder': '例如http://<casurl>/login',
'system.config.auth.loginUrlTip': '当身份认证失败,会重新定向到该登录页',
'system.config.auth.verifyUrl': '验证地址',
'system.config.auth.verifyUrlRequired': '验证地址不能为空',
'system.config.auth.verifyUrlPlaceholder': '例如http://<casurl>/serviceValidate',
'system.config.auth.verifyUrlTip': '用于验证登录的信息是否正确',
'system.config.auth.callbackUrl': '回调地址',
'system.config.auth.callbackUrlRequired': '回调地址不能为空',
// eslint-disable-next-line no-template-curly-in-string
'system.config.auth.callbackUrlPlaceholder': '例如http://<meteresphere-endpoint>/sso/callback/cas/suthld',
'system.config.auth.OIDCCallbackUrlPlaceholder':
// eslint-disable-next-line no-template-curly-in-string
'例如http://<metersphere-endpoint>/sso/callback or http://<metersphere-endpoint>/sso/callback/authld',
'system.config.auth.authUrl': '授权端地址',
'system.config.auth.authUrlRequired': '授权端地址不能为空',
'system.config.auth.authUrlPlaceholder':
'例如http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/auth',
'system.config.auth.tokenUrl': 'Token 端点地址',
'system.config.auth.tokenUrlRequired': 'Token 端点地址不能为空',
'system.config.auth.tokenUrlPlaceholder':
'例如http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/token',
'system.config.auth.userInfoUrl': '用户信息端点地址',
'system.config.auth.userInfoUrlRequired': '用户信息端点地址不能为空',
'system.config.auth.userInfoUrlPlaceholder':
'例如http://<keyclock>auth/realms/<metersphere>/protocol/openid-connect/userinfo',
'system.config.auth.clientId': '客户端 ID',
'system.config.auth.clientIdRequired': '客户端 ID不能为空',
'system.config.auth.clientIdPlaceholder': '例如metersphere',
'system.config.auth.clientSecret': '客户端密钥',
'system.config.auth.clientSecretRequired': '客户端密钥不能为空',
'system.config.auth.clientSecretPlaceholder': 'OIDC client secret',
'system.config.auth.logoutSessionUrl': '注销会话端地址',
'system.config.auth.logoutSessionUrlRequired': '注销会话端地址不能为空',
'system.config.auth.logoutSessionUrlPlaceholder':
'例如http://<keyclock>/auth/realms/<metersphere>/protocol/openid-connect/logout',
'system.config.auth.password': '密码',
'system.config.auth.passwordRequired': '密码不能为空',
'system.config.auth.passwordPlaceholder': 'OIDC client secret',
'system.config.auth.LDAPPasswordPlaceholder': '请输入',
'system.config.auth.linkRange': '连接范围',
'system.config.auth.linkRangePlaceholder': 'openid profile email',
'system.config.auth.propertyMap': '属性映射',
'system.config.auth.propertyMapRequired': '属性映射不能为空',
'system.config.auth.propertyMapPlaceholder': '{"userid":"login","username":"name","email":"email"}',
'system.config.auth.LDAPUrl': 'LDAP 地址',
'system.config.auth.LDAPUrlRequired': 'LDAP 地址不能为空',
'system.config.auth.LDAPUrlPlaceholder': '请输入',
'system.config.auth.LDAPUrlTip': '设置服务端地址',
'system.config.auth.DN': '绑定 DN',
'system.config.auth.DNRequired': 'DN 不能为空',
'system.config.auth.DNPlaceholder': '请输入',
'system.config.auth.OU': '用户 OU',
'system.config.auth.OURequired': 'OU 不能为空',
'system.config.auth.OUPlaceholder': '请输入',
'system.config.auth.OUTip': '多个 OU 用 "I" 分隔',
'system.config.auth.userFilter': '用户过滤器',
'system.config.auth.userFilterRequired': '用户过滤器不能为空',
'system.config.auth.userFilterPlaceholder': '请输入',
'system.config.auth.userFilterTip': 'cn or uid or sAMAccountName=%(user)s',
'system.config.auth.LDAPPropertyMap': 'LDAP 属性映射',
'system.config.auth.LDAPPropertyMapRequired': 'LDAP 属性映射不能为空',
'system.config.auth.LDAPPropertyMapPlaceholder': '请输入',
'system.config.auth.LDAPPropertyMapTip': '左侧键为 MeterSphere 用户属性,右侧值为认证平台用户属性',
'system.config.auth.testLink': '测试连接',
'system.config.auth.testLogin': '测试登录',
};