feat(系统设置): 参数设置-基础设置页面&部分组件调整

This commit is contained in:
baiqi 2023-07-28 16:34:32 +08:00 committed by rubylliu
parent dd89cf0905
commit 411e2d1e5d
23 changed files with 902 additions and 94 deletions

View File

@ -6,11 +6,13 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, onBeforeMount } from 'vue';
import enUS from '@arco-design/web-vue/es/locale/lang/en-us';
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
import GlobalSetting from '@/components/pure/global-setting/index.vue';
import useLocale from '@/locale/useLocale';
import { saveBaseInfo, getBaseInfo } from '@/api/modules/setting/config';
import { getLocalStorage, setLocalStorage } from '@/utils/local-storage';
const { currentLocale } = useLocale();
const locale = computed(() => {
@ -23,4 +25,25 @@
return zhCN;
}
});
// url url
onBeforeMount(async () => {
try {
const isInitUrl = getLocalStorage('isInitUrl'); // url
if (isInitUrl === 'true') return;
const res = await getBaseInfo();
if (res.url === 'http://127.0.0.1:8081') {
await saveBaseInfo([
{
paramKey: 'base.url',
paramValue: window.location.origin,
type: 'string',
},
]);
setLocalStorage('isInitUrl', 'true'); // url
}
} catch (error) {
console.log(error);
}
});
</script>

View File

@ -0,0 +1,35 @@
import MSR from '@/api/http/index';
import {
TestEmailUrl,
SaveBaseInfoUrl,
SaveEmailInfoUrl,
GetBaseInfoUrl,
GetEmailInfoUrl,
} from '@/api/requrls/setting/config';
import type { SaveInfoParams, TestEmailParams, EmailConfig, BaseConfig } from '@/models/setting/config';
// 测试邮箱连接
export function testEmail(data: TestEmailParams) {
return MSR.post({ url: TestEmailUrl, data });
}
// 保存基础信息
export function saveBaseInfo(data: SaveInfoParams) {
return MSR.post({ url: SaveBaseInfoUrl, data });
}
// 获取基础信息
export function getBaseInfo() {
return MSR.get<BaseConfig>({ url: GetBaseInfoUrl });
}
// 保存邮箱信息
export function saveEmailInfo(data: SaveInfoParams) {
return MSR.post({ url: SaveEmailInfoUrl, data });
}
// 获取邮箱信息
export function getEmailInfo() {
return MSR.get<EmailConfig>({ url: GetEmailInfoUrl });
}

View File

@ -0,0 +1,5 @@
export const TestEmailUrl = '/system/parameter/test/email';
export const SaveBaseInfoUrl = '/system/parameter/save/base-info';
export const SaveEmailInfoUrl = '/system/parameter/edit/email-info';
export const GetEmailInfoUrl = '/system/parameter/get/email-info';
export const GetBaseInfoUrl = '/system/parameter/get/base-info';

View File

@ -142,6 +142,12 @@
.btn-outline-primary-active();
.btn-outline-primary-disabled();
}
.arco-btn-outline--secondary {
.btn-outline-sec-default();
.btn-outline-sec-hover();
.btn-outline-sec-active();
.btn-outline-sec-disabled();
}
/** 输入框,选择器,文本域 **/
.arco-input-wrapper,
@ -452,7 +458,6 @@
/** 滚动条 **/
.arco-scrollbar-track-direction-horizontal {
margin-bottom: 4px;
height: 6px;
.arco-scrollbar-thumb-bar {
@apply m-0;

View File

@ -38,6 +38,11 @@
background-color: var(--color-text-n9) !important;
}
}
.btn-outline-sec-default() {
border-color: var(--color-text-brand) !important;
color: var(--color-text-1) !important;
background-color: white !important;
}
.btn-outline-sec-active() {
&:not(:disabled):active {
border-color: var(--color-text-brand) !important;

View File

@ -0,0 +1,30 @@
<template>
<div class="mt-[4px] w-full text-[12px] text-[var(--color-text-4)]">
{{ props.text }}
<MsIcon
v-if="props.showFillIcon"
type="icon-icon_corner_right_up"
class="cursor-pointer text-[rgb(var(--primary-6))]"
@click="fillHeapByDefault"
></MsIcon>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
text: string;
showFillIcon?: boolean;
}>(),
{
showFillIcon: true,
}
);
const emit = defineEmits(['fill']);
function fillHeapByDefault() {
emit('fill');
}
</script>
<style lang="less" scoped></style>

View File

@ -18,12 +18,14 @@
<a-scrollbar
class="pr-[5px]"
:style="{
overflowY: 'auto',
minWidth: 1000,
overflow: 'auto',
width: `calc(100vw - ${menuWidth}px - 48px)`,
height: props.autoHeight ? 'auto' : `calc(100vh - ${cardOverHeight}px)`,
}"
>
<slot></slot>
<div class="min-w-[1000px]">
<slot></slot>
</div>
</a-scrollbar>
</div>
<div
@ -50,9 +52,10 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from '@/hooks/useI18n';
import { computed } from 'vue';
import useAppStore from '@/store/modules/app';
const props = withDefaults(
defineProps<
@ -89,6 +92,12 @@
const router = useRouter();
const { t } = useI18n();
const appStore = useAppStore();
const collapsedWidth = 86;
const menuWidth = computed(() => {
return appStore.menuCollapse ? collapsedWidth : appStore.menuWidth;
});
const _spcialHeight = props.hasBreadcrumb ? 31 + props.specialHeight : props.specialHeight; // 31
const cardOverHeight = computed(() => {
@ -100,7 +109,7 @@
//
return 192;
}
return 246 + _spcialHeight;
return 256 + _spcialHeight;
});
function back() {
@ -149,5 +158,8 @@
:deep(.arco-scrollbar-track-direction-vertical) {
right: -10px;
}
:deep(.arco-scrollbar-track-direction-horizontal) {
bottom: -10px;
}
}
</style>

View File

@ -7,7 +7,10 @@
<a-skeleton-line :rows="props.skeletonLine" :line-height="24" />
</a-space>
</a-skeleton>
<a-descriptions v-else :data="(props.descriptions as unknown as DescData[])" size="large" :column="1">
<a-descriptions v-else :data="(props.descriptions as unknown as DescData[])" size="large" :column="props.column">
<template #title>
<slot name="title"></slot>
</template>
<a-descriptions-item v-for="item of props.descriptions" :key="item.label" :label="item.label">
<template v-if="item.isTag">
<a-tag
@ -22,7 +25,7 @@
</template>
<a-button v-else-if="item.isButton" type="text" @click="handleItemClick(item)">{{ item.value }}</a-button>
<div v-else>
{{ item.value?.toString() === '' ? '-' : item.value }}
<slot name="value" :item="item">{{ item.value?.toString() === '' ? '-' : item.value }}</slot>
</div>
</a-descriptions-item>
</a-descriptions>
@ -34,16 +37,23 @@
export interface Description {
label: string;
value: (string | number) | (string | number)[];
key?: string;
isTag?: boolean;
isButton?: boolean;
onClick?: () => void;
}
const props = defineProps<{
showSkeleton?: boolean;
skeletonLine?: number;
descriptions: Description[];
}>();
const props = withDefaults(
defineProps<{
showSkeleton?: boolean;
skeletonLine?: number;
column?: number;
descriptions: Description[];
}>(),
{
column: 1,
}
);
function handleItemClick(item: Description) {
if (typeof item.onClick === 'function') {

View File

@ -4,10 +4,9 @@
:width="props.width"
:footer="props.footer"
:mask="props.mask"
:class="props.mask ? '' : 'ms-drawer-no-mask'"
@ok="handleOk"
:class="['ms-drawer', props.mask ? '' : 'ms-drawer-no-mask']"
@cancel="handleCancel"
@close="handleCancel"
@close="handleClose"
>
<template #title>
<slot name="title">
@ -18,19 +17,38 @@
</div>
</slot>
</template>
<slot>
<MsDescription
v-if="props.descriptions?.length > 0 || showDescription"
:descriptions="props.descriptions"
:show-skeleton="props.showSkeleton"
:skeleton-line="10"
></MsDescription>
</slot>
<a-scrollbar
:style="{
overflowY: 'auto',
height: 'calc(100vh - 146px)',
}"
>
<slot>
<MsDescription
v-if="props.descriptions && props.descriptions.length > 0"
:descriptions="props.descriptions"
:show-skeleton="props.showSkeleton"
:skeleton-line="10"
></MsDescription>
</slot>
</a-scrollbar>
<template #footer>
<slot name="footer">
<a-button :disabled="props.okLoading" @click="handleCancel">
{{ t(props.cancelText || 'ms.drawer.cancel') }}
</a-button>
<a-button type="primary" :loading="props.okLoading" @click="handleOk">
{{ t(props.okText || 'ms.drawer.ok') }}
</a-button>
</slot>
</template>
</a-drawer>
</template>
<script setup lang="ts">
import { ref, watch, defineAsyncComponent } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import type { Description } from '@/components/pure/ms-description/index.vue';
//
@ -44,9 +62,11 @@
descriptions?: Description[];
footer?: boolean;
mask?: boolean;
showDescription?: boolean;
showSkeleton?: boolean;
[key: string]: any;
okLoading?: boolean;
okText?: string;
cancelText?: string;
width: number;
}
const props = withDefaults(defineProps<DrawerProps>(), {
@ -54,7 +74,9 @@
mask: true,
showSkeleton: false,
});
const emit = defineEmits(['update:visible']);
const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
const { t } = useI18n();
const visible = ref(props.visible);
@ -66,11 +88,18 @@
);
const handleOk = () => {
visible.value = false;
emit('confirm');
};
const handleCancel = () => {
visible.value = false;
emit('update:visible', false);
emit('cancel');
};
const handleClose = () => {
visible.value = false;
emit('update:visible', false);
};
</script>
@ -92,6 +121,11 @@
border-bottom: 1px solid var(--color-text-n8);
}
}
.ms-drawer {
.arco-scrollbar-track-direction-vertical {
right: -12px;
}
}
.ms-drawer-no-mask {
left: auto;
.arco-drawer {

View File

@ -0,0 +1,4 @@
export default {
'ms.drawer.cancel': 'Cancel',
'ms.drawer.ok': 'Confirm',
};

View File

@ -0,0 +1,4 @@
export default {
'ms.drawer.cancel': '取消',
'ms.drawer.ok': '确认',
};

View File

@ -36,6 +36,7 @@ export default {
'menu.settings.system.resourcePool': 'Resource Pool',
'menu.settings.system.resourcePoolDetail': 'Add resource pool',
'menu.settings.system.resourcePoolEdit': 'Edit resource pool',
'menu.settings.system.parameter': 'System parameter',
'navbar.action.locale': 'Switch to English',
...sys,
...localeSettings,

View File

@ -36,6 +36,7 @@ export default {
'menu.settings.system.resourcePool': '资源池',
'menu.settings.system.resourcePoolDetail': '添加资源池',
'menu.settings.system.resourcePoolEdit': '编辑资源池',
'menu.settings.system.parameter': '系统参数',
'navbar.action.locale': '切换为中文',
...sys,
...localeSettings,

View File

@ -0,0 +1,38 @@
// 基础信息配置
export interface BaseConfig {
url: string;
prometheusHost: string;
}
// 邮箱信息配置
export interface EmailConfig {
host: string; // 主机
port: string; // 端口
account: string; // 账户
from: string; // 发件人
password: string; // 密码
ssl: string;
tsl: string;
recipient: string; // 收件人
}
interface ParamItem {
paramKey: string; // 参数的 key
paramValue: string; // 参数的值
type: string; // 参数类型,一般是 string
}
// 保存基础信息、邮箱信息接口入参
export type SaveInfoParams = ParamItem[];
// 测试邮箱连接接口入参
export interface TestEmailParams {
'smtp.host': string;
'smtp.port': string;
'smtp.account': string;
'smtp.password': string;
'smtp.from': string;
'smtp.ssl': string;
'smtp.tsl': string;
'smtp.recipient': string;
}

View File

@ -89,6 +89,16 @@ const Setting: AppRouteRecordRaw = {
isTopMenu: true,
},
},
{
path: 'parameter',
name: 'settingSystemParameter',
component: () => import('@/views/setting/system/config/index.vue'),
meta: {
locale: 'menu.settings.system.parameter',
roles: ['*'],
isTopMenu: true,
},
},
],
},
{

View File

@ -120,3 +120,16 @@ export function formatFileSize(fileSize: number): string {
return `${formattedSize} ${unit}`;
}
/**
*
* @param str
* @returns
*/
export function desensitize(str: string): string {
if (!str || typeof str !== 'string') {
return '';
}
return str.replace(/./g, '*');
}

View File

@ -0,0 +1,491 @@
<template>
<div>
<MsCard class="mb-[16px]" :loading="baseloading" simple auto-height>
<div class="mb-[16px] flex justify-between">
<div class="text-[var(--color-text-000)]">{{ t('system.config.baseInfo') }}</div>
<a-button type="outline" size="mini" @click="baseInfoDrawerVisible = true">
{{ t('system.config.update') }}
</a-button>
</div>
<MsDescription :descriptions="baseInfoDescs" class="no-bottom" :column="2" />
</MsCard>
<MsCard class="mb-[16px]" :loading="emailLoading" simple auto-height>
<div class="mb-[16px] flex justify-between">
<div class="text-[var(--color-text-000)]">{{ t('system.config.emailConfig') }}</div>
<a-button type="outline" size="mini" @click="emailConfigDrawerVisible = true">
{{ t('system.config.update') }}
</a-button>
</div>
<MsDescription :descriptions="emailInfoDescs" :column="2">
<template #value="{ item }">
<div v-if="item.key && ['ssl', 'tsl'].includes(item.key)">
<div v-if="item.value === 'true'" class="flex items-center">
<icon-check-circle-fill class="mr-[8px] text-[rgb(var(--success-6))]" />{{
t('system.config.email.open')
}}
</div>
<div v-else class="flex items-center">
<MsIcon type="icon-icon_disable" class="mr-[4px] text-[var(--color-text-4)]" />
{{ t('system.config.email.close') }}
</div>
</div>
<div v-else-if="item.key === 'password' && item.value?.toString() !== ''">
<span v-if="pswInVisible">
{{ item.value }}
<icon-eye class="cursor-pointer text-[var(--color-text-4)]" @click="togglePswVisible" />
</span>
<span v-else>
{{ desensitize(item.value as string) }}
<icon-eye-invisible class="cursor-pointer text-[var(--color-text-4)]" @click="togglePswVisible" />
</span>
</div>
<div v-else>{{ item.value?.toString() === '' ? '-' : item.value }}</div>
</template>
</MsDescription>
<a-button
type="outline"
size="mini"
class="arco-btn-outline--secondary"
:loading="testLoading"
@click="testLink('page')"
>
{{ t('system.config.email.test') }}
</a-button>
</MsCard>
<MsDrawer
v-model:visible="baseInfoDrawerVisible"
:title="t('system.config.baseInfo.updateTitle')"
:ok-text="t('system.config.baseInfo.update')"
:ok-loading="baseDrawerLoading"
:width="680"
@confirm="updateBaseInfo"
@cancel="baseInfoCancel"
>
<a-form ref="baseFormRef" :model="baseInfoForm" layout="vertical">
<a-form-item
:label="t('system.config.pageUrl')"
field="url"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.baseInfo.pageUrlRequired') }]"
required
>
<a-input
v-model:model-value="baseInfoForm.url"
:max-length="250"
:placeholder="t('system.config.baseInfo.pageUrlPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.baseInfo.pageUrlSub', { url: defaulUrl })" @fill="fillDefaultUrl" />
</a-form-item>
<a-form-item :label="t('system.config.prometheus')" field="prometheusHost" asterisk-position="end">
<a-input
v-model:model-value="baseInfoForm.prometheusHost"
:max-length="250"
:placeholder="t('system.config.baseInfo.prometheusPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub
:text="t('system.config.baseInfo.prometheusSub', { prometheus: defaulPrometheus })"
@fill="fillDefaultPrometheus"
/>
</a-form-item>
</a-form>
</MsDrawer>
<MsDrawer
v-model:visible="emailConfigDrawerVisible"
:title="t('system.config.email.updateTitle')"
:ok-text="t('system.config.email.update')"
:ok-loading="emailDrawerLoading"
:width="680"
@confirm="updateEmailConfig"
@cancel="emailConfigCancel"
>
<a-form ref="emailFormRef" :model="emailConfigForm" layout="vertical">
<a-form-item
:label="t('system.config.email.host')"
field="host"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.email.hostRequired') }]"
required
>
<a-input
v-model:model-value="emailConfigForm.host"
:max-length="250"
:placeholder="t('system.config.email.hostPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.email.port')"
field="port"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.email.portRequired') }]"
required
>
<a-input
v-model:model-value="emailConfigForm.port"
:max-length="250"
:placeholder="t('system.config.email.portPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.email.account')"
field="account"
asterisk-position="end"
:rules="[{ required: true, message: t('system.config.email.accountRequired') }, emailRule]"
required
>
<a-input
v-model:model-value="emailConfigForm.account"
:max-length="250"
:placeholder="t('system.config.email.accountPlaceholder')"
autocomplete="off"
allow-clear
></a-input>
</a-form-item>
<a-form-item :label="t('system.config.email.password')" field="password" asterisk-position="end">
<a-input-password
v-model:model-value="emailConfigForm.password"
:max-length="250"
:placeholder="t('system.config.email.passwordPlaceholder')"
autocomplete="off"
allow-clear
></a-input-password>
</a-form-item>
<a-form-item :label="t('system.config.email.from')" field="from" asterisk-position="end" :rules="[emailRule]">
<a-input
v-model:model-value="emailConfigForm.from"
:max-length="250"
:placeholder="t('system.config.email.fromPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item
:label="t('system.config.email.recipient')"
field="recipient"
asterisk-position="end"
:rules="[emailRule]"
>
<a-input
v-model:model-value="emailConfigForm.recipient"
:max-length="250"
:placeholder="t('system.config.email.recipientPlaceholder')"
allow-clear
></a-input>
</a-form-item>
<a-form-item :label="t('system.config.email.ssl')" field="ssl" asterisk-position="end">
<a-switch v-model:model-value="emailConfigForm.ssl" />
<MsFormItemSub :text="t('system.config.email.sslTip')" :show-fill-icon="false" />
</a-form-item>
<a-form-item :label="t('system.config.email.tsl')" field="tsl" asterisk-position="end">
<a-switch v-model:model-value="emailConfigForm.tsl" />
<MsFormItemSub :text="t('system.config.email.tslTip')" :show-fill-icon="false" />
</a-form-item>
<a-button type="outline" class="w-[88px]" :loading="drawerTestLoading" @click="testLink('drawer')">
{{ t('system.config.email.test') }}
</a-button>
</a-form>
</MsDrawer>
</div>
</template>
<script setup lang="ts">
import { onBeforeMount, ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import { desensitize } from '@/utils';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsDescription, { Description } from '@/components/pure/ms-description/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsFormItemSub from '@/components/bussiness/ms-form-item-sub/index.vue';
import { validateEmail } from '@/utils/validate';
import { testEmail, saveBaseInfo, saveEmailInfo, getBaseInfo, getEmailInfo } from '@/api/modules/setting/config';
import type { EmailConfig, TestEmailParams } from '@/models/setting/config';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
const { t } = useI18n();
const baseloading = ref(false);
const baseDrawerLoading = ref(false);
const baseInfoDrawerVisible = ref(false);
const baseFormRef = ref<FormInstance>();
const baseInfo = ref({
url: 'http://127.0.0.1:8081',
prometheusHost: 'http://ms-prometheus:9090',
});
const baseInfoForm = ref({ ...baseInfo.value });
const baseInfoDescs = ref<Description[]>([]);
//
const defaulUrl = 'https://metersphere.com';
const defaulPrometheus = 'http://ms-prometheus:9090';
function fillDefaultUrl() {
baseInfoForm.value.url = defaulUrl;
}
function fillDefaultPrometheus() {
baseInfoForm.value.prometheusHost = defaulPrometheus;
}
/**
* 初始化基础信息
*/
async function initBaseInfo() {
try {
baseloading.value = true;
const res = await getBaseInfo();
baseInfo.value = { ...res };
baseInfoForm.value = { ...res };
baseInfoDescs.value = [
{
label: t('system.config.pageUrl'),
value: res.url,
},
{
label: t('system.config.prometheus'),
value: res.prometheusHost,
},
];
} catch (error) {
console.log(error);
} finally {
baseloading.value = false;
}
}
/**
* 拼接基础信息参数
*/
function makeBaseInfoParams() {
const { url, prometheusHost } = baseInfoForm.value;
return [
{ paramKey: 'base.url', paramValue: url, type: 'text' },
{ paramKey: 'base.prometheus.host', paramValue: prometheusHost, type: 'text' },
];
}
/**
* 保存基础信息
*/
function updateBaseInfo() {
baseFormRef.value?.validate(async (errors: Record<string, ValidatedError> | undefined) => {
if (!errors) {
try {
baseDrawerLoading.value = true;
await saveBaseInfo(makeBaseInfoParams());
Message.success(t('system.config.baseInfo.updateSuccess'));
baseInfoDrawerVisible.value = false;
initBaseInfo();
} catch (error) {
console.log(error);
} finally {
baseDrawerLoading.value = false;
}
}
});
}
function baseInfoCancel() {
baseFormRef.value?.resetFields();
baseInfoForm.value = { ...baseInfo.value };
}
const emailLoading = ref(false);
const emailDrawerLoading = ref(false);
const testLoading = ref(false);
const drawerTestLoading = ref(false);
const emailConfigDrawerVisible = ref(false);
const emailConfig = ref({
host: '', //
port: '', //
account: '', //
from: '', //
password: '', //
ssl: false, // ssl
tsl: false, // tsl
recipient: '', //
});
const emailConfigForm = ref({ ...emailConfig.value });
const emailFormRef = ref<FormInstance>();
const emailInfoDescs = ref<Description[]>([]);
const pswInVisible = ref(false); //
function togglePswVisible() {
pswInVisible.value = !pswInVisible.value;
}
const emailRule = {
validator: (value: string | undefined, callback: (error?: string) => void) => {
if (value && !validateEmail(value)) {
callback(t('system.config.email.emailErrTip'));
}
},
};
/**
* 初始化邮箱信息
*/
async function initEmailInfo() {
try {
emailLoading.value = true;
const res = await getEmailInfo();
const _ssl = Boolean(res.ssl);
const _tsl = Boolean(res.tsl);
emailConfig.value = { ...res, ssl: _ssl, tsl: _tsl };
emailConfigForm.value = { ...res, ssl: _ssl, tsl: _tsl };
const { host, port, account, password, from, recipient, ssl, tsl } = res;
emailInfoDescs.value = [
{
label: t('system.config.email.host'),
value: host,
},
{
label: t('system.config.email.port'),
value: port,
},
{
label: t('system.config.email.account'),
value: account,
},
{
label: t('system.config.email.password'),
value: password,
key: 'password',
},
{
label: t('system.config.email.from'),
value: from,
},
{
label: t('system.config.email.recipient'),
value: recipient,
},
{
label: t('system.config.email.ssl'),
value: ssl,
key: 'ssl',
},
{
label: t('system.config.email.tsl'),
value: tsl,
key: 'tsl',
},
];
} catch (error) {
console.log(error);
} finally {
emailLoading.value = false;
}
}
/**
* 拼接邮箱信息参数
*/
function makeEmailParams() {
const { host, port, account, password, from, recipient, ssl, tsl } = emailConfigForm.value;
return [
{ paramKey: 'smtp.host', paramValue: host, type: 'text' },
{ paramKey: 'smtp.port', paramValue: port, type: 'text' },
{ paramKey: 'smtp.account', paramValue: account, type: 'text' },
{ paramKey: 'smtp.password', paramValue: password, type: 'text' },
{ paramKey: 'smtp.from', paramValue: from, type: 'text' },
{ paramKey: 'smtp.recipient', paramValue: recipient, type: 'text' },
{ paramKey: 'smtp.ssl', paramValue: ssl?.toString(), type: 'text' },
{ paramKey: 'smtp.tsl', paramValue: tsl?.toString(), type: 'text' },
];
}
/**
* 保存邮箱信息
*/
function updateEmailConfig() {
emailFormRef.value?.validate(async (errors: Record<string, ValidatedError> | undefined) => {
if (!errors) {
try {
emailDrawerLoading.value = true;
await saveEmailInfo(makeEmailParams());
Message.success(t('system.config.email.updateSuccess'));
emailConfigDrawerVisible.value = false;
initEmailInfo();
} catch (error) {
console.log(error);
} finally {
emailDrawerLoading.value = false;
}
}
});
}
function emailConfigCancel() {
emailFormRef.value?.resetFields();
emailConfigForm.value = { ...emailConfig.value };
}
/**
* 拼接测试邮箱参数
* @param form 抽屉中的表单/页面中的表单
*/
function makeEmailTestParams(form: EmailConfig) {
const { host, port, account, password, from, recipient, ssl, tsl } = form;
return {
'smtp.host': host,
'smtp.port': port,
'smtp.account': account,
'smtp.password': password,
'smtp.from': from,
'smtp.ssl': ssl,
'smtp.tsl': tsl,
'smtp.recipient': recipient,
};
}
/**
* 测试邮箱
* @param emailInfo 来源于抽屉/页面
*/
async function testLink(emailInfo: 'page' | 'drawer') {
try {
let params = {} as TestEmailParams;
if (emailInfo === 'drawer') {
drawerTestLoading.value = true;
params = makeEmailTestParams({
...emailConfigForm.value,
ssl: emailConfigForm.value.ssl?.toString(),
tsl: emailConfigForm.value.tsl?.toString(),
});
} else {
testLoading.value = true;
params = makeEmailTestParams({
...emailConfig.value,
ssl: emailConfig.value.ssl?.toString(),
tsl: emailConfig.value.tsl?.toString(),
});
}
await testEmail(params);
Message.success(t('system.config.email.testSuccess'));
} catch (error) {
console.log(error);
} finally {
testLoading.value = false;
drawerTestLoading.value = false;
}
}
onBeforeMount(() => {
initBaseInfo();
initEmailInfo();
});
</script>
<style lang="less" scoped>
:deep(.no-bottom) {
.arco-descriptions-item-label,
.arco-descriptions-item-value {
@apply pb-0;
}
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<MsCard
class="mb-[16px]"
:title="t('system.config.parameterConfig')"
hide-back
hide-footer
auto-height
no-content-padding
>
<a-tabs v-model:active-key="activeTab" class="no-content">
<a-tab-pane key="baseConfig" :title="t('system.config.baseConfig')" />
<a-tab-pane key="pageConfig" :title="t('system.config.pageConfig')" />
<a-tab-pane key="authConfig" :title="t('system.config.authConfig')" />
</a-tabs>
</MsCard>
<baseConfig v-show="activeTab === 'baseConfig'" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import { useI18n } from '@/hooks/useI18n';
import baseConfig from './components/baseConfig.vue';
const { t } = useI18n();
const activeTab = ref('baseConfig');
</script>
<style lang="less" scoped>
:deep(.no-content) {
.arco-tabs-content {
display: none;
}
}
</style>

View File

@ -0,0 +1,6 @@
export default {
'system.config.parameterConfig': 'System Parameters Configuration',
'system.config.baseConfig': 'Basic Settings',
'system.config.pageConfig': 'Interface settings',
'system.config.authConfig': 'Authentication Settings',
};

View File

@ -0,0 +1,48 @@
export default {
'system.config.parameterConfig': '系统参数配置',
'system.config.baseConfig': '基础设置',
'system.config.pageConfig': '界面设置',
'system.config.authConfig': '认证设置',
'system.config.baseInfo': '基本信息',
'system.config.update': '更新',
'system.config.pageUrl': '当前站点 URL',
'system.config.prometheus': 'Prometheus',
'system.config.emailConfig': '邮件设置',
'system.config.email.host': 'SMTP 主机',
'system.config.email.port': 'SMTP 端口',
'system.config.email.account': 'SMTP 账户',
'system.config.email.password': 'SMTP 密码',
'system.config.email.from': '指定发件人',
'system.config.email.recipient': '测试收件人',
'system.config.email.ssl': 'SSL',
'system.config.email.tsl': 'TSL',
'system.config.email.open': '开启',
'system.config.email.close': '关闭',
'system.config.email.test': '测试连接',
'system.config.baseInfo.updateTitle': '更新基本信息',
'system.config.baseInfo.update': '更新',
'system.config.baseInfo.updateSuccess': '更新成功',
'system.config.baseInfo.pageUrlSub': '例如:{url}',
'system.config.baseInfo.pageUrlRequired': '站点 URL 不能为空',
'system.config.baseInfo.pageUrlPlaceholder': '请输入当前站点 URL',
'system.config.baseInfo.prometheusSub': '例如:{prometheus}',
'system.config.baseInfo.prometheusRequired': 'prometheus 不能为空',
'system.config.baseInfo.prometheusPlaceholder': ' 请输入 prometheus',
'system.config.email.updateTitle': '更新邮件设置',
'system.config.email.update': '更新',
'system.config.email.hostRequired': 'SMTP 主机不能为空',
'system.config.email.hostPlaceholder': '请输入SMTP 主机地址',
'system.config.email.portRequired': 'SMTP 端口不能为空',
'system.config.email.portPlaceholder': '请输入SMTP 端口',
'system.config.email.accountRequired': 'SMTP 账户不能为空',
'system.config.email.accountPlaceholder': '请输入SMTP 账户',
'system.config.email.passwordRequired': 'SMTP 密码不能为空',
'system.config.email.passwordPlaceholder': '请输入SMTP 密码',
'system.config.email.fromPlaceholder': '请输入指定发件人邮箱',
'system.config.email.recipientPlaceholder': '请输入测试收件人邮箱',
'system.config.email.sslTip': '若 SMTP 端口是 465需要启用 SSL',
'system.config.email.tslTip': '若 SMTP 端口是 587需要启用 TSL',
'system.config.email.emailErrTip': '邮箱格式错误,请重新输入',
'system.config.email.updateSuccess': '更新成功',
'system.config.email.testSuccess': '邮箱连接成功',
};

View File

@ -1,7 +1,7 @@
<template>
<MsDrawer
v-model:visible="showScriptDrawer"
width="680px"
:width="680"
:mask="false"
:footer="false"
:title="t('system.plugin.showScriptTitle', { name: props.config.title })"

View File

@ -1,7 +1,7 @@
<template>
<MsDrawer
v-model:visible="showJobDrawer"
width="680px"
:width="680"
:title="t('system.resourcePool.customJobTemplate')"
:footer="false"
@close="handleClose"

View File

@ -1,67 +1,65 @@
<template>
<div>
<MsCard :loading="loading" has-breadcrumb simple>
<div class="mb-4 flex items-center justify-between">
<a-button type="primary" @click="addPool">
{{ t('system.resourcePool.createPool') }}
</a-button>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('system.resourcePool.searchPool')"
class="w-[230px]"
allow-clear
@search="searchPool"
@press-enter="searchPool"
></a-input-search>
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<template #name="{ record }">
<a-button type="text" @click="showPoolDetail(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.resourcePool.tableEnable') }}
</div>
<div v-else class="flex items-center text-[var(--color-text-4)]">
<icon-stop class="mr-[2px]" />
{{ t('system.resourcePool.tableDisable') }}
</div>
</template>
<template #action="{ record }">
<MsButton @click="editPool(record)">{{ t('system.resourcePool.editPool') }}</MsButton>
<MsButton v-if="record.enable" @click="disabledPool(record)">{{
t('system.resourcePool.tableDisable')
}}</MsButton>
<MsButton v-else @click="enablePool(record)">{{ t('system.resourcePool.tableEnable') }}</MsButton>
<MsTableMoreAction :list="tableActions" @select="handleSelect($event, record)"></MsTableMoreAction>
</template>
</ms-base-table>
</MsCard>
<MsDrawer
v-model:visible="showDetailDrawer"
width="480px"
:title="activePool?.name"
:title-tag="activePool?.enable ? t('system.resourcePool.tableEnable') : t('system.resourcePool.tableDisable')"
:title-tag-color="activePool?.enable ? 'green' : 'gray'"
:descriptions="activePoolDesc"
:footer="false"
:mask="false"
:show-skeleton="drawerLoading"
show-description
>
<template #tbutton>
<a-button type="outline" size="mini" :disabled="drawerLoading" @click="editPool(activePool)">
{{ t('system.resourcePool.editPool') }}
</a-button>
<MsCard :loading="loading" has-breadcrumb simple>
<div class="mb-4 flex items-center justify-between">
<a-button type="primary" @click="addPool">
{{ t('system.resourcePool.createPool') }}
</a-button>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('system.resourcePool.searchPool')"
class="w-[230px]"
allow-clear
@search="searchPool"
@press-enter="searchPool"
></a-input-search>
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<template #name="{ record }">
<a-button type="text" @click="showPoolDetail(record)">{{ record.name }}</a-button>
</template>
</MsDrawer>
<JobTemplateDrawer
v-model:visible="showJobDrawer"
:default-val="activePool?.testResourceReturnDTO.jobDefinition || ''"
read-only
/>
</div>
<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.resourcePool.tableEnable') }}
</div>
<div v-else class="flex items-center text-[var(--color-text-4)]">
<icon-stop class="mr-[2px]" />
{{ t('system.resourcePool.tableDisable') }}
</div>
</template>
<template #action="{ record }">
<MsButton @click="editPool(record)">{{ t('system.resourcePool.editPool') }}</MsButton>
<MsButton v-if="record.enable" @click="disabledPool(record)">{{
t('system.resourcePool.tableDisable')
}}</MsButton>
<MsButton v-else @click="enablePool(record)">{{ t('system.resourcePool.tableEnable') }}</MsButton>
<MsTableMoreAction :list="tableActions" @select="handleSelect($event, record)"></MsTableMoreAction>
</template>
</ms-base-table>
</MsCard>
<MsDrawer
v-model:visible="showDetailDrawer"
:width="480"
:title="activePool?.name"
:title-tag="activePool?.enable ? t('system.resourcePool.tableEnable') : t('system.resourcePool.tableDisable')"
:title-tag-color="activePool?.enable ? 'green' : 'gray'"
:descriptions="activePoolDesc"
:footer="false"
:mask="false"
:show-skeleton="drawerLoading"
show-description
>
<template #tbutton>
<a-button type="outline" size="mini" :disabled="drawerLoading" @click="editPool(activePool)">
{{ t('system.resourcePool.editPool') }}
</a-button>
</template>
</MsDrawer>
<JobTemplateDrawer
v-model:visible="showJobDrawer"
:default-val="activePool?.testResourceReturnDTO.jobDefinition || ''"
read-only
/>
</template>
<script setup lang="ts">
@ -85,7 +83,6 @@
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import type { ResourcePoolDetail } from '@/models/setting/resourcePool';
import { sleep } from '@/utils';
const { t } = useI18n();
const router = useRouter();