First translation step

This commit is contained in:
Lorenzo
2025-06-25 20:09:29 +02:00
parent f1a25b21a6
commit 3ab99647aa
90 changed files with 2827 additions and 2378 deletions
@@ -8,209 +8,209 @@ import { useSettingStore } from "/@/store/settings";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: false,
},
},
},
search: {
formItem: {
labelCol: {
style: {
// width: "100px"
},
},
wrapperCol: {
style: {
width: "50%",
},
},
},
},
rowHandle: {
minWidth: 200,
fixed: "right",
buttons: {
edit: {
show: false,
},
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100,
},
form: {
show: false,
},
},
userId: {
title: "用户Id",
type: "number",
search: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
}),
},
form: {
show: false,
},
column: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
}),
width: 100,
},
},
pipelineId: {
title: "流水线Id",
type: "number",
search: {
show: true,
},
form: {
show: false,
},
column: {
width: 100,
},
},
pipelineTitle: {
title: "流水线名称",
type: "text",
search: {
show: true,
},
column: {
width: 300,
tooltip: true,
ellipsis: true,
cellRender: ({ row, value }) => {
return <router-link to={{ path: "/certd/pipeline/detail", query: { id: row.pipelineId, editMode: false, historyId: row.id } }}>{value}</router-link>;
},
},
},
triggerType: {
title: "触发类型",
type: "dict-select",
search: {
show: true,
},
dict: dict({
data: [
{ value: "user", label: "手动执行" },
{ value: "timer", label: "定时执行" },
],
}),
form: {
show: false,
value: "custom",
},
column: {
sorter: true,
width: 90,
align: "center",
show: true,
component: {
color: "auto",
},
},
},
status: {
title: "状态",
type: "dict-select",
search: {
show: true,
},
dict: dict({
data: statusUtil.getOptions(),
}),
form: {
show: false,
},
column: {
sorter: true,
width: 120,
align: "center",
},
},
createTime: {
title: "创建时间",
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: "更新时间",
type: "datetime",
form: {
show: false,
},
column: {
show: true,
},
},
},
},
};
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: false,
},
},
},
search: {
formItem: {
labelCol: {
style: {
// width: "100px"
},
},
wrapperCol: {
style: {
width: "50%",
},
},
},
},
rowHandle: {
minWidth: 200,
fixed: "right",
buttons: {
edit: {
show: false,
},
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100,
},
form: {
show: false,
},
},
userId: {
title: t("certd.fields.userId"),
type: "number",
search: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
}),
},
form: {
show: false,
},
column: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
}),
width: 100,
},
},
pipelineId: {
title: t("certd.fields.pipelineId"),
type: "number",
search: {
show: true,
},
form: {
show: false,
},
column: {
width: 100,
},
},
pipelineTitle: {
title: t('certd.fields.pipelineName'),
type: "text",
search: {
show: true,
},
column: {
width: 300,
tooltip: true,
ellipsis: true,
cellRender: ({ row, value }) => {
return <router-link to={{ path: "/certd/pipeline/detail", query: { id: row.pipelineId, editMode: false, historyId: row.id } }}>{value}</router-link>;
},
},
},
triggerType: {
title: t("certd.fields.triggerType"),
type: "dict-select",
search: {
show: true,
},
dict: dict({
data: [
{ value: "user", label: t("certd.triggerTypes.manual") },
{ value: "timer", label: t("certd.triggerTypes.timer") },
],
}),
form: {
show: false,
value: "custom",
},
column: {
sorter: true,
width: 90,
align: "center",
show: true,
component: {
color: "auto",
},
},
},
status: {
title: t("certd.fields.status"),
type: "dict-select",
search: {
show: true,
},
dict: dict({
data: statusUtil.getOptions(),
}),
form: {
show: false,
},
column: {
sorter: true,
width: 120,
align: "center",
},
},
createTime: {
title: t("certd.fields.createTime"),
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: t("certd.fields.updateTime"),
type: "datetime",
form: {
show: false,
},
column: {
show: true,
},
},
},
},
};
}
@@ -1,11 +1,11 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">流水线执行记录</div>
<div class="title">{{ t("certd.pipelineExecutionRecords") }}</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<a-tooltip :title="t('certd.batchDelete')">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
@@ -13,12 +13,16 @@
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineOptions({
name: "PipelineHistory",
@@ -29,17 +33,17 @@ const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
title: t("certd.confirm"),
content: t("certd.confirmBatchDeleteContent", { count: selectedRowKeys.value.length }),
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
message.info(t("certd.deleteSuccess"));
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error("请先勾选记录");
message.error(t("certd.pleaseSelectRecords"));
}
};
@@ -1,9 +1,14 @@
<template>
<a-button v-if="showButton" type="primary" @click="open">修改密码</a-button>
<a-button v-if="showButton" type="primary" @click="open">
{{ $t("passwordForm.changePasswordButton") }}
</a-button>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
import { CrudOptions, useColumns, useFormWrapper } from "@fast-crud/fast-crud";
import * as api from "/@/views/certd/mine/api";
import { notification } from "ant-design-vue";
@@ -17,23 +22,24 @@ let passwordFormRef = ref();
const validatePass1 = async (rule: any, value: any) => {
if (value === "") {
throw new Error("请输入密码");
throw new Error(t("passwordForm.enterPassword"));
}
const formData = passwordFormRef.value.getFormData();
if (formData.confirmNewPassword !== "") {
passwordFormRef.value.formRef.formRef.validateFields(["confirmNewPassword"]);
}
if (formData.password === formData.newPassword) {
throw new Error("新密码不能和旧密码相同");
throw new Error(t("passwordForm.newPasswordNotSameOld"));
}
};
const validatePass2 = async (rule: any, value: any) => {
if (value === "") {
throw new Error("请再次输入密码");
throw new Error(t("passwordForm.enterPasswordAgain"));
} else if (value !== passwordFormRef.value.getFormData().newPassword) {
throw new Error("两次输入密码不一致!");
throw new Error(t("passwordForm.passwordsNotMatch"));
}
};
const userStore = useUserStore();
const { openDialog } = useFormWrapper();
const { buildFormOptions } = useColumns();
@@ -43,7 +49,7 @@ const passwordFormOptions: CrudOptions = {
span: 24,
},
wrapper: {
title: "修改密码",
title: t("passwordForm.title"),
width: "500px",
},
async doSubmit({ form }) {
@@ -52,34 +58,37 @@ const passwordFormOptions: CrudOptions = {
await userStore.loadUserInfo();
},
async afterSubmit() {
notification.success({ message: "修改成功" });
notification.success({ message: t("passwordForm.successMessage") });
},
},
columns: {
password: {
title: "旧密码",
title: t("passwordForm.oldPassword"),
type: "password",
form: {
rules: [{ required: true, message: "请输入旧密码" }],
rules: [{ required: true, message: t("passwordForm.oldPasswordRequired") }],
},
},
newPassword: {
title: "新密码",
title: t("passwordForm.newPassword"),
type: "password",
form: {
rules: [
{ required: true, message: "请输入确认密码" },
{ required: true, message: t("passwordForm.newPasswordRequired") },
//@ts-ignore
{ validator: validatePass1, trigger: "blur" },
],
},
},
confirmNewPassword: {
title: "确认新密码",
title: t("passwordForm.confirmNewPassword"),
type: "password",
form: {
rules: [
{ required: true, message: "请输入确认密码" },
{
required: true,
message: t("passwordForm.confirmNewPasswordRequired"),
},
//@ts-ignore
{ validator: validatePass2, trigger: "blur" },
],
File diff suppressed because it is too large Load Diff
@@ -1,14 +1,14 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">我的流水线</div>
<div class="title">{{ t("certd.myPipelines") }}</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
<div class="batch-actions-inner">
<span> 已选择 {{ selectedRowKeys.length }} </span>
<fs-button icon="ion:trash-outline" class="color-red" type="link" text="批量删除" @click="batchDelete"></fs-button>
<fs-button icon="icon-park-outline:replay-music" class="need-plus" type="link" text="强制重新运行" @click="batchRerun"></fs-button>
<span>{{ t("certd.selectedCount", { count: selectedRowKeys.length }) }}</span>
<fs-button icon="ion:trash-outline" class="color-red" type="link" :text="t('certd.batchDelete')" @click="batchDelete"></fs-button>
<fs-button icon="icon-park-outline:replay-music" class="need-plus" type="link" :text="t('certd.batchForceRerun')" @click="batchRerun"></fs-button>
<change-group class="color-green" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-group>
<change-notification class="color-green" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-notification>
<change-trigger class="color-green" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger>
@@ -16,12 +16,13 @@
</div>
<template #actionbar-right> </template>
<template #form-bottom>
<div>申请证书</div>
<div>{{ t("certd.applyCertificate") }}</div>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted, ref } from "vue";
import { dict, useFs } from "@fast-crud/fast-crud";
@@ -31,6 +32,9 @@ import ChangeGroup from "./components/change-group.vue";
import ChangeTrigger from "./components/change-trigger.vue";
import { Modal, notification } from "ant-design-vue";
import * as api from "./api";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
import ChangeNotification from "/@/views/certd/pipeline/components/change-notification.vue";
defineOptions({
@@ -1,32 +1,46 @@
<template>
<a-modal v-model:open="openRef" class="order-modal" title="订单确认" @ok="orderCreate">
<a-modal v-model:open="openRef" class="order-modal" :title="$t('order.confirmTitle')" @ok="orderCreate">
<div v-if="product" class="order-box">
<div class="flex-o mt-5"><span class="label">套餐</span>{{ product.title }}</div>
<div class="flex-o mt-5"><span class="label">说明</span>{{ product.intro }}</div>
<div class="flex-o mt-5">
<span class="label">规格</span>
<span class="label">{{$t('order.package')}}</span>{{ product.title }}
</div>
<div class="flex-o mt-5">
<span class="label">{{$t('order.description')}}</span>{{ product.intro }}
</div>
<div class="flex-o mt-5">
<span class="label">{{$t('order.specifications')}}</span>
<span class="flex-o flex-wrap">
<span class="flex-o">流水线<suite-value class="ml-5" :model-value="product.content.maxPipelineCount" unit="条" /></span>
<span class="flex-o">域名<suite-value class="ml-5" :model-value="product.content.maxDomainCount" unit="" /></span>
<span class="flex-o">部署次数<suite-value class="ml-5" :model-value="product.content.maxDeployCount" unit="次" /></span>
<span class="flex-o">
{{$t('order.pipeline')}}<suite-value class="ml-5" :model-value="product.content.maxPipelineCount" unit="{{$t('order.unit.pieces')}}" />
</span>
<span class="flex-o">
{{$t('order.domain')}}<suite-value class="ml-5" :model-value="product.content.maxDomainCount" unit="{{$t('order.unit.count')}}" />
</span>
<span class="flex-o">
{{$t('order.deployTimes')}}<suite-value class="ml-5" :model-value="product.content.maxDeployCount" unit="{{$t('order.unit.times')}}" />
</span>
</span>
</div>
<div class="flex-o mt-5">
<span class="label">时长</span>
<span class="label">{{$t('order.duration')}}</span>
<duration-value v-model="formRef.duration"></duration-value>
</div>
<div class="flex-o mt-5"><span class="label">价格</span> <price-input :edit="false" :model-value="durationSelected.price"></price-input></div>
<div class="flex-o mt-5">
<span class="label">{{$t('order.price')}}</span>
<price-input :edit="false" :model-value="durationSelected.price"></price-input>
</div>
<div class="flex-o mt-5">
<span class="label">支付方式</span>
<div v-if="durationSelected.price === 0">免费</div>
<span class="label">{{$t('order.paymentMethod')}}</span>
<div v-if="durationSelected.price === 0">{{$t('order.free')}}</div>
<fs-dict-select v-else v-model:value="formRef.payType" :dict="paymentsDictRef" style="width: 200px"> </fs-dict-select>
</div>
</div>
</a-modal>
</template>
<script setup lang="tsx">
import { ref } from "vue";
import { GetPaymentTypes, OrderModalOpenReq, TradeCreate } from "/@/views/certd/suite/api";
@@ -11,19 +11,21 @@
<div class="text">
<div class="left">
<div>
<span>您好{{ userInfo.nickName || userInfo.username }} 欢迎使用 {{ siteInfo.title }}</span>
<span>{{ t('certd.dashboard.greeting', { name: userInfo.nickName || userInfo.username, site: siteInfo.title }) }}</span>
</div>
<div class="flex-o flex-wrap profile-badges">
<a-tooltip :title="deltaTimeTip">
<a-badge :dot="deltaTimeWarning">
<a-tag :color="deltaTimeWarning ? 'red' : 'green'" class="flex-inline pointer"> <fs-icon icon="ion:time-outline"></fs-icon> {{ now }}</a-tag>
<a-tag :color="deltaTimeWarning ? 'red' : 'green'" class="flex-inline pointer">
<fs-icon icon="ion:time-outline"></fs-icon> {{ now }}
</a-tag>
</a-badge>
</a-tooltip>
<template v-if="userStore.isAdmin">
<a-divider type="vertical" />
<a-badge :dot="hasNewVersion">
<a-tag color="blue" class="flex-inline pointer mr-0" :title="'最新版本:' + latestVersion" @click="openUpgradeUrl()">
<a-tag color="blue" class="flex-inline pointer mr-0" :title="t('certd.dashboard.latestVersion', { version: latestVersion })" @click="openUpgradeUrl()">
<fs-icon icon="ion:rocket-outline" class="mr-5"></fs-icon>
v{{ version }}
</a-tag>
@@ -37,7 +39,7 @@
</template>
<template v-if="settingsStore.isPlus && settingsStore.sysPublic.userValidTimeEnabled === true && userInfo.validTime">
<a-divider type="vertical" />
<valid-time-format class="flex-o" prefix="账户有效期:" :model-value="userInfo.validTime" />
<valid-time-format class="flex-o" :prefix="t('certd.dashboard.validUntil')" :model-value="userInfo.validTime" />
</template>
</div>
</div>
@@ -46,9 +48,9 @@
<div class="suggest hidden md:block">
<tutorial-button class="flex-center mt-2">
<a-tooltip title="点击查看详细教程">
<a-tooltip :title="t('certd.dashboard.tutorialTooltip')">
<a-tag color="blue" class="flex-center">
仅需3步全自动申请部署证书
{{ t('certd.dashboard.tutorialText') }}
<fs-icon class="font-size-16 ml-5" icon="mingcute:question-line"></fs-icon>
</a-tag>
</a-tooltip>
@@ -59,9 +61,10 @@
<div v-if="!settingStore.isComm" class="warning">
<a-alert type="warning" show-icon>
<template #message>
证书和授权为敏感信息不要使用来历不明的在线Certd服务和镜像以免泄露请务必私有化部署使用认准官方版本发布渠道
<a class="ml-5 flex-inline" href="https://gitee.com/certd/certd" target="_blank">gitee</a> <a class="ml-5 flex-inline" href="https://github.com/certd/certd" target="_blank">github</a>
<a class="ml-5 flex-inline" href="https://certd.docmirror.cn" target="_blank">帮助文档</a>
{{ t('certd.dashboard.alertMessage') }}
<a class="ml-5 flex-inline" href="https://gitee.com/certd/certd" target="_blank">gitee</a>
<a class="ml-5 flex-inline" href="https://github.com/certd/certd" target="_blank">github</a>
<a class="ml-5 flex-inline" href="https://certd.docmirror.cn" target="_blank">{{ t('certd.dashboard.helpDoc') }}</a>
</template>
</a-alert>
</div>
@@ -69,30 +72,32 @@
<div class="statistic-data m-20">
<a-row :gutter="20" class="flex-wrap">
<a-col :md="6" :xs="24">
<statistic-card title="证书流水线数量" :count="count.pipelineCount">
<statistic-card :title="t('certd.dashboard.pipelineCount')" :count="count.pipelineCount">
<template v-if="count.pipelineCount === 0" #default>
<div class="flex-center flex-1 flex-col">
<div style="font-size: 18px; font-weight: 700">您还没有证书流水线</div>
<fs-button type="primary" class="mt-10" icon="ion:add-circle-outline" @click="goPipeline">立即创建</fs-button>
<div style="font-size: 18px; font-weight: 700">{{ t('certd.dashboard.noPipeline') }}</div>
<fs-button type="primary" class="mt-10" icon="ion:add-circle-outline" @click="goPipeline">{{ t('certd.dashboard.createNow') }}</fs-button>
</div>
</template>
<template #footer>
<router-link to="/certd/pipeline" class="flex"><fs-icon icon="ion:settings-outline" class="mr-5 fs-16" /> 管理流水线</router-link>
<router-link to="/certd/pipeline" class="flex">
<fs-icon icon="ion:settings-outline" class="mr-5 fs-16" /> {{ t('certd.dashboard.managePipeline') }}
</router-link>
</template>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card title="流水线状态" :footer="false">
<statistic-card :title="t('certd.dashboard.pipelineStatus')" :footer="false">
<pie-count v-if="count.pipelineStatusCount" :data="count.pipelineStatusCount"></pie-count>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card title="最近运行统计" :footer="false">
<day-count v-if="count.historyCountPerDay" :data="count.historyCountPerDay" title="运行次数"></day-count>
<statistic-card :title="t('certd.dashboard.recentRun')" :footer="false">
<day-count v-if="count.historyCountPerDay" :data="count.historyCountPerDay" :title="t('certd.dashboard.runCount')"></day-count>
</statistic-card>
</a-col>
<a-col :md="6" :xs="24">
<statistic-card title="最快到期证书">
<statistic-card :title="t('certd.dashboard.expiringCerts')">
<expiring-list v-if="count.expiringList" :data="count.expiringList"></expiring-list>
</statistic-card>
</a-col>
@@ -102,7 +107,8 @@
<div v-if="pluginGroups" class="plugin-list">
<a-card>
<template #title>
已支持的部署任务总览 <a-tag color="green">{{ pluginGroups.groups.all.plugins.length }}</a-tag>
{{ t('certd.dashboard.supportedTasks') }}
<a-tag color="green">{{ pluginGroups.groups.all.plugins.length }}</a-tag>
</template>
<a-row :gutter="10">
<a-col v-for="item of pluginGroups.groups.all.plugins" :key="item.name" class="plugin-item-col" :xl="4" :md="6" :xs="24">
@@ -130,6 +136,7 @@
</div>
</template>
<script lang="ts" setup>
import { FsIcon } from "@fast-crud/fast-crud";
import SimpleSteps from "/@/components/tutorial/simple-steps.vue";
@@ -148,6 +155,8 @@ import { UserInfoRes } from "/@/store/user/api.user";
import { GetStatisticCount } from "/@/views/framework/home/dashboard/api";
import { useRouter } from "vue-router";
import * as api from "./api";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
import { usePluginStore } from "/@/store/plugin";
defineOptions({
name: "DashboardUser",
@@ -2,7 +2,10 @@
<fs-page class="home—index bg-neutral-100 dark:bg-black">
<!-- <page-content />-->
<dashboard-user />
<change-password-button ref="changePasswordButtonRef" :show-button="false"></change-password-button>
<change-password-button
ref="changePasswordButtonRef"
:show-button="false"
></change-password-button>
</fs-page>
</template>
@@ -12,20 +15,23 @@ import { useUserStore } from "/@/store/user";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { onMounted, ref } from "vue";
import { Modal } from "ant-design-vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const userStore = useUserStore();
const changePasswordButtonRef = ref();
onMounted(() => {
if (userStore.getUserInfo.isWeak === true) {
Modal.info({
title: "修改密码",
content: "为了您的账户安全,请立即修改密码",
title: t("passwordForm.title"),
content: t("passwordForm.weakPasswordWarning"),
onOk: () => {
changePasswordButtonRef.value.open({
password: "123456",
});
},
okText: "立即修改",
okText: t("passwordForm.changeNow"),
});
}
});
@@ -1,74 +1,96 @@
<template>
<div class="main login-page">
<a-form v-if="!twoFactor.loginId" ref="formRef" class="user-layout-login" name="custom-validation" :model="formState" v-bind="layout" @finish="handleFinish" @finish-failed="handleFinishFailed">
<!-- <div class="login-title">登录</div>-->
<a-tabs v-model:active-key="formState.loginType" :tab-bar-style="{ textAlign: 'center', borderBottom: 'unset' }">
<a-tab-pane key="password" tab="密码登录" :disabled="sysPublicSettings.passwordLoginEnabled !== true">
<template v-if="formState.loginType === 'password'">
<!-- <div class="login-title">登录</div>-->
<a-form-item required has-feedback name="username" :rules="rules.username">
<a-input v-model:value="formState.username" placeholder="请输入用户名/邮箱/手机号" autocomplete="off">
<template #prefix>
<fs-icon icon="ion:phone-portrait-outline"></fs-icon>
</template>
</a-input>
</a-form-item>
<a-form-item has-feedback name="password" :rules="rules.password">
<a-input-password v-model:value="formState.password" placeholder="请输入密码" autocomplete="off">
<template #prefix>
<fs-icon icon="ion:lock-closed-outline"></fs-icon>
</template>
</a-input-password>
</a-form-item>
</template>
</a-tab-pane>
<a-tab-pane key="sms" tab="短信验证码登录" :disabled="sysPublicSettings.smsLoginEnabled !== true">
<template v-if="formState.loginType === 'sms'">
<a-form-item has-feedback name="mobile" :rules="rules.mobile">
<a-input v-model:value="formState.mobile" placeholder="请输入手机号" autocomplete="off">
<template #prefix>
<fs-icon icon="ion:phone-portrait-outline"></fs-icon>
</template>
</a-input>
</a-form-item>
<a-form-item has-feedback name="imgCode">
<image-code v-model:value="formState.imgCode" v-model:random-str="formState.randomStr"></image-code>
</a-form-item>
<div class="main login-page">
<a-form v-if="!twoFactor.loginId" ref="formRef" class="user-layout-login" name="custom-validation"
:model="formState" v-bind="layout" @finish="handleFinish" @finish-failed="handleFinishFailed">
<!-- <div class="login-title">登录</div>-->
<a-tabs v-model:active-key="formState.loginType"
:tab-bar-style="{ textAlign: 'center', borderBottom: 'unset' }">
<a-tab-pane key="password" :tab="$t('authentication.passwordTab')"
:disabled="sysPublicSettings.passwordLoginEnabled !== true">
<template v-if="formState.loginType === 'password'">
<!-- <div class="login-title">登录</div>-->
<a-form-item required has-feedback name="username" :rules="rules.username">
<a-input v-model:value="formState.username"
:placeholder="$t('authentication.usernamePlaceholder')" autocomplete="off">
<template #prefix>
<fs-icon icon="ion:phone-portrait-outline"></fs-icon>
</template>
</a-input>
</a-form-item>
<a-form-item has-feedback name="password" :rules="rules.password">
<a-input-password v-model:value="formState.password"
:placeholder="$t('authentication.passwordPlaceholder')" autocomplete="off">
<template #prefix>
<fs-icon icon="ion:lock-closed-outline"></fs-icon>
</template>
</a-input-password>
</a-form-item>
</template>
</a-tab-pane>
<a-tab-pane key="sms" :tab="$t('authentication.smsTab')"
:disabled="sysPublicSettings.smsLoginEnabled !== true">
<template v-if="formState.loginType === 'sms'">
<a-form-item has-feedback name="mobile" :rules="rules.mobile">
<a-input v-model:value="formState.mobile"
:placeholder="$t('authentication.mobilePlaceholder')" autocomplete="off">
<template #prefix>
<fs-icon icon="ion:phone-portrait-outline"></fs-icon>
</template>
</a-input>
</a-form-item>
<a-form-item name="smsCode" :rules="rules.smsCode">
<sms-code v-model:value="formState.smsCode" :img-code="formState.imgCode" :mobile="formState.mobile" :phone-code="formState.phoneCode" :random-str="formState.randomStr" />
</a-form-item>
</template>
</a-tab-pane>
</a-tabs>
<a-form-item>
<a-button type="primary" size="large" html-type="submit" :loading="loading" class="login-button">登录</a-button>
<a-form-item has-feedback name="imgCode">
<image-code v-model:value="formState.imgCode"
v-model:random-str="formState.randomStr"></image-code>
</a-form-item>
<div v-if="!settingStore.isComm" class="mt-2"><a href="https://certd.docmirror.cn/guide/use/forgotpasswd/" target="_blank">忘记管理员密码</a></div>
</a-form-item>
<a-form-item name="smsCode" :rules="rules.smsCode">
<sms-code v-model:value="formState.smsCode" :img-code="formState.imgCode"
:mobile="formState.mobile" :phone-code="formState.phoneCode"
:random-str="formState.randomStr" />
</a-form-item>
</template>
</a-tab-pane>
</a-tabs>
<a-form-item>
<a-button type="primary" size="large" html-type="submit" :loading="loading" class="login-button">
{{ $t('authentication.loginButton') }}
</a-button>
<a-form-item class="user-login-other">
<router-link v-if="hasRegisterTypeEnabled()" class="register" :to="{ name: 'register' }"> 注册 </router-link>
</a-form-item>
</a-form>
<a-form v-else ref="twoFactorFormRef" class="user-layout-login" :model="twoFactor" v-bind="layout">
<div class="mb-10 flex flex-center">请打开您的Authenticator APP获取动态验证码</div>
<a-form-item name="verifyCode">
<a-input ref="verifyCodeInputRef" v-model:value="twoFactor.verifyCode" placeholder="请输入动态验证码" allow-clear @keydown.enter="handleTwoFactorSubmit">
<template #prefix>
<fs-icon icon="ion:lock-closed-outline"></fs-icon>
</template>
</a-input>
</a-form-item>
<a-form-item>
<loading-button type="primary" size="large" html-type="button" class="login-button" :click="handleTwoFactorSubmit">OTP验证登录</loading-button>
</a-form-item>
<div v-if="!settingStore.isComm" class="mt-2">
<a href="https://certd.docmirror.cn/guide/use/forgotpasswd/" target="_blank">
{{ $t('authentication.forgotAdminPassword') }}
</a>
</div>
</a-form-item>
<a-form-item class="user-login-other">
<a class="register" @click="twoFactor.loginId = null"> 返回 </a>
</a-form-item>
</a-form>
</div>
<a-form-item class="user-login-other">
<router-link v-if="hasRegisterTypeEnabled()" class="register" :to="{ name: 'register' }">
{{ $t('authentication.registerLink') }}
</router-link>
</a-form-item>
</a-form>
<a-form v-else ref="twoFactorFormRef" class="user-layout-login" :model="twoFactor" v-bind="layout">
<div class="mb-10 flex flex-center">请打开您的Authenticator APP获取动态验证码</div>
<a-form-item name="verifyCode">
<a-input ref="verifyCodeInputRef" v-model:value="twoFactor.verifyCode" placeholder="请输入动态验证码"
allow-clear @keydown.enter="handleTwoFactorSubmit">
<template #prefix>
<fs-icon icon="ion:lock-closed-outline"></fs-icon>
</template>
</a-input>
</a-form-item>
<a-form-item>
<loading-button type="primary" size="large" html-type="button" class="login-button"
:click="handleTwoFactorSubmit">OTP验证登录</loading-button>
</a-form-item>
<a-form-item class="user-login-other">
<a class="register" @click="twoFactor.loginId = null"> 返回 </a>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick, reactive, ref, toRaw } from "vue";
@@ -79,200 +101,208 @@ import ImageCode from "/@/views/framework/login/image-code.vue";
import SmsCode from "/@/views/framework/login/sms-code.vue";
export default defineComponent({
name: "LoginPage",
components: { SmsCode, ImageCode },
setup() {
const verifyCodeInputRef = ref();
const loading = ref(false);
const userStore = useUserStore();
const settingStore = useSettingStore();
const formRef = ref();
const formState = reactive({
username: "",
phoneCode: "86",
mobile: "",
password: "",
loginType: "password", //password
imgCode: "",
smsCode: "",
randomStr: "",
});
name: "LoginPage",
components: { SmsCode, ImageCode },
setup() {
const verifyCodeInputRef = ref();
const loading = ref(false);
const userStore = useUserStore();
const settingStore = useSettingStore();
const formRef = ref();
const formState = reactive({
username: "",
phoneCode: "86",
mobile: "",
password: "",
loginType: "password", //password
imgCode: "",
smsCode: "",
randomStr: "",
});
const rules = {
mobile: [
{
required: true,
message: "请输入手机号",
},
],
username: [
{
required: true,
message: "请输入用户名",
},
],
password: [
{
required: true,
message: "请输入登录密码",
},
],
smsCode: [
{
required: true,
message: "请输入短信验证码",
},
],
};
const layout = {
labelCol: {
span: 0,
},
wrapperCol: {
span: 24,
},
};
const rules = {
mobile: [
{
required: true,
message: "请输入手机号",
},
],
username: [
{
required: true,
message: "请输入用户名",
},
],
password: [
{
required: true,
message: "请输入登录密码",
},
],
smsCode: [
{
required: true,
message: "请输入短信验证码",
},
],
};
const layout = {
labelCol: {
span: 0,
},
wrapperCol: {
span: 24,
},
};
const twoFactor = reactive({
loginId: "",
verifyCode: "",
});
const twoFactor = reactive({
loginId: "",
verifyCode: "",
});
const handleTwoFactorSubmit = async () => {
await userStore.loginByTwoFactor(twoFactor);
};
const handleTwoFactorSubmit = async () => {
await userStore.loginByTwoFactor(twoFactor);
};
const handleFinish = async (values: any) => {
loading.value = true;
try {
const loginType = formState.loginType;
await userStore.login(loginType, toRaw(formState));
} catch (e: any) {
//@ts-ignore
if (e.code === 10020) {
//双重认证
//@ts-ignore
twoFactor.loginId = e.data;
await nextTick();
verifyCodeInputRef.value.focus();
} else {
throw e;
}
} finally {
loading.value = false;
}
};
const handleFinish = async (values: any) => {
loading.value = true;
try {
const loginType = formState.loginType;
await userStore.login(loginType, toRaw(formState));
} catch (e: any) {
//@ts-ignore
if (e.code === 10020) {
//双重认证
//@ts-ignore
twoFactor.loginId = e.data;
await nextTick();
verifyCodeInputRef.value.focus();
} else {
throw e;
}
} finally {
loading.value = false;
}
};
const handleFinishFailed = (errors: any) => {
utils.logger.log(errors);
};
const handleFinishFailed = (errors: any) => {
utils.logger.log(errors);
};
const resetForm = () => {
formRef.value.resetFields();
};
const resetForm = () => {
formRef.value.resetFields();
};
const isLoginError = ref();
const isLoginError = ref();
const sysPublicSettings = settingStore.getSysPublic;
const sysPublicSettings = settingStore.getSysPublic;
function hasRegisterTypeEnabled() {
return sysPublicSettings.registerEnabled && (sysPublicSettings.usernameRegisterEnabled || sysPublicSettings.emailRegisterEnabled);
}
function hasRegisterTypeEnabled() {
return sysPublicSettings.registerEnabled && (sysPublicSettings.usernameRegisterEnabled || sysPublicSettings.emailRegisterEnabled);
}
return {
loading,
formState,
formRef,
rules,
layout,
handleFinishFailed,
handleFinish,
resetForm,
isLoginError,
sysPublicSettings,
hasRegisterTypeEnabled,
twoFactor,
handleTwoFactorSubmit,
verifyCodeInputRef,
settingStore,
};
},
return {
loading,
formState,
formRef,
rules,
layout,
handleFinishFailed,
handleFinish,
resetForm,
isLoginError,
sysPublicSettings,
hasRegisterTypeEnabled,
twoFactor,
handleTwoFactorSubmit,
verifyCodeInputRef,
settingStore,
};
},
});
</script>
<style lang="less">
@import "../../../style/theme/index.less";
.login-page.main {
//margin: 20px !important;
margin-bottom: 100px;
.user-layout-login {
//label {
// font-size: 14px;
//}
//margin: 20px !important;
margin-bottom: 100px;
.login-title {
color: @primary-color;
font-size: 18px;
text-align: center;
margin: 20px;
}
.getCaptcha {
display: block;
width: 100%;
}
.user-layout-login {
//label {
// font-size: 14px;
//}
.image-code {
height: 34px;
}
.input-right {
width: 160px;
margin-left: 10px;
}
.forge-password {
font-size: 14px;
}
.login-title {
color: @primary-color;
font-size: 18px;
text-align: center;
margin: 20px;
}
button.login-button {
padding: 0 15px;
font-size: 16px;
width: 100%;
}
.getCaptcha {
display: block;
width: 100%;
}
.user-login-other {
text-align: left;
margin-top: 30px;
margin-bottom: 30px;
//line-height: 22px;
.image-code {
height: 34px;
}
.item-icon {
font-size: 24px;
color: rgba(0, 0, 0, 0.2);
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
.input-right {
width: 160px;
margin-left: 10px;
}
&:hover {
color: @primary-color;
}
}
.forge-password {
font-size: 14px;
}
.register {
float: right;
}
}
.fs-icon {
color: rgba(0, 0, 0, 0.45);
margin-right: 4px;
}
.ant-input-affix-wrapper {
line-height: 1.8 !important;
font-size: 14px !important;
> * {
line-height: 1.8 !important;
font-size: 14px !important;
}
}
}
button.login-button {
padding: 0 15px;
font-size: 16px;
width: 100%;
}
.user-login-other {
text-align: left;
margin-top: 30px;
margin-bottom: 30px;
//line-height: 22px;
.item-icon {
font-size: 24px;
color: rgba(0, 0, 0, 0.2);
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
.register {
float: right;
}
}
.fs-icon {
color: rgba(0, 0, 0, 0.45);
margin-right: 4px;
}
.ant-input-affix-wrapper {
line-height: 1.8 !important;
font-size: 14px !important;
>* {
line-height: 1.8 !important;
font-size: 14px !important;
}
}
}
}
</style>