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
@@ -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>