feat(个人中心): 个人中心 fake-page&切换组织
|
@ -22,6 +22,11 @@ export default mergeConfig(
|
|||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/front\/file/, ''),
|
||||
},
|
||||
'/plugin/image': {
|
||||
target: 'http://172.16.200.18:8081/',
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(/^\/front\/plugin\/image/, ''),
|
||||
},
|
||||
'/base-display': {
|
||||
target: 'http://172.16.200.18:8081/',
|
||||
changeOrigin: true,
|
||||
|
|
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 25 KiB |
|
@ -1,10 +1,18 @@
|
|||
// 系统全局类的接口
|
||||
import MSR from '@/api/http/index';
|
||||
import { GetVersionUrl } from '@/api/requrls/system';
|
||||
import { GetVersionUrl, OrgOptionsUrl, SwitchOrgUrl } from '@/api/requrls/system';
|
||||
|
||||
// 获取系统版本
|
||||
export function getSystemVersion() {
|
||||
return MSR.get<string>({ url: GetVersionUrl }, { ignoreCancelToken: true });
|
||||
}
|
||||
|
||||
export default { getSystemVersion };
|
||||
// 获取当前登录用户组织机构下拉选项
|
||||
export function getOrgOptions() {
|
||||
return MSR.get<{ id: string; name: string }[]>({ url: OrgOptionsUrl }, { ignoreCancelToken: true });
|
||||
}
|
||||
|
||||
// 切换用户当前组织
|
||||
export function switchUserOrg(organizationId: string, userId: string) {
|
||||
return MSR.post({ url: SwitchOrgUrl, data: { organizationId, userId } }, { ignoreCancelToken: true });
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// 系统全局类的接口
|
||||
|
||||
export const GetVersionUrl = '/system/version/current';
|
||||
|
||||
export default { GetVersionUrl };
|
||||
export const OrgOptionsUrl = '/system/organization/switch-option';
|
||||
export const SwitchOrgUrl = '/system/organization/switch';
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
</a-spin>
|
||||
</div>
|
||||
<div class="flex w-[calc(100%-293px)] flex-col p-[16px]">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-[4px] text-[var(--color-text-1)]">{{ activeFolderName }}</div>
|
||||
<div class="text-[var(--color-text-4)]">({{ activeFolderName }})</div>
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
<script lang="tsx">
|
||||
import { compile, computed, defineComponent, h, ref } from 'vue';
|
||||
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
import MsPersonInfoDrawer from '@/components/business/ms-personal-drawer/index.vue';
|
||||
|
||||
import { getOrgOptions, switchUserOrg } from '@/api/modules/system';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useUser from '@/hooks/useUser';
|
||||
import { BOTTOM_MENU_LIST } from '@/router/constants';
|
||||
import { PERSONAL_ROUTE } from '@/router/routes/base';
|
||||
import { useAppStore, useUserStore } from '@/store';
|
||||
import { openWindow, regexUrl } from '@/utils';
|
||||
import { listenerRouteChange } from '@/utils/route-listener';
|
||||
|
||||
import { WorkbenchRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
import useMenuTree from './use-menu-tree';
|
||||
import type { RouteMeta } from 'vue-router';
|
||||
|
||||
|
@ -61,7 +66,6 @@
|
|||
});
|
||||
}
|
||||
};
|
||||
const personalActiveMenus = ref(['']);
|
||||
/**
|
||||
* 查找激活的菜单项
|
||||
* @param target 目标菜单名
|
||||
|
@ -88,11 +92,6 @@
|
|||
if (isFind) return; // 节省性能
|
||||
backtrack(el, [el?.name as string]);
|
||||
});
|
||||
personalActiveMenus.value = [''];
|
||||
if (result.length === 0) {
|
||||
backtrack(PERSONAL_ROUTE, [PERSONAL_ROUTE.name as string]);
|
||||
personalActiveMenus.value = [...result];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
/**
|
||||
|
@ -114,17 +113,53 @@
|
|||
};
|
||||
|
||||
const personalMenusVisible = ref(false);
|
||||
const personalDrawerVisible = ref(false);
|
||||
const switchOrgVisible = ref(false);
|
||||
const orgKeyword = ref('');
|
||||
const originOrgList = ref<{ id: string; name: string }[]>([]);
|
||||
const orgList = computed(() => originOrgList.value.filter((e) => e.name.includes(orgKeyword.value)));
|
||||
|
||||
async function switchOrg(id: string) {
|
||||
try {
|
||||
Message.loading(t('personal.switchOrgLoading'));
|
||||
await switchUserOrg(id, userStore.id || '');
|
||||
Message.clear();
|
||||
Message.success(t('personal.switchOrgSuccess'));
|
||||
personalMenusVisible.value = false;
|
||||
orgKeyword.value = '';
|
||||
await router.replace({ name: WorkbenchRouteEnum.WORKBENCH });
|
||||
userStore.isLogin();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
const res = await getOrgOptions();
|
||||
originOrgList.value = res || [];
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
const personalMenus = [
|
||||
{
|
||||
label: t('personal.info'),
|
||||
icon: <MsIcon type="icon-icon-contacts" class="text-[var(--color-text-4)]" />,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
route: PERSONAL_ROUTE.children![0],
|
||||
event: () => {
|
||||
personalDrawerVisible.value = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('personal.switchOrg'),
|
||||
icon: <MsIcon type="icon-icon_switch_outlined1" class="text-[var(--color-text-4)]" />,
|
||||
isTrigger: true,
|
||||
event: () => {
|
||||
switchOrgVisible.value = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
divider: <a-divider class="ms-dropdown-divider" />,
|
||||
|
@ -152,19 +187,65 @@
|
|||
if (e.divider) {
|
||||
return e.divider;
|
||||
}
|
||||
if (e.isTrigger) {
|
||||
return (
|
||||
<a-dropdown
|
||||
trigger="click"
|
||||
position="right"
|
||||
v-slots={{
|
||||
content: () => (
|
||||
<>
|
||||
<a-input-search
|
||||
v-model:model-value={orgKeyword.value}
|
||||
placeholder={t('personal.searchOrgPlaceholder')}
|
||||
/>
|
||||
<a-divider class="ms-dropdown-divider" />
|
||||
{orgList.value.map((item) => (
|
||||
<a-doption
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
class={item.id === appStore.currentOrgId ? 'active-org' : ''}
|
||||
>
|
||||
{item.name}
|
||||
{item.id === appStore.currentOrgId ? (
|
||||
<MsTag
|
||||
type="primary"
|
||||
theme="light"
|
||||
size="small"
|
||||
class="ml-[4px] !bg-[rgb(var(--primary-9))] px-[4px]"
|
||||
>
|
||||
{t('personal.currentOrg')}
|
||||
</MsTag>
|
||||
) : null}
|
||||
</a-doption>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
}}
|
||||
onSelect={(orgId: string) => {
|
||||
switchOrg(orgId);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="arco-trigger-menu-item"
|
||||
onClick={() => {
|
||||
if (typeof e.event === 'function') {
|
||||
e.event();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{e.icon}
|
||||
{e.label}
|
||||
</div>
|
||||
</a-dropdown>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
'arco-trigger-menu-item',
|
||||
personalActiveMenus.value.includes(e.route?.name as string)
|
||||
? 'arco-trigger-menu-selected'
|
||||
: '',
|
||||
]}
|
||||
class="arco-trigger-menu-item"
|
||||
onClick={() => {
|
||||
if (typeof e.event === 'function') {
|
||||
e.event();
|
||||
} else if (e.route) {
|
||||
goto(e.route);
|
||||
}
|
||||
personalMenusVisible.value = false;
|
||||
}}
|
||||
|
@ -188,6 +269,16 @@
|
|||
</a-trigger>
|
||||
);
|
||||
};
|
||||
const personalInfoDrawer = () => {
|
||||
return (
|
||||
<MsPersonInfoDrawer
|
||||
visible={personalDrawerVisible.value}
|
||||
onUpdate:visible={(e) => {
|
||||
personalDrawerVisible.value = e;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSubMenu = () => {
|
||||
function travel(_route: (RouteRecordRaw | null)[] | null, nodes = []) {
|
||||
|
@ -220,28 +311,31 @@
|
|||
};
|
||||
|
||||
return () => (
|
||||
<a-menu
|
||||
mode={'vertical'}
|
||||
v-model:collapsed={collapsed.value}
|
||||
v-model:open-keys={openKeys.value}
|
||||
show-collapse-button={appStore.device !== 'mobile'}
|
||||
auto-open={false}
|
||||
selected-keys={selectedKey.value}
|
||||
auto-open-selected={true}
|
||||
level-indent={34}
|
||||
style="height: 100%;width:100%;"
|
||||
onCollapse={setCollapse}
|
||||
trigger-props={{
|
||||
'show-arrow': false,
|
||||
'popup-offset': -4,
|
||||
}}
|
||||
v-slots={{
|
||||
'collapse-icon': () => (appStore.menuCollapse ? <icon-right /> : <icon-left />),
|
||||
}}
|
||||
>
|
||||
{renderSubMenu()}
|
||||
{personalInfoMenu()}
|
||||
</a-menu>
|
||||
<>
|
||||
<a-menu
|
||||
mode={'vertical'}
|
||||
v-model:collapsed={collapsed.value}
|
||||
v-model:open-keys={openKeys.value}
|
||||
show-collapse-button={appStore.device !== 'mobile'}
|
||||
auto-open={false}
|
||||
selected-keys={selectedKey.value}
|
||||
auto-open-selected={true}
|
||||
level-indent={34}
|
||||
style="height: 100%;width:100%;"
|
||||
onCollapse={setCollapse}
|
||||
trigger-props={{
|
||||
'show-arrow': false,
|
||||
'popup-offset': -4,
|
||||
}}
|
||||
v-slots={{
|
||||
'collapse-icon': () => (appStore.menuCollapse ? <icon-right /> : <icon-left />),
|
||||
}}
|
||||
>
|
||||
{renderSubMenu()}
|
||||
{personalInfoMenu()}
|
||||
</a-menu>
|
||||
{personalInfoDrawer()}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -340,4 +434,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.active-org {
|
||||
color: rgb(var(--primary-5));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,381 @@
|
|||
<template>
|
||||
<div class="flex h-full flex-col gap-[16px]">
|
||||
<div class="flex items-center">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ t('ms.personal.apiKey') }}</div>
|
||||
<a-tooltip :content="t('ms.personal.apiKeyTip')" position="right">
|
||||
<icon-question-circle
|
||||
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-tooltip :content="t('ms.personal.maxTip')" position="right" :disabled="apiKeyList.length < 5">
|
||||
<a-button type="outline" class="w-[60px]" :disabled="apiKeyList.length >= 5">{{ t('common.new') }}</a-button>
|
||||
</a-tooltip>
|
||||
<a-spin class="api-list-content" :loading="loading">
|
||||
<div v-for="item of apiKeyList" :key="item.id" class="api-item">
|
||||
<div class="mb-[8px] border-b border-solid border-[var(--color-text-n8)]">
|
||||
<div class="px-[16px]">
|
||||
<div class="api-item-label">Access Key</div>
|
||||
<div class="api-item-value-strong">
|
||||
{{ item.accessKey }}
|
||||
<MsTag v-if="item.isExpire" type="warning" theme="light" size="small" class="mx-[4px] px-[4px]">
|
||||
{{ t('ms.personal.expired') }}
|
||||
</MsTag>
|
||||
<MsIcon type="icon-icon_copy_outlined" class="copy-icon" @click="handleCopy(item.accessKey)" />
|
||||
</div>
|
||||
<div class="api-item-label">Secret Key</div>
|
||||
<div class="api-item-value-strong">
|
||||
{{ item.desensitization ? item.secretKey.replace(/./g, '*') : item.secretKey }}
|
||||
<MsIcon
|
||||
:type="item.desensitization ? 'icon-icon_preview_close_one' : 'icon-icon_visible_outlined'"
|
||||
class="eye-icon"
|
||||
@click="desensitization(item)"
|
||||
/>
|
||||
<MsIcon type="icon-icon_copy_outlined" class="copy-icon" @click="handleCopy(item.secretKey)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-[16px]">
|
||||
<div class="api-item-label">{{ t('ms.personal.desc') }}</div>
|
||||
<a-tooltip :content="item.desc">
|
||||
<div class="api-item-value one-line-text">{{ item.desc }}</div>
|
||||
</a-tooltip>
|
||||
<div class="api-item-label">{{ t('ms.personal.createTime') }}</div>
|
||||
<div class="api-item-value">{{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
|
||||
<div class="api-item-label">{{ t('ms.personal.expireTime') }}</div>
|
||||
<div class="api-item-value">
|
||||
{{ dayjs(item.expireTime).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
<a-tooltip v-if="item.isExpire" :content="t('ms.personal.expiredTip')">
|
||||
<MsIcon type="icon-icon_warning_filled" class="ml-[4px] text-[rgb(var(--warning-6))]" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between px-[16px]">
|
||||
<MsTableMoreAction :list="actions" trigger="click" @select="handleMoreActionSelect($event, item)">
|
||||
<a-button size="mini" type="outline" class="arco-btn-outline--secondary">
|
||||
{{ t('common.setting') }}
|
||||
</a-button>
|
||||
</MsTableMoreAction>
|
||||
<a-switch
|
||||
v-model:model-value="item.enable"
|
||||
size="small"
|
||||
:before-change="() => handleBeforeEnableChange(item)"
|
||||
></a-switch>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
<a-modal
|
||||
v-model:visible="timeModalVisible"
|
||||
:title="t('ms.personal.changeAvatar')"
|
||||
title-align="start"
|
||||
:ok-text="t('common.save')"
|
||||
class="ms-usemodal"
|
||||
:width="680"
|
||||
unmount-on-close
|
||||
@before-ok="handleTimeConfirm"
|
||||
@close="handleTimeClose"
|
||||
>
|
||||
<a-form ref="timeFormRef" :model="timeForm" layout="vertical">
|
||||
<a-form-item :label="t('ms.personal.timeSetting')">
|
||||
<a-radio-group v-model:model-value="timeForm.activeTimeType" type="button">
|
||||
<a-radio value="forever" class="show-type-icon">{{ t('ms.personal.forever') }}</a-radio>
|
||||
<a-radio value="custom" class="show-type-icon">{{ t('ms.personal.custom') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="timeForm.activeTimeType === 'custom'"
|
||||
field="time"
|
||||
:label="t('ms.personal.timeSetting')"
|
||||
:rules="[{ required: true, message: t('ms.personal.expiredTimeRequired') }]"
|
||||
asterisk-position="end"
|
||||
>
|
||||
<a-date-picker
|
||||
v-model:model-value="timeForm.time"
|
||||
show-time
|
||||
:time-picker-props="{ defaultValue: '00:00:00' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
class="w-[240px]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item field="desc" :label="t('ms.personal.accessKeyDesc')">
|
||||
<a-input
|
||||
v-model:model-value="timeForm.desc"
|
||||
:max-length="64"
|
||||
:placeholder="t('ms.personal.accessKeyDescPlaceholder')"
|
||||
show-word-limit
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
|
||||
const { copy } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
|
||||
interface ApiKeyItem {
|
||||
id: string;
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
desc: string;
|
||||
createTime: number;
|
||||
expireTime: number;
|
||||
enable: boolean;
|
||||
desensitization: boolean;
|
||||
isExpire: boolean;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const apiKeyList = ref<ApiKeyItem[]>([
|
||||
{
|
||||
id: '238dy23d2',
|
||||
accessKey: 'dueiouhwded',
|
||||
secretKey: 'asdasdas',
|
||||
desc: '92387yd9283d2',
|
||||
createTime: 1629782400000,
|
||||
expireTime: 1629982400000,
|
||||
enable: true,
|
||||
desensitization: true,
|
||||
isExpire: false,
|
||||
},
|
||||
{
|
||||
id: 'ih02i3d23',
|
||||
accessKey: 'sdshsd',
|
||||
secretKey: 'poj4f',
|
||||
desc: '92387yd9283d2',
|
||||
createTime: 1629782400000,
|
||||
expireTime: 1629982400000,
|
||||
enable: false,
|
||||
desensitization: true,
|
||||
isExpire: true,
|
||||
},
|
||||
{
|
||||
id: '34hy34h3',
|
||||
accessKey: 'sdshsd',
|
||||
secretKey: 'poj4f',
|
||||
desc: '92387yd9283d2',
|
||||
createTime: 1629782400000,
|
||||
expireTime: 1629982400000,
|
||||
enable: false,
|
||||
desensitization: true,
|
||||
isExpire: true,
|
||||
},
|
||||
{
|
||||
id: 'f23',
|
||||
accessKey: 'sdshsd',
|
||||
secretKey: 'poj4f',
|
||||
desc: '92387yd9283d2',
|
||||
createTime: 1629782400000,
|
||||
expireTime: 1629982400000,
|
||||
enable: false,
|
||||
desensitization: true,
|
||||
isExpire: true,
|
||||
},
|
||||
{
|
||||
id: 'ih02i3ed23',
|
||||
accessKey: 'sdshsd',
|
||||
secretKey: 'poj4f',
|
||||
desc: '92387yd9283d2',
|
||||
createTime: 1629782400000,
|
||||
expireTime: 1629982400000,
|
||||
enable: false,
|
||||
desensitization: true,
|
||||
isExpire: true,
|
||||
},
|
||||
]);
|
||||
const actions: ActionsItem[] = [
|
||||
{
|
||||
label: t('ms.personal.validTime'),
|
||||
eventTag: 'time',
|
||||
},
|
||||
{
|
||||
isDivider: true,
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
eventTag: 'delete',
|
||||
},
|
||||
];
|
||||
|
||||
async function handleCopy(val: string) {
|
||||
await copy(val);
|
||||
Message.success(t('ms.personal.copySuccess'));
|
||||
}
|
||||
|
||||
function desensitization(item: ApiKeyItem) {
|
||||
item.desensitization = !item.desensitization;
|
||||
}
|
||||
|
||||
async function handleBeforeEnableChange(item: ApiKeyItem) {
|
||||
if (item.enable) {
|
||||
openModal({
|
||||
type: 'error',
|
||||
title: t('ms.personal.confirmClose'),
|
||||
content: t('ms.personal.closeTip'),
|
||||
okText: t('common.confirmClose'),
|
||||
cancelText: t('common.cancel'),
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
Message.success(t('ms.personal.closeSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Message.success(t('ms.personal.openSuccess'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteApiKey(item: ApiKeyItem) {
|
||||
openModal({
|
||||
type: 'error',
|
||||
title: t('ms.personal.confirmDelete'),
|
||||
content: t('ms.personal.deleteTip'),
|
||||
okText: t('common.confirmDelete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
Message.success(t('common.deleteSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const timeModalVisible = ref(false);
|
||||
const defaultTimeForm = {
|
||||
activeTimeType: 'forever',
|
||||
time: '',
|
||||
desc: '',
|
||||
};
|
||||
const timeForm = ref({ ...defaultTimeForm });
|
||||
const timeFormRef = ref<FormInstance>();
|
||||
|
||||
function handleTimeConfirm(done: (closed: boolean) => void) {
|
||||
timeFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
Message.success(t('common.updateSuccess'));
|
||||
done(true);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
done(false);
|
||||
}
|
||||
} else {
|
||||
done(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleTimeClose() {
|
||||
timeFormRef.value?.resetFields();
|
||||
timeForm.value = { ...defaultTimeForm };
|
||||
}
|
||||
|
||||
function handleMoreActionSelect(item: ActionsItem, apiKey: ApiKeyItem) {
|
||||
if (item.eventTag === 'time') {
|
||||
timeForm.value = {
|
||||
activeTimeType: 'forever',
|
||||
time: apiKey.expireTime ? dayjs(apiKey.expireTime).format('YYYY-MM-DD HH:mm:ss') : '',
|
||||
desc: apiKey.desc,
|
||||
};
|
||||
timeModalVisible.value = true;
|
||||
} else if (item.eventTag === 'delete') {
|
||||
deleteApiKey(apiKey);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.api-list-content {
|
||||
@apply grid flex-1 overflow-auto;
|
||||
.ms-scroll-bar();
|
||||
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(318px, 2fr));
|
||||
padding: 16px;
|
||||
border-radius: var(--border-radius-small);
|
||||
background-color: var(--color-text-n9);
|
||||
}
|
||||
.api-item {
|
||||
@apply bg-white;
|
||||
|
||||
padding: 16px 0;
|
||||
height: 335px;
|
||||
border-radius: var(--border-radius-small);
|
||||
.api-item-label {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
.api-item-value {
|
||||
@apply flex items-center;
|
||||
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
.api-item-value-strong {
|
||||
@apply flex items-center;
|
||||
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
&:hover {
|
||||
.copy-icon {
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
.copy-icon,
|
||||
.eye-icon {
|
||||
@apply cursor-pointer;
|
||||
|
||||
margin-left: 4px;
|
||||
color: var(--color-text-brand);
|
||||
&:hover {
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
}
|
||||
.copy-icon {
|
||||
@apply invisible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,265 @@
|
|||
<template>
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ t('ms.personal.baseInfo') }}</div>
|
||||
<a-button v-if="!isEdit" type="outline" size="mini" class="p-[2px_8px]" @click="isEdit = true">
|
||||
{{ t('common.update') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="mb-[16px] flex items-center">
|
||||
<MsAvatar :avatar="userStore.avatar || 'default'" class="mb-[4px]" />
|
||||
<a-button
|
||||
type="outline"
|
||||
class="arco-btn-outline--secondary ml-[8px] p-[2px_8px]"
|
||||
size="mini"
|
||||
@click="avatarModalVisible = true"
|
||||
>
|
||||
{{ t('ms.personal.changeAvatar') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<a-form v-if="isEdit" ref="baseInfoFormRef" :model="baseInfoForm" layout="vertical">
|
||||
<a-form-item
|
||||
field="name"
|
||||
:label="t('ms.personal.name')"
|
||||
:rules="[{ required: true, message: t('ms.personal.nameRequired') }]"
|
||||
asterisk-position="end"
|
||||
>
|
||||
<a-input
|
||||
v-model:modelValue="baseInfoForm.name"
|
||||
:placeholder="t('ms.personal.namePlaceholder')"
|
||||
:max-length="255"
|
||||
show-word-limit
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="email"
|
||||
:label="t('ms.personal.email')"
|
||||
:rules="[{ required: true, message: t('ms.personal.emailRequired') }, { validator: checkUerEmail }]"
|
||||
asterisk-position="end"
|
||||
>
|
||||
<a-input v-model:modelValue="baseInfoForm.email" :placeholder="t('ms.personal.emailPlaceholder')" />
|
||||
<MsFormItemSub :text="t('ms.personal.emailTip')" :show-fill-icon="false" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="phone"
|
||||
:label="t('ms.personal.phone')"
|
||||
:rules="[{ required: true, message: t('ms.personal.phoneRequired') }, { validator: checkUerPhone }]"
|
||||
asterisk-position="end"
|
||||
>
|
||||
<a-input
|
||||
v-model:modelValue="baseInfoForm.phone"
|
||||
:placeholder="t('ms.personal.phonePlaceholder')"
|
||||
:max-length="11"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" class="mr-[14px]" :loading="updateLoading" @click="updateBaseInfo">
|
||||
{{ t('common.update') }}
|
||||
</a-button>
|
||||
<a-button type="secondary" :disabled="updateLoading" @click="cancelEdit">{{ t('common.cancel') }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<MsDescription v-else :descriptions="descriptions">
|
||||
<template #tag>
|
||||
<MsTag> 组织 1 </MsTag>
|
||||
<br />
|
||||
<MsTag size="small" class="mt-[8px] !bg-[rgb(var(--primary-1))] !text-[rgb(var(--primary-5))]"> 项目 1 </MsTag>
|
||||
</template>
|
||||
</MsDescription>
|
||||
<a-modal
|
||||
v-model:visible="avatarModalVisible"
|
||||
:title="t('ms.personal.changeAvatar')"
|
||||
title-align="start"
|
||||
:ok-text="t('common.save')"
|
||||
class="ms-usemodal"
|
||||
:width="680"
|
||||
@before-ok="handleChangeAvatarConfirm"
|
||||
>
|
||||
<a-radio-group v-model:model-value="activeAvatarType" type="button">
|
||||
<a-radio value="builtIn" class="show-type-icon">{{ t('ms.personal.builtIn') }}</a-radio>
|
||||
<a-radio value="word" class="show-type-icon">{{ t('ms.personal.wordAvatar') }}</a-radio>
|
||||
</a-radio-group>
|
||||
<div v-show="activeAvatarType === 'builtIn'" class="avatar-content">
|
||||
<div class="avatar" @click="changeAvatar('default')">
|
||||
<MsAvatar avatar="default" class="mb-[4px]" />
|
||||
<div class="text-[12px] text-[var(--color-text-1)]">{{ t('ms.personal.default') }}</div>
|
||||
<MsIcon
|
||||
v-if="activeAvatar === 'default'"
|
||||
type="icon-icon_succeed_filled"
|
||||
:style="{ color: 'rgb(var(--success-6))' }"
|
||||
class="check-icon"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="(avatar, index) of avatarList" :key="avatar" class="avatar" @click="changeAvatar(index)">
|
||||
<MsAvatar :avatar="avatar" class="mb-[4px]" />
|
||||
<div class="text-[12px] text-[var(--color-text-1)]">{{ t('ms.personal.avatar', { index: index }) }}</div>
|
||||
<MsIcon
|
||||
v-if="activeAvatar === index"
|
||||
type="icon-icon_succeed_filled"
|
||||
:style="{ color: 'rgb(var(--success-6))' }"
|
||||
class="check-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="activeAvatarType === 'word'" class="mb-[8px] flex flex-wrap gap-[24px] pt-[14px]">
|
||||
<div class="avatar" @click="changeAvatar('word')">
|
||||
<MsAvatar avatar="word" class="mb-[4px]">
|
||||
{{ userStore.name?.substring(0, 4) }}
|
||||
</MsAvatar>
|
||||
<div class="text-[12px] text-[var(--color-text-1)]">{{ t('ms.personal.wordAvatar') }}</div>
|
||||
<MsIcon
|
||||
v-if="activeAvatar === 'word'"
|
||||
type="icon-icon_succeed_filled"
|
||||
:style="{ color: 'rgb(var(--success-6))' }"
|
||||
class="check-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsAvatar from '@/components/pure/ms-avatar/index.vue';
|
||||
import MsDescription, { Description } from '@/components/pure/ms-description/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useUserStore from '@/store/modules/user/index';
|
||||
import { validateEmail, validatePhone } from '@/utils/validate';
|
||||
|
||||
import type { FormInstance } from '@arco-design/web-vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isEdit = ref(false);
|
||||
const descriptions = ref<Description[]>([
|
||||
{
|
||||
label: t('ms.personal.name'),
|
||||
value: userStore.name || '',
|
||||
},
|
||||
{
|
||||
label: t('ms.personal.email'),
|
||||
value: userStore.email || '',
|
||||
},
|
||||
{
|
||||
label: t('ms.personal.phone'),
|
||||
value: userStore.phone || '',
|
||||
},
|
||||
{
|
||||
label: t('ms.personal.org'),
|
||||
value: [],
|
||||
isTag: true,
|
||||
},
|
||||
]);
|
||||
const baseInfoForm = ref({
|
||||
name: userStore.name,
|
||||
email: userStore.email,
|
||||
phone: userStore.phone,
|
||||
});
|
||||
const baseInfoFormRef = ref<FormInstance>();
|
||||
const updateLoading = ref(false);
|
||||
|
||||
function cancelEdit() {
|
||||
isEdit.value = false;
|
||||
baseInfoFormRef.value?.resetFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户邮箱
|
||||
* @param value 输入的值
|
||||
* @param callback 失败回调,入参是提示信息
|
||||
* @param index 当前输入的表单项对应 list 的下标,用于校验重复输入的时候排除自身
|
||||
*/
|
||||
function checkUerEmail(value: string | undefined, callback: (error?: string) => void) {
|
||||
if (value === '' || value === undefined) {
|
||||
callback(t('system.user.createUserEmailNotNull'));
|
||||
} else if (!validateEmail(value)) {
|
||||
callback(t('system.user.createUserEmailErr'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户手机号
|
||||
* @param value 输入的值
|
||||
* @param callback 失败回调,入参是提示信息
|
||||
*/
|
||||
function checkUerPhone(value: string | undefined, callback: (error?: string) => void) {
|
||||
if (value !== '' && value !== undefined && !validatePhone(value)) {
|
||||
callback(t('system.user.createUserPhoneErr'));
|
||||
}
|
||||
}
|
||||
|
||||
function updateBaseInfo() {
|
||||
baseInfoFormRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
updateLoading.value = true;
|
||||
Message.success(t('common.updateSuccess'));
|
||||
isEdit.value = false;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
updateLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const avatarModalVisible = ref(false);
|
||||
|
||||
async function handleChangeAvatarConfirm(done: (closed: boolean) => void) {
|
||||
try {
|
||||
// if (replaceVersion.value !== '') {
|
||||
// await useLatestVersion(replaceVersion.value);
|
||||
// }
|
||||
// await toggleVersionStatus(activeRecord.value.id);
|
||||
Message.success(t('common.updateSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
done(false);
|
||||
} finally {
|
||||
done(true);
|
||||
}
|
||||
}
|
||||
|
||||
const activeAvatarType = ref<'builtIn' | 'word'>('builtIn');
|
||||
const activeAvatar = ref<string | number>('default');
|
||||
const avatarList = ref<string[]>([]);
|
||||
let i = 1;
|
||||
while (i <= 46) {
|
||||
avatarList.value.push(`/images/avatar/avatar-${i}.jpg`);
|
||||
i++;
|
||||
}
|
||||
|
||||
function changeAvatar(avatar: string | number) {
|
||||
activeAvatar.value = avatar;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.avatar-content {
|
||||
@apply grid items-center justify-center;
|
||||
|
||||
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
|
||||
margin-bottom: 8px;
|
||||
gap: 24px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
.avatar {
|
||||
@apply relative flex cursor-pointer flex-col items-center justify-center;
|
||||
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.check-icon {
|
||||
@apply absolute right-0 rounded-full bg-white;
|
||||
|
||||
bottom: 22px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ t('ms.personal.localExecution') }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-[16px]">
|
||||
<div class="config-card">
|
||||
<div class="config-card-title">
|
||||
<div class="config-card-title-text">{{ t('ms.personal.apiLocalExecution') }}</div>
|
||||
<MsTag theme="outline" :type="tagMap[apiConfig.status].type" size="small" class="px-[4px]">
|
||||
{{ tagMap[apiConfig.status].text }}
|
||||
</MsTag>
|
||||
</div>
|
||||
<a-input
|
||||
v-model:model-value="apiConfig.url"
|
||||
:placeholder="t('ms.personal.apiLocalExecutionPlaceholder')"
|
||||
class="mb-[16px]"
|
||||
></a-input>
|
||||
<div class="config-card-footer">
|
||||
<a-button
|
||||
type="outline"
|
||||
class="px-[8px]"
|
||||
size="mini"
|
||||
:disabled="apiConfig.url.trim() === ''"
|
||||
:loading="testApiLoading"
|
||||
@click="testApi"
|
||||
>
|
||||
{{ t('ms.personal.test') }}
|
||||
</a-button>
|
||||
<div class="flex items-center">
|
||||
<div class="mr-[4px] text-[12px] leading-[16px] text-[var(--color-text-4)]">
|
||||
{{ t('ms.personal.priorityLocalExec') }}
|
||||
</div>
|
||||
<a-switch
|
||||
v-model:model-value="apiConfig.isPriorityLocalExec"
|
||||
size="small"
|
||||
:disabled="apiConfig.status !== 1 || testApiLoading"
|
||||
:before-change="(val) => handleApiPriorityBeforeChange(val)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card">
|
||||
<div class="config-card-title">
|
||||
<div class="config-card-title-text">{{ t('ms.personal.uiLocalExecution') }}</div>
|
||||
<MsTag theme="outline" :type="tagMap[uiConfig.status].type" size="small" class="px-[4px]">
|
||||
{{ tagMap[uiConfig.status].text }}
|
||||
</MsTag>
|
||||
</div>
|
||||
<a-input
|
||||
v-model:model-value="uiConfig.url"
|
||||
:placeholder="t('ms.personal.uiLocalExecutionPlaceholder')"
|
||||
class="mb-[16px]"
|
||||
></a-input>
|
||||
<div class="config-card-footer">
|
||||
<a-button
|
||||
type="outline"
|
||||
class="px-[8px]"
|
||||
size="mini"
|
||||
:disabled="uiConfig.url.trim() === ''"
|
||||
:loading="testUiLoading"
|
||||
@click="testUi"
|
||||
>
|
||||
{{ t('ms.personal.test') }}
|
||||
</a-button>
|
||||
<div class="flex items-center">
|
||||
<div class="mr-[4px] text-[12px] leading-[16px] text-[var(--color-text-4)]">
|
||||
{{ t('ms.personal.priorityLocalExec') }}
|
||||
</div>
|
||||
<a-switch
|
||||
v-model:model-value="uiConfig.isPriorityLocalExec"
|
||||
size="small"
|
||||
:disabled="uiConfig.status !== 1 || testUiLoading"
|
||||
:before-change="(val) => handleUiPriorityBeforeChange(val)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsTag, { TagType } from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
type Status = 0 | 1 | 2;
|
||||
interface TagMapItem {
|
||||
type: TagType;
|
||||
text: string;
|
||||
}
|
||||
const tagMap: Record<Status, TagMapItem> = {
|
||||
0: {
|
||||
type: 'default',
|
||||
text: t('ms.personal.unConfig'),
|
||||
},
|
||||
1: {
|
||||
type: 'success',
|
||||
text: t('ms.personal.testPass'),
|
||||
},
|
||||
2: {
|
||||
type: 'danger',
|
||||
text: t('ms.personal.testFail'),
|
||||
},
|
||||
};
|
||||
const testApiLoading = ref(false);
|
||||
const apiConfig = ref({
|
||||
url: '',
|
||||
status: 1 as Status,
|
||||
isPriorityLocalExec: false,
|
||||
});
|
||||
|
||||
async function testApi() {
|
||||
try {
|
||||
testApiLoading.value = true;
|
||||
Message.success(t('ms.personal.testPass'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
testApiLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApiPriorityBeforeChange(val: string | number | boolean) {
|
||||
try {
|
||||
Message.success(val ? t('ms.personal.apiLocalExecutionOpen') : t('ms.personal.apiLocalExecutionClose'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const testUiLoading = ref(false);
|
||||
const uiConfig = ref({
|
||||
url: '',
|
||||
status: 1 as Status,
|
||||
isPriorityLocalExec: false,
|
||||
});
|
||||
|
||||
async function testUi() {
|
||||
try {
|
||||
testApiLoading.value = true;
|
||||
Message.success(t('ms.personal.testPass'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
testApiLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUiPriorityBeforeChange(val: string | number | boolean) {
|
||||
try {
|
||||
Message.success(val ? t('ms.personal.uiLocalExecutionOpen') : t('ms.personal.uiLocalExecutionClose'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.config-card {
|
||||
padding: 16px;
|
||||
border: 2px solid white;
|
||||
border-radius: var(--border-radius-medium);
|
||||
background-color: var(--color-text-n9);
|
||||
box-shadow: 0 6px 15px 0 rgb(120 56 135 / 5%);
|
||||
.config-card-title {
|
||||
@apply flex items-center;
|
||||
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
.config-card-title-text {
|
||||
@apply font-medium;
|
||||
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
.config-card-footer {
|
||||
@apply flex items-center justify-between;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ t('ms.personal.setPsw') }}</div>
|
||||
</div>
|
||||
<a-form ref="formRef" :model="form" layout="vertical" :rules="rules">
|
||||
<a-form-item field="password" :label="t('ms.personal.currentPsw')" asterisk-position="end">
|
||||
<a-input-password
|
||||
v-model="form.password"
|
||||
:placeholder="t('invite.passwordPlaceholder')"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item field="newPsw" :label="t('ms.personal.newPsw')" asterisk-position="end">
|
||||
<MsPasswordInput v-model:password="form.newPsw" />
|
||||
<MsFormItemSub :text="t('ms.personal.changePswTip')" :show-fill-icon="false" />
|
||||
</a-form-item>
|
||||
<a-form-item field="confirmPsw" class="hidden-item">
|
||||
<a-input-password
|
||||
v-model="form.confirmPsw"
|
||||
:placeholder="t('invite.passwordPlaceholder')"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" class="mt-[16px]" :loading="loading" @click="changePsw">
|
||||
{{ t('common.save') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsPasswordInput from '@/components/pure/ms-password-input/index.vue';
|
||||
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { validatePasswordLength, validateWordPassword } from '@/utils/validate';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const form = ref({
|
||||
password: '',
|
||||
newPsw: '',
|
||||
confirmPsw: '',
|
||||
});
|
||||
const formRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
const pswValidateRes = ref(false);
|
||||
const pswLengthValidateRes = ref(false);
|
||||
|
||||
const rules = {
|
||||
password: [{ required: true, message: t('invite.passwordNotNull') }],
|
||||
newPsw: [
|
||||
{ required: true, message: t('invite.passwordNotNull') },
|
||||
{
|
||||
validator: (value: string, callback: (error?: string) => void) => {
|
||||
pswValidateRes.value = validateWordPassword(value);
|
||||
pswLengthValidateRes.value = validatePasswordLength(value);
|
||||
if (!pswLengthValidateRes.value) {
|
||||
callback(t('invite.passwordLengthRule'));
|
||||
} else if (!pswValidateRes.value) {
|
||||
callback(t('invite.passwordWordRule'));
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
confirmPsw: [
|
||||
{ required: true, message: t('invite.repasswordNotNull') },
|
||||
{
|
||||
validator: (value: string, callback: (error?: string) => void) => {
|
||||
if (value !== form.value.newPsw) {
|
||||
callback(t('invite.repasswordNotSame'));
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function changePsw() {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
loading.value = true;
|
||||
Message.success(t('common.updateSuccess'));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,231 @@
|
|||
<template>
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<div class="mb-[16px] flex items-center justify-between">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ t('ms.personal.tripartite') }}</div>
|
||||
</div>
|
||||
<div class="platform-card-container">
|
||||
<div class="platform-card">
|
||||
<div class="mb-[16px] flex items-center">
|
||||
<a-image src="/plugin/image/jira?imagePath=static/jira.jpg" width="24"></a-image>
|
||||
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]">JIRA</div>
|
||||
<a-tooltip :content="t('ms.personal.jiraTip')" position="right">
|
||||
<icon-exclamation-circle
|
||||
class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<MsTag theme="light" :type="tagMap[jiraConfig.status].type" size="small" class="px-[4px]">
|
||||
{{ tagMap[jiraConfig.status].text }}
|
||||
</MsTag>
|
||||
</div>
|
||||
<a-form ref="jiraFormRef" :model="jiraConfig">
|
||||
<a-form-item :label="t('ms.personal.authType')">
|
||||
<a-radio-group v-model:model-value="jiraConfig.authType">
|
||||
<a-radio value="basic">Basic Auth</a-radio>
|
||||
<a-radio value="token">Bearer Token</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('ms.personal.platformAccount')">
|
||||
<a-input
|
||||
v-model:model-value="jiraConfig.platformAccount"
|
||||
:placeholder="t('ms.personal.platformAccountPlaceholder', { type: 'JIRA' })"
|
||||
class="w-[312px]"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('ms.personal.platformPsw')">
|
||||
<a-input-password
|
||||
v-model:model-value="jiraConfig.platformPsw"
|
||||
:placeholder="t('ms.personal.platformPswPlaceholder', { type: 'JIRA' })"
|
||||
class="mr-[8px] w-[312px]"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
></a-input-password>
|
||||
<a-button type="outline" :disabled="jiraConfig.platformAccount === '' || jiraConfig.platformPsw === ''">
|
||||
{{ t('ms.personal.valid') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="platform-card">
|
||||
<div class="mb-[16px] flex items-center">
|
||||
<a-image src="/plugin/image/jira?imagePath=static/jira.jpg" width="24"></a-image>
|
||||
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]">
|
||||
{{ t('ms.personal.zendao') }}
|
||||
</div>
|
||||
<a-tooltip :content="t('ms.personal.zendaoTip')" position="right">
|
||||
<icon-exclamation-circle
|
||||
class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<MsTag theme="light" :type="tagMap[zendaoConfig.status].type" size="small" class="px-[4px]">
|
||||
{{ tagMap[zendaoConfig.status].text }}
|
||||
</MsTag>
|
||||
</div>
|
||||
<a-form ref="zendaoFormRef" :model="zendaoConfig">
|
||||
<a-form-item :label="t('ms.personal.platformAccount')">
|
||||
<a-input
|
||||
v-model:model-value="zendaoConfig.platformAccount"
|
||||
:placeholder="t('ms.personal.platformAccountPlaceholder', { type: t('ms.personal.zendao') })"
|
||||
class="w-[312px]"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('ms.personal.platformPsw')">
|
||||
<a-input-password
|
||||
v-model:model-value="zendaoConfig.platformPsw"
|
||||
:placeholder="t('ms.personal.platformPswPlaceholder', { type: t('ms.personal.zendao') })"
|
||||
class="mr-[8px] w-[312px]"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
></a-input-password>
|
||||
<a-button type="outline" :disabled="zendaoConfig.platformAccount === '' || zendaoConfig.platformPsw === ''">
|
||||
{{ t('ms.personal.valid') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="platform-card">
|
||||
<div class="mb-[16px] flex items-center">
|
||||
<a-image src="/plugin/image/jira?imagePath=static/jira.jpg" width="24"></a-image>
|
||||
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]"> Azure DeVops </div>
|
||||
<a-tooltip :content="t('ms.personal.azureTip')" position="right">
|
||||
<icon-exclamation-circle
|
||||
class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<MsTag theme="light" :type="tagMap[azureConfig.status].type" size="small" class="px-[4px]">
|
||||
{{ tagMap[azureConfig.status].text }}
|
||||
</MsTag>
|
||||
</div>
|
||||
<a-form ref="zendaoFormRef" :model="azureConfig">
|
||||
<a-form-item>
|
||||
<template #label>
|
||||
<div class="flex text-right leading-none"> Personal Access Tokens </div>
|
||||
</template>
|
||||
<a-input
|
||||
v-model:model-value="azureConfig.token"
|
||||
:placeholder="t('ms.personal.azurePlaceholder')"
|
||||
class="mr-[8px] w-[312px]"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
></a-input>
|
||||
<a-button type="outline" :disabled="azureConfig.token === ''">
|
||||
{{ t('ms.personal.valid') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<div class="platform-card">
|
||||
<div class="mb-[16px] flex items-center">
|
||||
<a-image src="/plugin/image/jira?imagePath=static/jira.jpg" width="24"></a-image>
|
||||
<div class="ml-[8px] mr-[4px] font-medium text-[var(--color-text-1)]"> TAPD </div>
|
||||
<a-popover position="right">
|
||||
<template #content>
|
||||
<div class="bg-[var(--color-text-n9)] p-[12px]">
|
||||
<a-image src="/images/tapd-user.png" :width="385"></a-image>
|
||||
</div>
|
||||
</template>
|
||||
<icon-exclamation-circle
|
||||
class="mr-[8px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
/>
|
||||
</a-popover>
|
||||
<MsTag theme="light" :type="tagMap[tapdConfig.status].type" size="small" class="px-[4px]">
|
||||
{{ tagMap[tapdConfig.status].text }}
|
||||
</MsTag>
|
||||
</div>
|
||||
<a-form ref="zendaoFormRef" :model="tapdConfig">
|
||||
<a-form-item :label="t('ms.personal.platformName')">
|
||||
<a-input
|
||||
v-model:model-value="tapdConfig.name"
|
||||
:placeholder="t('ms.personal.platformNamePlaceholder')"
|
||||
class="mr-[8px] w-[312px]"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<a-button type="outline" :disabled="tapdConfig.name === ''">
|
||||
{{ t('ms.personal.valid') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MsTag, { TagType } from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
type Status = 0 | 1 | 2;
|
||||
interface TagMapItem {
|
||||
type: TagType;
|
||||
text: string;
|
||||
}
|
||||
const tagMap: Record<Status, TagMapItem> = {
|
||||
0: {
|
||||
type: 'default',
|
||||
text: t('ms.personal.unValid'),
|
||||
},
|
||||
1: {
|
||||
type: 'success',
|
||||
text: t('ms.personal.validPass'),
|
||||
},
|
||||
2: {
|
||||
type: 'danger',
|
||||
text: t('ms.personal.validFail'),
|
||||
},
|
||||
};
|
||||
|
||||
const jiraConfig = ref({
|
||||
status: 0 as Status,
|
||||
authType: 'basic',
|
||||
platformAccount: '',
|
||||
platformPsw: '',
|
||||
});
|
||||
|
||||
const zendaoConfig = ref({
|
||||
status: 0 as Status,
|
||||
platformAccount: '',
|
||||
platformPsw: '',
|
||||
});
|
||||
|
||||
const azureConfig = ref({
|
||||
status: 0 as Status,
|
||||
token: '',
|
||||
});
|
||||
|
||||
const tapdConfig = ref({
|
||||
status: 0 as Status,
|
||||
name: '',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.platform-card-container {
|
||||
@apply flex flex-1 flex-wrap overflow-auto;
|
||||
.ms-scroll-bar();
|
||||
|
||||
padding: 16px;
|
||||
border-radius: var(--border-radius-small);
|
||||
background-color: var(--color-text-n9);
|
||||
gap: 16px;
|
||||
}
|
||||
.platform-card {
|
||||
@apply w-full bg-white;
|
||||
|
||||
padding: 16px;
|
||||
border-radius: var(--border-radius-small);
|
||||
:deep(.arco-form-item-label) {
|
||||
color: var(--color-text-4) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<MsDrawer v-model:visible="innerVisible" :title="t('ms.personal')" :width="960" :footer="false" no-content-padding>
|
||||
<div class="flex h-full w-full">
|
||||
<div class="h-full w-[208px] bg-[var(--color-text-n9)]">
|
||||
<MsMenuPanel
|
||||
class="h-full !rounded-none bg-[var(--color-text-n9)] p-[16px_24px]"
|
||||
:default-key="activeMenu"
|
||||
:menu-list="menuList"
|
||||
active-class="!bg-transparent font-medium"
|
||||
@toggle-menu="(val) => (activeMenu = val)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 p-[24px]">
|
||||
<baseInfo v-if="activeMenu === 'baseInfo'" />
|
||||
<setPsw v-else-if="activeMenu === 'setPsw'" />
|
||||
<apiKey v-else-if="activeMenu === 'apiKey'" />
|
||||
<localExec v-else-if="activeMenu === 'local'" />
|
||||
<tripartite v-else-if="activeMenu === 'tripartite'" />
|
||||
</div>
|
||||
</div>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsMenuPanel from '@/components/pure/ms-menu-panel/index.vue';
|
||||
import apiKey from './components/apiKey.vue';
|
||||
import baseInfo from './components/baseInfo.vue';
|
||||
import localExec from './components/localExec.vue';
|
||||
import setPsw from './components/setPsw.vue';
|
||||
import tripartite from './components/tripartite.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerVisible = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
innerVisible.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => innerVisible.value,
|
||||
(val) => {
|
||||
emit('update:visible', val);
|
||||
}
|
||||
);
|
||||
|
||||
const activeMenu = ref('baseInfo');
|
||||
const menuList = ref([
|
||||
{
|
||||
name: 'personal',
|
||||
title: t('ms.personal.info'),
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
name: 'baseInfo',
|
||||
title: t('ms.personal.baseInfo'),
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
name: 'setPsw',
|
||||
title: t('ms.personal.setPsw'),
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
name: 'setting',
|
||||
title: t('ms.personal.setting'),
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
name: 'apiKey',
|
||||
title: t('ms.personal.apiKey'),
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
name: 'local',
|
||||
title: t('ms.personal.localExecution'),
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
name: 'tripartite',
|
||||
title: t('ms.personal.tripartite'),
|
||||
level: 2,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,87 @@
|
|||
export default {
|
||||
'ms.personal': 'Personal center',
|
||||
'ms.personal.info': 'Personal information',
|
||||
'ms.personal.baseInfo': 'Basic Information',
|
||||
'ms.personal.setPsw': 'Password settings',
|
||||
'ms.personal.setting': 'Personal settings',
|
||||
'ms.personal.apiKey': 'APIKEY',
|
||||
'ms.personal.tripartite': 'Tripartite account',
|
||||
'ms.personal.changeAvatar': 'Change avatar',
|
||||
'ms.personal.name': 'User name',
|
||||
'ms.personal.namePlaceholder': 'Please enter user name',
|
||||
'ms.personal.nameRequired': 'Username cannot be empty',
|
||||
'ms.personal.email': 'Email',
|
||||
'ms.personal.emailPlaceholder': 'Please input your email',
|
||||
'ms.personal.emailRequired': 'E-mail can not be empty',
|
||||
'ms.personal.emailTip':
|
||||
'After modifying the email address, you need to use the new email address to log in to the system',
|
||||
'ms.personal.phone': 'Phone number',
|
||||
'ms.personal.phonePlaceholder': 'Please enter the phone number',
|
||||
'ms.personal.phoneRequired': 'Phone number can not be blank',
|
||||
'ms.personal.org': 'Organizations and projects',
|
||||
'ms.personal.builtIn': 'System built-in',
|
||||
'ms.personal.wordAvatar': 'Text avatar',
|
||||
'ms.personal.default': 'Default',
|
||||
'ms.personal.avatar': 'Avatar{index}',
|
||||
'ms.personal.currentPsw': 'Current Password',
|
||||
'ms.personal.newPsw': 'New Password',
|
||||
'ms.personal.changePswTip': 'After changing the password, you need to use the new email to log in to the system',
|
||||
'ms.personal.apiKeyTip': 'After adding, you can access MeterSphere',
|
||||
'ms.personal.expireTime': 'Expiration',
|
||||
'ms.personal.expired': 'Expired',
|
||||
'ms.personal.expiredTip': 'The expiration time can be changed in [Settings]',
|
||||
'ms.personal.validTime': 'Effective time',
|
||||
'ms.personal.desc': 'Description',
|
||||
'ms.personal.createTime': 'Created time',
|
||||
'ms.personal.copySuccess': 'Copied successfully',
|
||||
'ms.personal.maxTip': 'Up to 5 APIKEYs can be added',
|
||||
'ms.personal.confirmClose': 'Confirm to close?',
|
||||
'ms.personal.closeTip':
|
||||
'After closing, the test tasks executed using the Access Key will fail. Please operate with caution!',
|
||||
'ms.personal.closeSuccess': 'Closed successfully',
|
||||
'ms.personal.confirmDelete': 'Confirm deletion?',
|
||||
'ms.personal.deleteTip':
|
||||
'After deletion, the test tasks executed using the Access Key will fail. Please operate with caution!',
|
||||
'ms.personal.openSuccess': 'Activated successfully',
|
||||
'ms.personal.forever': 'Permanently valid',
|
||||
'ms.personal.custom': 'Custom',
|
||||
'ms.personal.timeSetting': 'Time setting',
|
||||
'ms.personal.expiredTime': 'Expire date',
|
||||
'ms.personal.expiredTimeRequired': 'Expiration time cannot be empty',
|
||||
'ms.personal.accessKeyDesc': 'Access Key description',
|
||||
'ms.personal.accessKeyDescPlaceholder': 'Please enter Access Key description',
|
||||
'ms.personal.localExecution': 'Execute locally',
|
||||
'ms.personal.apiLocalExecution': 'Interface execution locally',
|
||||
'ms.personal.apiLocalExecutionPlaceholder':
|
||||
'Please enter the local interface execution program URL and press Enter to detect',
|
||||
'ms.personal.apiLocalExecutionOpen': 'Interface local priority execution is enabled',
|
||||
'ms.personal.apiLocalExecutionClose': 'Interface local priority execution is turned off',
|
||||
'ms.personal.uiLocalExecution': 'UI execution locally',
|
||||
'ms.personal.uiLocalExecutionPlaceholder': 'Please enter the local selenium-server address and press Enter to check',
|
||||
'ms.personal.uiLocalExecutionOpen': 'UI local priority execution is enabled',
|
||||
'ms.personal.uiLocalExecutionClose': 'UI local priority execution is turned off',
|
||||
'ms.personal.test': 'Test and save',
|
||||
'ms.personal.testPass': 'Test passed',
|
||||
'ms.personal.testFail': 'Test failed',
|
||||
'ms.personal.unConfig': 'Not configured',
|
||||
'ms.personal.priorityLocalExec': 'Prioritize local execution',
|
||||
'ms.personal.jiraTip':
|
||||
'This information is the user authentication information for submitting defects through Jira. If not filled in, the default information configured by the organization will be used.',
|
||||
'ms.personal.validPass': 'Verification passed',
|
||||
'ms.personal.validFail': 'Verification failed',
|
||||
'ms.personal.unValid': 'Not verified',
|
||||
'ms.personal.valid': 'Verify',
|
||||
'ms.personal.authType': 'Authentication',
|
||||
'ms.personal.platformAccount': 'Account',
|
||||
'ms.personal.platformAccountPlaceholder': 'Please enter {type} account',
|
||||
'ms.personal.platformPsw': 'Password',
|
||||
'ms.personal.platformPswPlaceholder': 'Please enter {type} password',
|
||||
'ms.personal.platformName': 'Nickname',
|
||||
'ms.personal.platformNamePlaceholder': 'Please enter TAPD nickname',
|
||||
'ms.personal.zendao': 'ZenTao',
|
||||
'ms.personal.zendaoTip':
|
||||
'This information is the user name and password for submitting defects through ZenTao. If not filled in, the default information configured by the organization is used.',
|
||||
'ms.personal.azureTip':
|
||||
'This information is the user token information for submitting defects through Azure Devops. If not filled in, the default information configured by the organization will be used.',
|
||||
'ms.personal.azurePlaceholder': 'Please enter Personal Access Tokens',
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
export default {
|
||||
'ms.personal': '个人中心',
|
||||
'ms.personal.info': '个人信息',
|
||||
'ms.personal.baseInfo': '基本信息',
|
||||
'ms.personal.setPsw': '密码设置',
|
||||
'ms.personal.setting': '个人设置',
|
||||
'ms.personal.apiKey': 'APIKEY',
|
||||
'ms.personal.tripartite': '三方平台账号',
|
||||
'ms.personal.changeAvatar': '更换头像',
|
||||
'ms.personal.name': '用户名称',
|
||||
'ms.personal.namePlaceholder': '请输入用户名称',
|
||||
'ms.personal.nameRequired': '用户名称不能为空',
|
||||
'ms.personal.email': '邮箱',
|
||||
'ms.personal.emailPlaceholder': '请输入邮箱',
|
||||
'ms.personal.emailRequired': '邮箱不能为空',
|
||||
'ms.personal.emailTip': '修改邮箱后,需要使用新的邮箱登录系统',
|
||||
'ms.personal.phone': '手机号码',
|
||||
'ms.personal.phonePlaceholder': '请输入手机号码',
|
||||
'ms.personal.phoneRequired': '手机号码不能为空',
|
||||
'ms.personal.org': '组织与项目',
|
||||
'ms.personal.builtIn': '系统内置',
|
||||
'ms.personal.wordAvatar': '文字头像',
|
||||
'ms.personal.default': '默认',
|
||||
'ms.personal.avatar': '头像{index}',
|
||||
'ms.personal.currentPsw': '当前密码',
|
||||
'ms.personal.newPsw': '新密码',
|
||||
'ms.personal.changePswTip': '修改密码后,需要使用新的邮箱登录系统',
|
||||
'ms.personal.apiKeyTip': '新增后,可访问 MeterSphere',
|
||||
'ms.personal.expireTime': '过期时间',
|
||||
'ms.personal.expired': '已到期',
|
||||
'ms.personal.expiredTip': '可在【设置】内更改到期时间',
|
||||
'ms.personal.validTime': '有效时间',
|
||||
'ms.personal.desc': '描述',
|
||||
'ms.personal.createTime': '创建时间',
|
||||
'ms.personal.copySuccess': '复制成功',
|
||||
'ms.personal.maxTip': '最多可添加 5 个APIKEY',
|
||||
'ms.personal.confirmClose': '确认关闭吗',
|
||||
'ms.personal.closeTip': '关闭后,将导致使用该 Access Key 执行的测试任务执行失败,请谨慎操作!',
|
||||
'ms.personal.closeSuccess': '关闭成功',
|
||||
'ms.personal.confirmDelete': '确认删除吗',
|
||||
'ms.personal.deleteTip': '删除后,将导致使用该 Access Key 执行的测试任务执行失败,请谨慎操作!',
|
||||
'ms.personal.openSuccess': '启用成功',
|
||||
'ms.personal.forever': '永久有效',
|
||||
'ms.personal.custom': '自定义',
|
||||
'ms.personal.timeSetting': '时间设置',
|
||||
'ms.personal.expiredTime': '到期时间',
|
||||
'ms.personal.expiredTimeRequired': '到期时间不能为空',
|
||||
'ms.personal.accessKeyDesc': 'Access Key 描述',
|
||||
'ms.personal.accessKeyDescPlaceholder': '请输入Access Key 描述',
|
||||
'ms.personal.localExecution': '本地执行',
|
||||
'ms.personal.apiLocalExecution': '接口本地执行',
|
||||
'ms.personal.apiLocalExecutionPlaceholder': '请输入本地接口执行程序URL,回车检测',
|
||||
'ms.personal.apiLocalExecutionOpen': '接口本地优先执行已开启',
|
||||
'ms.personal.apiLocalExecutionClose': '接口本地优先执行已关闭',
|
||||
'ms.personal.uiLocalExecution': 'UI 本地执行',
|
||||
'ms.personal.uiLocalExecutionPlaceholder': '请输入本地 selenium-server 地址,回车检测',
|
||||
'ms.personal.uiLocalExecutionOpen': 'UI 本地优先执行已开启',
|
||||
'ms.personal.uiLocalExecutionClose': 'UI 本地优先执行已关闭',
|
||||
'ms.personal.test': '检测并保存',
|
||||
'ms.personal.testPass': '检测通过',
|
||||
'ms.personal.testFail': '检测失败',
|
||||
'ms.personal.unConfig': '未配置',
|
||||
'ms.personal.priorityLocalExec': '优先本地执行',
|
||||
'ms.personal.jiraTip': '该信息为通过 Jira 提交缺陷的用户认证信息,若未填写,则使用组织配置的默认信息',
|
||||
'ms.personal.validPass': '校验通过',
|
||||
'ms.personal.validFail': '校验失败',
|
||||
'ms.personal.unValid': '未校验',
|
||||
'ms.personal.valid': '校验',
|
||||
'ms.personal.authType': '认证方式',
|
||||
'ms.personal.platformAccount': '平台账号',
|
||||
'ms.personal.platformAccountPlaceholder': '请输入 {type} 账号',
|
||||
'ms.personal.platformPsw': '平台密码',
|
||||
'ms.personal.platformPswPlaceholder': '请输入 {type} 密码',
|
||||
'ms.personal.platformName': '平台昵称',
|
||||
'ms.personal.platformNamePlaceholder': '请输入 TAPD 昵称',
|
||||
'ms.personal.zendao': '禅道',
|
||||
'ms.personal.zendaoTip': '该信息为通过禅道提交缺陷的的用户名、密码,若未填写,则使用组织配置的默认信息',
|
||||
'ms.personal.azureTip': '该信息为通过Azure Devops提交缺陷的用户令牌信息,若未填写,则使用组织配置的默认信息',
|
||||
'ms.personal.azurePlaceholder': '请输入 Personal Access Tokens',
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<MsIcon v-if="props.avatar === 'default'" type="icon-icon_that_person" size="40" class="text-[var(--color-text-4)]" />
|
||||
<a-avatar v-else-if="props.avatar === 'word'" class="bg-[rgb(var(--primary-1))] text-[rgb(var(--primary-6))]">
|
||||
<slot></slot>
|
||||
</a-avatar>
|
||||
<a-avatar v-else :image-url="avatar"></a-avatar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
avatar: 'default' | 'word' | string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -21,43 +21,45 @@
|
|||
<div class="ms-description-item-value">
|
||||
<slot name="item-value" :item="item">
|
||||
<template v-if="item.isTag">
|
||||
<MsTag
|
||||
v-for="tag of Array.isArray(item.value) ? item.value : [item.value]"
|
||||
:key="`${tag}`"
|
||||
theme="outline"
|
||||
color="var(--color-text-n8)"
|
||||
class="mb-[8px] mr-[8px] font-normal !text-[var(--color-text-1)]"
|
||||
:closable="item.closable"
|
||||
@close="emit('tagClose', tag, item)"
|
||||
>
|
||||
{{ tag }}
|
||||
</MsTag>
|
||||
<span v-if="!item.showTagAdd" v-show="Array.isArray(item.value) && item.value.length === 0">-</span>
|
||||
<div v-else>
|
||||
<template v-if="showTagInput">
|
||||
<a-input
|
||||
ref="inputRef"
|
||||
v-model.trim="addTagInput"
|
||||
size="mini"
|
||||
:error="!!tagInputError"
|
||||
@keyup.enter="handleAddTag(item)"
|
||||
@blur="handleAddTag(item)"
|
||||
>
|
||||
<template #suffix>
|
||||
<icon-loading v-if="tagInputLoading" class="text-[rgb(var(--primary-5))]" />
|
||||
</template>
|
||||
</a-input>
|
||||
<span v-if="tagInputError" class="text-[12px] leading-[16px] text-[rgb(var(--danger-6))]">
|
||||
{{ t('ms.description.addTagRepeat') }}
|
||||
</span>
|
||||
</template>
|
||||
<MsTag v-else type="primary" theme="outline" class="cursor-pointer" @click="handleEdit">
|
||||
<template #icon>
|
||||
<MsIcon type="icon-icon_add_outlined" class="text-[rgb(var(--primary-5))]" />
|
||||
</template>
|
||||
{{ t('ms.description.addTag') }}
|
||||
<slot name="tag" :item="item">
|
||||
<MsTag
|
||||
v-for="tag of Array.isArray(item.value) ? item.value : [item.value]"
|
||||
:key="`${tag}`"
|
||||
theme="outline"
|
||||
color="var(--color-text-n8)"
|
||||
:class="`mb-[8px] mr-[8px] font-normal !text-[var(--color-text-1)] ${item.tagClass || ''}`"
|
||||
:closable="item.closable"
|
||||
@close="emit('tagClose', tag, item)"
|
||||
>
|
||||
{{ tag }}
|
||||
</MsTag>
|
||||
</div>
|
||||
<span v-if="!item.showTagAdd" v-show="Array.isArray(item.value) && item.value.length === 0">-</span>
|
||||
<div v-else>
|
||||
<template v-if="showTagInput">
|
||||
<a-input
|
||||
ref="inputRef"
|
||||
v-model.trim="addTagInput"
|
||||
size="mini"
|
||||
:error="!!tagInputError"
|
||||
@keyup.enter="handleAddTag(item)"
|
||||
@blur="handleAddTag(item)"
|
||||
>
|
||||
<template #suffix>
|
||||
<icon-loading v-if="tagInputLoading" class="text-[rgb(var(--primary-5))]" />
|
||||
</template>
|
||||
</a-input>
|
||||
<span v-if="tagInputError" class="text-[12px] leading-[16px] text-[rgb(var(--danger-6))]">
|
||||
{{ t('ms.description.addTagRepeat') }}
|
||||
</span>
|
||||
</template>
|
||||
<MsTag v-else type="primary" theme="outline" class="cursor-pointer" @click="handleEdit">
|
||||
<template #icon>
|
||||
<MsIcon type="icon-icon_add_outlined" class="text-[rgb(var(--primary-5))]" />
|
||||
</template>
|
||||
{{ t('ms.description.addTag') }}
|
||||
</MsTag>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<MsButton v-else-if="item.isButton" type="text" @click="handleItemClick(item)">
|
||||
{{ item.value }}
|
||||
|
@ -108,6 +110,7 @@
|
|||
value: (string | number) | (string | number)[];
|
||||
key?: string;
|
||||
isTag?: boolean; // 是否标签
|
||||
tagClass?: string; // 标签自定义类名
|
||||
closable?: boolean; // 标签是否可关闭
|
||||
showTagAdd?: boolean; // 是否显示添加标签
|
||||
isButton?: boolean;
|
||||
|
|
|
@ -117,11 +117,6 @@
|
|||
}
|
||||
);
|
||||
|
||||
const contentExtraHeight = computed(() => {
|
||||
// 默认有页脚、内边距时的额外高度146,内边距 30,页脚 60
|
||||
return 146 - (props.noContentPadding ? 24 : 0) - (props.footer ? 0 : 60);
|
||||
});
|
||||
|
||||
const handleContinue = () => {
|
||||
emit('continue');
|
||||
};
|
||||
|
@ -178,6 +173,12 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.arco-scrollbar {
|
||||
@apply h-full;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.arco-drawer {
|
||||
@apply bg-white;
|
||||
|
@ -229,7 +230,4 @@
|
|||
background-color: var(--color-neutral-3);
|
||||
cursor: col-resize;
|
||||
}
|
||||
.arco-scrollbar {
|
||||
@apply h-full;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div class="menu-wrapper">
|
||||
<div class="menu-content">
|
||||
<div v-if="props.title" class="mb-2 font-medium">{{ props.title }}</div>
|
||||
<div class="menu">
|
||||
<div
|
||||
v-for="(item, index) of props.menuList"
|
||||
:key="item.name"
|
||||
class="menu-item px-2"
|
||||
:class="{
|
||||
'text-[--color-text-4]': item.level === 1,
|
||||
'menu-item--active': item.name === currentKey && item.level !== 1,
|
||||
'cursor-pointer': item.level !== 1,
|
||||
'mt-[2px]': item.level === 1 && index !== 0,
|
||||
[props.activeClass || '']: item.name === currentKey && item.level !== 1,
|
||||
}"
|
||||
:style="{
|
||||
'border-top': item.level === 1 && index !== 0 ? '1px solid var(--color-border-2)' : 'none',
|
||||
}"
|
||||
>
|
||||
<div @click="toggleMenu(item.name)">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
defaultKey?: string;
|
||||
menuList: {
|
||||
title: string;
|
||||
level: number;
|
||||
name: string;
|
||||
}[];
|
||||
activeClass?: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggleMenu', val: string): void;
|
||||
}>();
|
||||
|
||||
const currentKey = ref(props.defaultKey);
|
||||
|
||||
const toggleMenu = (itemName: string) => {
|
||||
if (itemName) {
|
||||
currentKey.value = itemName;
|
||||
emit('toggleMenu', itemName);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.menu-wrapper {
|
||||
border-radius: 12px;
|
||||
color: var(--color-text-1);
|
||||
box-shadow: 0 0 10px rgb(120 56 135/ 5%);
|
||||
.menu-content {
|
||||
width: 100%;
|
||||
.menu {
|
||||
.menu-item {
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
font-family: 'PingFang SC';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu-item--active {
|
||||
border-radius: 4px;
|
||||
color: rgb(var(--primary-5));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<a-popover position="tl" trigger="focus" :title="t('ms.passwordInput.passwordTipTitle')">
|
||||
<a-input-password
|
||||
v-model="innerPsw"
|
||||
:placeholder="t('ms.passwordInput.passwordPlaceholder')"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
@input="validatePsw"
|
||||
@clear="validatePsw(innerPsw)"
|
||||
/>
|
||||
<template #content>
|
||||
<div class="check-list-item">
|
||||
<template v-if="pswLengthValidateRes">
|
||||
<icon-check-circle-fill class="check-list-item--success" />{{ t('ms.passwordInput.passwordLengthRule') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<icon-close-circle-fill class="check-list-item--error" />{{ t('ms.passwordInput.passwordLengthRule') }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="check-list-item">
|
||||
<template v-if="pswValidateRes">
|
||||
<icon-check-circle-fill class="check-list-item--success" />{{ t('ms.passwordInput.passwordWordRule') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<icon-close-circle-fill class="check-list-item--error" />{{ t('ms.passwordInput.passwordWordRule') }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { validatePasswordLength, validateWordPassword } from '@/utils/validate';
|
||||
|
||||
const props = defineProps<{
|
||||
password: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:password', val: string): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerPsw = ref(props.password);
|
||||
|
||||
watch(
|
||||
() => props.password,
|
||||
(val) => {
|
||||
innerPsw.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => innerPsw.value,
|
||||
(val) => {
|
||||
emit('update:password', val);
|
||||
}
|
||||
);
|
||||
|
||||
const pswValidateRes = ref(false);
|
||||
const pswLengthValidateRes = ref(false);
|
||||
|
||||
function validatePsw(value: string) {
|
||||
pswValidateRes.value = validateWordPassword(value);
|
||||
pswLengthValidateRes.value = validatePasswordLength(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.check-list-item {
|
||||
@apply flex items-center gap-2;
|
||||
&:first-child {
|
||||
@apply mt-2;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
@apply mb-2;
|
||||
}
|
||||
.check-list-item--success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
.check-list-item--error {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
'ms.passwordInput.passwordPlaceholder': 'Please enter password',
|
||||
'ms.passwordInput.passwordTipTitle': 'Passwords must match both, and only the following rules are supported:',
|
||||
'ms.passwordInput.passwordLengthRule': 'Length is 8-32 bits',
|
||||
'ms.passwordInput.passwordWordRule': 'Must contain numbers and letters, Chinese or spaces are not allowed',
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
'ms.passwordInput.passwordPlaceholder': '请输入密码',
|
||||
'ms.passwordInput.passwordTipTitle': '密码须同时符合,仅支持以下规则',
|
||||
'ms.passwordInput.passwordLengthRule': '长度为8-32位',
|
||||
'ms.passwordInput.passwordWordRule': '必须包含数字和字母,不允许输入中文或空格',
|
||||
};
|
|
@ -6,7 +6,13 @@
|
|||
<template #content>
|
||||
<template v-for="item of props.list">
|
||||
<a-divider v-if="item.isDivider" :key="`${item.label}-divider`" margin="4px" />
|
||||
<a-doption v-else :key="item.label" :class="item.danger ? 'error-6' : ''" :disabled="item.disabled">
|
||||
<a-doption
|
||||
v-else
|
||||
:key="item.label"
|
||||
:class="item.danger ? 'error-6' : ''"
|
||||
:disabled="item.disabled"
|
||||
:value="item.eventTag"
|
||||
>
|
||||
<MsIcon v-if="item.icon" :type="item.icon" />
|
||||
{{ t(item.label || '') }}
|
||||
</a-doption>
|
||||
|
@ -32,7 +38,7 @@
|
|||
const emit = defineEmits(['select', 'close']);
|
||||
|
||||
function selectHandler(value: SelectedValue) {
|
||||
const item = props.list.find((e: ActionsItem) => t(e.label || '') === value);
|
||||
const item = props.list.find((e: ActionsItem) => e.eventTag === value);
|
||||
emit('select', item);
|
||||
}
|
||||
|
||||
|
|
|
@ -137,4 +137,7 @@
|
|||
background: none !important;
|
||||
}
|
||||
}
|
||||
.arco-tag-size-small {
|
||||
line-height: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<MsList :data="filterFileList" :bordered="false" :split="false" item-border no-hover>
|
||||
<template #item="{ item }">
|
||||
<a-list-item
|
||||
class="mb-[8px] rounded-[var(--border-radius-small)] border border-solid border-[var(--color-text-n8)] !p-[8px_12px]"
|
||||
class="mb-[8px] w-full rounded-[var(--border-radius-small)] border border-solid border-[var(--color-text-n8)] !p-[8px_12px]"
|
||||
>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
|
@ -25,11 +25,11 @@
|
|||
:type="getFileIcon(item)"
|
||||
size="24"
|
||||
:class="getFileEnum(item.file?.type) === 'unknown' ? 'text-[var(--color-text-4)]' : ''"
|
||||
></MsIcon>
|
||||
/>
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="font-normal">{{ item.file.name }}</div>
|
||||
<div class="one-line-text max-w-[80%] font-normal">{{ item.file.name }}</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div v-if="item.status === UploadStatus.init" class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
|
||||
|
@ -37,13 +37,17 @@
|
|||
</div>
|
||||
<div
|
||||
v-else-if="item.status === UploadStatus.done"
|
||||
class="text-[12px] leading-[16px] text-[var(--color-text-4)]"
|
||||
class="flex items-center gap-[8px] text-[12px] leading-[16px] text-[var(--color-text-4)]"
|
||||
>
|
||||
{{
|
||||
`${formatFileSize(item.file.size)} ${t('ms.upload.uploadAt')} ${dayjs(item.uploadedTime).format(
|
||||
'YYYY-MM-DD HH:mm:ss'
|
||||
)}`
|
||||
}}
|
||||
<div class="flex items-center">
|
||||
<MsIcon type="icon-icon_succeed_colorful" />
|
||||
{{ t('ms.upload.uploadSuccess') }}
|
||||
</div>
|
||||
</div>
|
||||
<a-progress
|
||||
v-else-if="item.status === UploadStatus.uploading"
|
||||
|
|
|
@ -9,6 +9,7 @@ export default {
|
|||
'ms.upload.uploadAt': 'Uploaded at',
|
||||
'ms.upload.fail': 'Failed',
|
||||
'ms.upload.delete': 'Delete',
|
||||
'ms.upload.uploadSuccess': 'Upload successful',
|
||||
'ms.upload.uploadFail': 'Upload failed',
|
||||
'ms.upload.all': 'All',
|
||||
'ms.upload.uploading': 'Waiting/Uploading',
|
||||
|
|
|
@ -8,6 +8,7 @@ export default {
|
|||
'ms.upload.preview': '预览',
|
||||
'ms.upload.uploadAt': '上传于',
|
||||
'ms.upload.uploadFail': '上传失败',
|
||||
'ms.upload.uploadSuccess': '上传成功',
|
||||
'ms.upload.delete': '删除',
|
||||
'ms.upload.all': '全部',
|
||||
'ms.upload.uploading': '等待/上传中',
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<a-divider direction="vertical" class="ml-0" />
|
||||
<a-select
|
||||
class="w-auto min-w-[150px] max-w-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
|
||||
:default-value="appStore.getCurrentProjectId"
|
||||
:default-value="appStore.currentProjectId"
|
||||
:bordered="false"
|
||||
allow-search
|
||||
@change="selectProject"
|
||||
|
@ -24,7 +24,7 @@
|
|||
<a-tooltip v-for="project of projectList" :key="project.id" :mouse-enter-delay="500" :content="project.name">
|
||||
<a-option
|
||||
:value="project.id"
|
||||
:class="project.id === appStore.getCurrentProjectId ? 'arco-select-option-selected' : ''"
|
||||
:class="project.id === appStore.currentProjectId ? 'arco-select-option-selected' : ''"
|
||||
>
|
||||
{{ project.name }}
|
||||
</a-option>
|
||||
|
@ -178,7 +178,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeMount, Ref, ref } from 'vue';
|
||||
import { computed, onBeforeMount, Ref, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
// import useUser from '@/hooks/useUser';
|
||||
|
@ -213,7 +213,7 @@
|
|||
|
||||
const projectList: Ref<ProjectListItem[]> = ref([]);
|
||||
|
||||
onBeforeMount(async () => {
|
||||
async function initProjects() {
|
||||
try {
|
||||
const res = await getProjectList(appStore.getCurrentOrgId);
|
||||
projectList.value = res;
|
||||
|
@ -221,8 +221,19 @@
|
|||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
initProjects();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => appStore.currentOrgId,
|
||||
async () => {
|
||||
initProjects();
|
||||
}
|
||||
);
|
||||
|
||||
const showProjectSelect = computed(() => {
|
||||
const { getRouteLevelByKey } = usePathMap();
|
||||
// 非项目级别页面不需要展示项目选择器
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
export enum GitPlatformEnum {
|
||||
GITEA = 'Gitea',
|
||||
GITHUB = 'Github',
|
||||
GITLAB = 'Gitlab',
|
||||
GITEE = 'Gitee',
|
||||
OTHER = 'Other',
|
||||
}
|
||||
export enum AuthScopeEnum {
|
||||
SYSTEM = 'SYSTEM',
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
@collapse="setCollapsed"
|
||||
>
|
||||
<div class="menu-wrapper">
|
||||
<Menu />
|
||||
<MsMenu />
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
<a-drawer
|
||||
|
@ -31,7 +31,7 @@
|
|||
:closable="false"
|
||||
@cancel="drawerCancel"
|
||||
>
|
||||
<Menu />
|
||||
<MsMenu />
|
||||
</a-drawer>
|
||||
<a-layout class="layout-content" :style="paddingStyle">
|
||||
<a-spin :loading="appStore.loading" :tip="appStore.loadingTip">
|
||||
|
@ -60,9 +60,9 @@
|
|||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import Footer from '@/components/pure/footer/index.vue';
|
||||
import Menu from '@/components/pure/menu/index.vue';
|
||||
import NavBar from '@/components/pure/navbar/index.vue';
|
||||
import MsBreadCrumb from '@/components/business/ms-breadcrumb/index.vue';
|
||||
import MsMenu from '@/components/business/ms-menu/index.vue';
|
||||
import PageLayout from './page-layout.vue';
|
||||
|
||||
import { GetTitleImgUrl } from '@/api/requrls/setting/config';
|
||||
|
|
|
@ -74,4 +74,5 @@ export default {
|
|||
'common.fork': 'Fork',
|
||||
'common.more': 'More',
|
||||
'common.recycle': 'Recycle Bin',
|
||||
'common.new': 'New',
|
||||
};
|
||||
|
|
|
@ -74,4 +74,5 @@ export default {
|
|||
'common.fork': '关注',
|
||||
'common.more': '更多',
|
||||
'common.recycle': '回收站',
|
||||
'common.new': '新增',
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createRouter, createWebHashHistory } from 'vue-router';
|
|||
import 'nprogress/nprogress.css';
|
||||
import createRouteGuard from './guard';
|
||||
import appRoutes from './routes';
|
||||
import { INVITE_ROUTE, NOT_FOUND_ROUTE, PERSONAL_ROUTE, REDIRECT_MAIN } from './routes/base';
|
||||
import { INVITE_ROUTE, NOT_FOUND_ROUTE, REDIRECT_MAIN } from './routes/base';
|
||||
import NProgress from 'nprogress'; // progress bar
|
||||
|
||||
NProgress.configure({ showSpinner: false }); // NProgress Configuration
|
||||
|
@ -27,7 +27,6 @@ const router = createRouter({
|
|||
REDIRECT_MAIN,
|
||||
NOT_FOUND_ROUTE,
|
||||
INVITE_ROUTE,
|
||||
PERSONAL_ROUTE,
|
||||
],
|
||||
scrollBehavior() {
|
||||
return { top: 0 };
|
||||
|
|
|
@ -38,18 +38,3 @@ export const INVITE_ROUTE: RouteRecordRaw = {
|
|||
hideInMenu: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const PERSONAL_ROUTE: RouteRecordRaw = {
|
||||
path: '/personal',
|
||||
name: 'personal',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {},
|
||||
children: [
|
||||
{
|
||||
path: '/personal/info',
|
||||
name: 'personalInfo',
|
||||
component: () => import('@/views/base/personal/index.vue'),
|
||||
meta: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -110,6 +110,8 @@ const useUserStore = defineStore('user', {
|
|||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -309,7 +309,7 @@ export const downloadUrlFile = (url: string, fileName: string) => {
|
|||
*/
|
||||
export const getHashParameters = (): Record<string, string> => {
|
||||
const query = window.location.hash.split('?')[1]; // 获取 URL 哈希参数部分
|
||||
const paramsArray = query.split('&'); // 将哈希参数字符串分割成数组
|
||||
const paramsArray = query?.split('&') || []; // 将哈希参数字符串分割成数组
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
// 遍历数组并解析参数
|
||||
|
|
|
@ -7,8 +7,7 @@ export const passwordLengthRegex = /^.{8,32}$/;
|
|||
// 密码校验,必须包含数字和字母
|
||||
export const passwordWordRegex = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9!@#$%^&*]+$/;
|
||||
// Git地址校验
|
||||
export const gitRepositoryUrlRegex =
|
||||
/^(?:(?:git:\/\/|https?:\/\/)(?:www\.)?)?(github\.com|gitee\.com)\/([^/]+)\/([^/]+)\.git$/;
|
||||
export const gitRepositoryUrlRegex = /\.git$/;
|
||||
|
||||
/**
|
||||
* 校验邮箱
|
||||
|
|
|
@ -14,34 +14,7 @@
|
|||
<a-input v-model="form.name" :placeholder="t('invite.namePlaceholder')" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item field="password" class="hidden-item">
|
||||
<a-popover position="tl" trigger="focus" :title="t('invite.passwordTipTitle')">
|
||||
<a-input-password
|
||||
v-model="form.password"
|
||||
:placeholder="t('invite.passwordPlaceholder')"
|
||||
allow-clear
|
||||
autocomplete="new-password"
|
||||
@input="validatePsw"
|
||||
@clear="validatePsw(form.password)"
|
||||
/>
|
||||
<template #content>
|
||||
<div class="check-list-item">
|
||||
<template v-if="pswLengthValidateRes">
|
||||
<icon-check-circle-fill class="check-list-item--success" />{{ t('invite.passwordLengthRule') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<icon-close-circle-fill class="check-list-item--error" />{{ t('invite.passwordLengthRule') }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="check-list-item">
|
||||
<template v-if="pswValidateRes">
|
||||
<icon-check-circle-fill class="check-list-item--success" />{{ t('invite.passwordWordRule') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<icon-close-circle-fill class="check-list-item--error" />{{ t('invite.passwordWordRule') }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<MsPasswordInput v-model:password="form.password" />
|
||||
</a-form-item>
|
||||
<a-form-item field="rePassword" class="hidden-item">
|
||||
<a-input-password
|
||||
|
@ -62,6 +35,8 @@
|
|||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { FormInstance, Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsPasswordInput from '@/components/pure/ms-password-input/index.vue';
|
||||
|
||||
import { registerByInvite } from '@/api/modules/setting/user';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { encrypted, sleep } from '@/utils';
|
||||
|
|
|
@ -15,7 +15,11 @@ export default {
|
|||
'invite.passwordLengthRule': 'The length is 8-32 digits',
|
||||
'invite.passwordWordRule': 'Must contain numbers and letters, Chinese or spaces are not allowed',
|
||||
'invite.success': 'Registered successfully',
|
||||
'personal.info': 'Personal Info',
|
||||
'personal.info': 'My Info',
|
||||
'personal.switchOrg': 'Switch Org',
|
||||
'personal.searchOrgPlaceholder': 'Please enter organization name',
|
||||
'personal.currentOrg': 'Current org',
|
||||
'personal.switchOrgLoading': 'Switching org...',
|
||||
'personal.switchOrgSuccess': 'Switching org successfully',
|
||||
'personal.exit': 'Log out',
|
||||
};
|
||||
|
|
|
@ -17,5 +17,9 @@ export default {
|
|||
'invite.success': '注册成功',
|
||||
'personal.info': '个人信息',
|
||||
'personal.switchOrg': '切换组织',
|
||||
'personal.searchOrgPlaceholder': '请输入组织名称',
|
||||
'personal.currentOrg': '当前组织',
|
||||
'personal.switchOrgLoading': '切换组织中...',
|
||||
'personal.switchOrgSuccess': '切换组织成功',
|
||||
'personal.exit': '退出系统',
|
||||
};
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<template>
|
||||
<div> </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -229,6 +229,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 功能测试-用例评审-用例详情
|
||||
*/
|
||||
import { FormInstance } from '@arco-design/web-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
|
|
|
@ -192,6 +192,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 功能测试-用例评审-创建评审
|
||||
*/
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
|
|
|
@ -99,6 +99,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 功能测试-用例评审-评审详情
|
||||
*/
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 功能测试-用例评审-评审列表
|
||||
*/
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
|
|
|
@ -20,8 +20,11 @@
|
|||
:before-change="handleEnableIntercept"
|
||||
:disabled="loading"
|
||||
size="small"
|
||||
class="mr-[8px]"
|
||||
class="mr-[4px]"
|
||||
/>
|
||||
<a-tooltip :content="t('project.fileManagement.uploadTipSingle')">
|
||||
<MsIcon type="icon-icon-maybe_outlined" class="mr-[8px] cursor-pointer hover:text-[rgb(var(--primary-5))]" />
|
||||
</a-tooltip>
|
||||
<MsButton
|
||||
type="icon"
|
||||
status="secondary"
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
|
@ -312,6 +312,12 @@
|
|||
}
|
||||
);
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (props.isModal) {
|
||||
initModules();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
initModules,
|
||||
});
|
||||
|
|
|
@ -44,6 +44,21 @@
|
|||
@batch-action="handleTableBatch"
|
||||
>
|
||||
<template #name="{ record, rowIndex }">
|
||||
<MsTag
|
||||
v-if="record.fileType.toLowerCase() === 'jar'"
|
||||
theme="light"
|
||||
type="success"
|
||||
:self-style="
|
||||
record.enable
|
||||
? {}
|
||||
: {
|
||||
color: 'var(--color-text-4)',
|
||||
backgroundColor: 'var(--color-text-n9)',
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ t(record.enable ? 'common.enable' : 'common.disable') }}
|
||||
</MsTag>
|
||||
<a-tooltip :content="record.name">
|
||||
<a-button type="text" class="px-0" @click="openFileDetail(record.id, rowIndex)">
|
||||
<div class="one-line-text max-w-[168px]">{{ record.name }}</div>
|
||||
|
@ -79,6 +94,7 @@
|
|||
:remote-params="{
|
||||
projectId: appStore.currentProjectId,
|
||||
moduleId: props.activeFolder,
|
||||
moduleIds: isMyOrAllFolder ? [] : [props.activeFolder],
|
||||
fileType: tableFileType,
|
||||
combine,
|
||||
keyword,
|
||||
|
@ -255,7 +271,14 @@
|
|||
<template #title>
|
||||
<div class="flex items-center">
|
||||
{{ isBatchMove ? t('project.fileManagement.batchMoveTitle') : t('project.fileManagement.singleMoveTitle') }}
|
||||
<div class="ml-[4px] text-[var(--color-text-4)]">
|
||||
<div
|
||||
class="one-line-text ml-[4px] max-w-[70%] text-[var(--color-text-4)]"
|
||||
:title="
|
||||
isBatchMove
|
||||
? t('project.fileManagement.batchMoveTitleSub', { count: tableSelected.length })
|
||||
: `(${activeFile?.name})`
|
||||
"
|
||||
>
|
||||
{{
|
||||
isBatchMove
|
||||
? t('project.fileManagement.batchMoveTitleSub', { count: tableSelected.length })
|
||||
|
@ -297,6 +320,7 @@
|
|||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
|
||||
import MsUpload from '@/components/pure/ms-upload/index.vue';
|
||||
import type { MsFileItem, UploadType } from '@/components/pure/ms-upload/types';
|
||||
|
@ -465,7 +489,7 @@
|
|||
title: 'project.fileManagement.name',
|
||||
slotName: 'name',
|
||||
dataIndex: 'name',
|
||||
width: 200,
|
||||
width: 270,
|
||||
},
|
||||
{
|
||||
title: 'project.fileManagement.type',
|
||||
|
@ -757,6 +781,7 @@
|
|||
combine.value.storage = 'minio';
|
||||
}
|
||||
let moduleIds: string[] = [props.activeFolder, ...props.offspringIds];
|
||||
|
||||
if (isMyOrAllFolder.value) {
|
||||
moduleIds = [];
|
||||
}
|
||||
|
|
|
@ -309,7 +309,7 @@
|
|||
id: '',
|
||||
projectId: '',
|
||||
name: '',
|
||||
platform: GitPlatformEnum.GITHUB,
|
||||
platform: GitPlatformEnum.GITEA,
|
||||
url: '',
|
||||
token: '',
|
||||
userName: '',
|
||||
|
|
|
@ -56,6 +56,7 @@ export default {
|
|||
'project.fileManagement.normalFileDesc': 'All file types',
|
||||
'project.fileManagement.jarFile': 'JAR files',
|
||||
'project.fileManagement.jarFileDesc': 'Files used for interface testing',
|
||||
'project.fileManagement.uploadTipSingle': 'Interface test script execution needs to be enabled',
|
||||
'project.fileManagement.uploadTip':
|
||||
'Interface test script execution needs to be enabled, which can be enabled with one click; files can be opened individually',
|
||||
'project.fileManagement.fileTypeTip': 'Switch the file type and the selected/uploaded file list will be cleared.',
|
||||
|
|
|
@ -54,6 +54,7 @@ export default {
|
|||
'project.fileManagement.normalFileDesc': '所有文件类型',
|
||||
'project.fileManagement.jarFile': 'JAR 文件',
|
||||
'project.fileManagement.jarFileDesc': '用于接口测试的文件',
|
||||
'project.fileManagement.uploadTipSingle': '接口测试脚本执行需开启',
|
||||
'project.fileManagement.uploadTip': '接口测试脚本执行需开启,可一键开启;可对文件单独开启',
|
||||
'project.fileManagement.fileTypeTip': '切换文件类型,已选/已上传文件列表会清空',
|
||||
'project.fileManagement.normalFileSubText': '支持任意文件类型,文件大小不超过 {size} MB',
|
||||
|
|
|
@ -1,27 +1,12 @@
|
|||
<template>
|
||||
<div class="wrapper flex min-h-[500px]" :style="{ height: 'calc(100vh - 90px)' }">
|
||||
<div class="left-menu-wrapper mr-[16px] w-[208px] min-w-[208px] bg-white p-[24px]">
|
||||
<div class="left-content">
|
||||
<div class="mb-2 font-medium">{{ t('project.permission.projectAndPermission') }}</div>
|
||||
<div class="menu">
|
||||
<div
|
||||
v-for="(item, index) of menuList"
|
||||
:key="item.key"
|
||||
class="menu-item px-2"
|
||||
:class="{
|
||||
'text-[--color-text-4]': item.level === 1,
|
||||
'is-active': item.name === currentKey && item.level !== 1,
|
||||
'cursor-pointer': item.level !== 1,
|
||||
}"
|
||||
:style="{
|
||||
'border-top': item.level === 1 && index !== 0 ? '1px solid var(--color-border-2)' : 'none',
|
||||
}"
|
||||
>
|
||||
<div @click="toggleMenu(item.name)">{{ t(item.title) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MsMenuPanel
|
||||
:title="t('project.permission.projectAndPermission')"
|
||||
:default-key="currentKey"
|
||||
:menu-list="menuList"
|
||||
class="mr-[16px] w-[208px] min-w-[208px] bg-white p-[24px]"
|
||||
@toggle-menu="toggleMenu"
|
||||
/>
|
||||
<MsCard simple :other-width="290" :min-width="700" :loading="isLoading">
|
||||
<router-view></router-view>
|
||||
</MsCard>
|
||||
|
@ -36,6 +21,7 @@
|
|||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsMenuPanel from '@/components/pure/ms-menu-panel/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
|
@ -48,43 +34,43 @@
|
|||
const menuList = ref([
|
||||
{
|
||||
key: 'project',
|
||||
title: 'project.permission.project',
|
||||
title: t('project.permission.project'),
|
||||
level: 1,
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
key: 'projectBasicInfo',
|
||||
title: 'project.permission.basicInfo',
|
||||
title: t('project.permission.basicInfo'),
|
||||
level: 2,
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_PERMISSION_BASIC_INFO,
|
||||
},
|
||||
{
|
||||
key: 'projectMenuManage',
|
||||
title: 'project.permission.menuManagement',
|
||||
title: t('project.permission.menuManagement'),
|
||||
level: 2,
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_PERMISSION_MENU_MANAGEMENT,
|
||||
},
|
||||
{
|
||||
key: 'projectVersion',
|
||||
title: 'project.permission.projectVersion',
|
||||
title: t('project.permission.projectVersion'),
|
||||
level: 2,
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_PERMISSION_VERSION,
|
||||
},
|
||||
{
|
||||
key: 'memberPermission',
|
||||
title: 'project.permission.memberPermission',
|
||||
title: t('project.permission.memberPermission'),
|
||||
level: 1,
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
key: 'projectMember',
|
||||
title: 'project.permission.member',
|
||||
title: t('project.permission.member'),
|
||||
level: 2,
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_PERMISSION_MEMBER,
|
||||
},
|
||||
{
|
||||
key: 'projectUserGroup',
|
||||
title: 'project.permission.userGroup',
|
||||
title: t('project.permission.userGroup'),
|
||||
level: 2,
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_PERMISSION_USER_GROUP,
|
||||
},
|
||||
|
|