Merge branch 'v2-translation' into v2-dev

# Conflicts:
#	packages/ui/certd-client/src/components/plugins/common/remote-select.vue
#	packages/ui/certd-client/src/router/source/modules/certd.ts
#	packages/ui/certd-client/src/views/certd/pipeline/certd-form/use.tsx
#	packages/ui/certd-client/src/views/certd/pipeline/crud.tsx
This commit is contained in:
xiaojunnuo
2025-06-29 00:26:34 +08:00
156 changed files with 10679 additions and 8692 deletions
@@ -1,120 +1,132 @@
<template>
<div class="cron-editor">
<div class="flex-o">
<cron-light :disabled="disabled" :readonly="readonly" :period="period" class="flex-o cron-ant" locale="zh-CN" format="quartz" :model-value="modelValue" @update:model-value="onUpdate" @error="onError" />
</div>
<div class="mt-5 flex">
<a-input :disabled="true" :readonly="readonly" :value="modelValue" @change="onChange"></a-input>
<fs-icon icon="ion:close-circle" class="pointer fs-16 ml-5 color-gray" title="清除选择" @click="onClear"></fs-icon>
</div>
<div class="helper">下次触发时间{{ nextTime }}</div>
<div class="fs-helper">{{ errorMessage }}</div>
</div>
<div class="cron-editor">
<div class="flex-o">
<cron-light :disabled="disabled" :readonly="readonly" :period="period" class="flex-o cron-ant"
locale="zh-CN" format="quartz" :model-value="modelValue" @update:model-value="onUpdate"
@error="onError" />
</div>
<div class="mt-5 flex">
<a-input :disabled="true" :readonly="readonly" :value="modelValue" @change="onChange"></a-input>
<fs-icon icon="ion:close-circle" class="pointer fs-16 ml-5 color-gray" :title="t('certd.cron.clearTip')"
@click="onClear"></fs-icon>
</div>
<div class="helper">{{ t('certd.cron.nextTrigger') }}{{ nextTime }}</div>
<div class="fs-helper">{{ errorMessage }}</div>
</div>
</template>
<script lang="ts" setup>
import parser from "cron-parser";
import { computed, ref } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
import { getCronNextTimes } from "/@/components/cron-editor/utils";
defineOptions({
name: "CronEditor",
name: "CronEditor",
});
const props = defineProps<{
modelValue?: string;
disabled?: boolean;
readonly?: boolean;
allowEveryMin?: boolean;
modelValue?: string;
disabled?: boolean;
readonly?: boolean;
allowEveryMin?: boolean;
}>();
const period = ref<string>("");
if (props.modelValue == null || props.modelValue.endsWith("* * *")) {
period.value = "day";
period.value = "day";
} else if (props.modelValue.endsWith("* *")) {
period.value = "month";
period.value = "month";
} else if (props.modelValue.endsWith("*")) {
period.value = "year";
period.value = "year";
}
const emit = defineEmits<{
"update:modelValue": any;
change: any;
"update:modelValue": any;
change: any;
}>();
const errorMessage = ref<string | null>(null);
const onUpdate = (value: string) => {
if (value === props.modelValue) {
return;
}
const arr: string[] = value.split(" ");
if (arr[0] === "*") {
arr[0] = "0";
}
if (!props.allowEveryMin) {
if (arr[1] === "*") {
arr[1] = "0";
}
}
if (value === props.modelValue) {
return;
}
const arr: string[] = value.split(" ");
if (arr[0] === "*") {
arr[0] = "0";
}
if (!props.allowEveryMin) {
if (arr[1] === "*") {
arr[1] = "0";
}
}
value = arr.join(" ");
value = arr.join(" ");
emit("update:modelValue", value);
errorMessage.value = undefined;
emit("update:modelValue", value);
errorMessage.value = undefined;
};
const onPeriod = (value: string) => {
period.value = value;
period.value = value;
};
const onChange = (e: any) => {
const value = e.target.value;
onUpdate(value);
const value = e.target.value;
onUpdate(value);
};
const onError = (error: any) => {
errorMessage.value = error;
errorMessage.value = error;
};
const onClear = () => {
if (props.disabled) {
return;
}
onUpdate("");
if (props.disabled) {
return;
}
onUpdate("");
};
const nextTime = computed(() => {
if (props.modelValue == null) {
return "请先设置正确的cron表达式";
}
if (props.modelValue == null) {
return t("certd.cron.tip");
}
try {
const nextTimes = getCronNextTimes(props.modelValue, 2);
return nextTimes.join("");
} catch (e) {
console.log(e);
return "请先设置正确的cron表达式";
}
try {
const nextTimes = getCronNextTimes(props.modelValue, 2);
return nextTimes.join("");
} catch (e) {
console.log(e);
return t("certd.cron.tip");
}
});
</script>
<style lang="less">
.cron-editor {
.cron-ant {
flex-wrap: wrap;
&* > {
margin-bottom: 2px;
display: flex;
align-items: center;
}
.vcron-select-list {
min-width: 56px;
}
.vcron-select-input {
min-height: 22px;
background-color: #fff;
}
.vcron-select-container {
display: flex;
align-items: center;
}
}
.cron-ant {
flex-wrap: wrap;
&*> {
margin-bottom: 2px;
display: flex;
align-items: center;
}
.vcron-select-list {
min-width: 56px;
}
.vcron-select-input {
min-height: 22px;
background-color: #fff;
}
.vcron-select-container {
display: flex;
align-items: center;
}
}
}
</style>
@@ -195,6 +195,7 @@ watch(
const input = (pluginType === "plugin" ? form?.input : form) || {};
const watches = {};
for (const key of props.watches) {
//@ts-ignore
watches[key] = input[key];
}
return {
@@ -16,18 +16,23 @@ const slots = defineSlots();
<template>
<div class="tutorial-button pointer" @click="open">
<template v-if="!slots.default">
<fs-icon v-if="showIcon === false" icon="ant-design:question-circle-outlined" class="mr-0.5"></fs-icon>
<div class="hidden md:block">使用教程</div>
<fs-icon
v-if="showIcon === false"
icon="ant-design:question-circle-outlined"
class="mr-0.5"
></fs-icon>
<div class="hidden md:block">{{$t('tutorial.title')}}</div>
</template>
<slot></slot>
<a-modal v-model:open="openedRef" class="tutorial-modal" width="90%">
<template #title> 使用教程 </template>
<template #title>{{$t('tutorial.title')}}</template>
<tutorial-steps v-if="openedRef" />
<template #footer></template>
</a-modal>
</div>
</template>
<style lang="less">
.tutorial-modal {
top: 50px;
@@ -4,6 +4,9 @@
<script lang="ts" setup>
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
type Step = {
title: string;
@@ -13,15 +16,9 @@ type Step = {
import { ref } from "vue";
const steps = ref<Step[]>([
{
title: "创建证书流水线"
},
{
title: "添加部署任务"
},
{
title: "定时运行"
}
{ title: t('certd.steps.createPipeline') },
{ title: t('certd.steps.addTask') },
{ title: t('certd.steps.scheduledRun') }
]);
const router = useRouter();
@@ -20,14 +20,18 @@
</div>
<div class="flex-center actions">
<fs-button class="m-10" icon="ion:arrow-back-outline" @click="prev()">上一步</fs-button>
<fs-button class="m-10" type="primary" icon-right="ion:arrow-forward-outline" @click="next()">下一步</fs-button>
<fs-button class="m-10" icon="ion:arrow-back-outline" @click="prev()">{{ t('guide.buttons.prev') }}</fs-button>
<fs-button class="m-10" type="primary" icon-right="ion:arrow-forward-outline" @click="next()">{{ t('guide.buttons.next') }}</fs-button>
</div>
</div>
</template>
<script setup lang="tsx">
import { FsRender } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
import SimpleSteps from "./simple-steps.vue";
type Step = {
title: string;
@@ -45,130 +49,151 @@ type StepItems = {
import { computed, nextTick, ref } from "vue";
const steps = ref<Step[]>([
{
title: "创建证书申请流水线",
description: "演示证书申请任务如何配置",
{
title: t("guide.createCertPipeline.title"),
description: t("guide.createCertPipeline.description"),
items: [
{
title: "教程演示内容",
descriptions: ["本教程演示如何自动申请证书并部署到Nginx上", "仅需3步,全自动申请部署证书"],
title: t("guide.createCertPipeline.items.tutorialTitle"),
descriptions: [
t("guide.createCertPipeline.items.tutorialDesc1"),
t("guide.createCertPipeline.items.tutorialDesc2"),
],
body: () => {
return <SimpleSteps></SimpleSteps>;
},
},
{
image: "/static/doc/images/1-add.png",
title: "创建证书流水线",
descriptions: ["点击添加证书流水线,填写证书申请信息"],
title: t("guide.createCertPipeline.items.createTitle"),
descriptions: [t("guide.createCertPipeline.items.createDesc")],
},
{
image: "/static/doc/images/3-add-success.png",
title: "流水线创建成功",
descriptions: ["点击手动触发即可申请证书"],
title: t("guide.createCertPipeline.items.successTitle"),
descriptions: [t("guide.createCertPipeline.items.successDesc")],
},
{
title: "接下来演示如何自动部署证书",
descriptions: ["如果您只需要申请证书,那么到这一步就可以了"],
title: t("guide.createCertPipeline.items.nextTitle"),
descriptions: [t("guide.createCertPipeline.items.nextDesc")],
},
],
},
{
title: "添加部署证书任务",
description: "这里演示部署证书到Nginx",
title: t("guide.addDeployTask.title"),
description: t("guide.addDeployTask.description"),
items: [
{
image: "/static/doc/images/5-1-add-host.png",
title: "添加证书部署任务",
descriptions: ["这里演示自动部署证书到nginx", "本系统提供海量部署插件,满足您的各种部署需求"],
title: t("guide.addDeployTask.items.addTaskTitle"),
descriptions: [
t("guide.addDeployTask.items.addTaskDesc1"),
t("guide.addDeployTask.items.addTaskDesc2"),
],
},
{
image: "/static/doc/images/5-2-add-host.png",
title: "填写任务参数",
descriptions: ["填写主机上证书文件的路径", "选择主机ssh登录授权"],
title: t("guide.addDeployTask.items.fillParamsTitle"),
descriptions: [
t("guide.addDeployTask.items.fillParamsDesc1"),
t("guide.addDeployTask.items.fillParamsDesc2"),
],
},
{
image: "/static/doc/images/5-3-add-host.png",
title: "让新证书生效",
descriptions: ["执行重启脚本", "让证书生效"],
title: t("guide.addDeployTask.items.activateCertTitle"),
descriptions: [
t("guide.addDeployTask.items.activateCertDesc1"),
t("guide.addDeployTask.items.activateCertDesc2"),
],
},
{
image: "/static/doc/images/5-4-add-host.png",
title: "部署任务添加成功",
descriptions: ["现在可以运行"],
title: t("guide.addDeployTask.items.taskSuccessTitle"),
descriptions: [t("guide.addDeployTask.items.taskSuccessDesc")],
},
{
image: "/static/doc/images/5-5-plugin-list.png",
title: "本系统提供茫茫多的部署插件",
descriptions: ["您可以根据自身需求将证书部署到各种应用和平台"],
},
],
},
{
title: "运行与测试",
description: "演示流水线运行,查看日志,成功后跳过等",
items: [
{
image: "/static/doc/images/9-start.png",
title: "运行测试一下",
descriptions: ["点击手动触发按钮,即可测试运行"],
},
{
image: "/static/doc/images/10-1-log.png",
title: "查看日志",
descriptions: ["点击任务可以查看状态和日志"],
},
{
image: "/static/doc/images/11-1-error.png",
title: "执行失败如何排查",
descriptions: ["查看错误日志"],
},
{
image: "/static/doc/images/11-2-error.png",
title: "执行失败如何排查",
descriptions: ["查看错误日志", "这里报的是nginx容器不存在,修改命令,改成正确的nginx容器名称即可"],
},
{
image: "/static/doc/images/12-1-log-success.png",
title: "执行成功",
descriptions: ["修改正确后,重新点击手动触发,重新运行一次,执行成功"],
},
{
image: "/static/doc/images/12-2-skip-log.png",
title: "成功后自动跳过",
descriptions: ["可以看到成功过的将会自动跳过,不会重复执行,只有当参数变更或者证书更新了,才会重新运行"],
},
{
image: "/static/doc/images/13-1-result.png",
title: "查看证书部署成功",
descriptions: ["访问nginx上的网站,可以看到证书已经部署成功"],
},
{
image: "/static/doc/images/13-3-download.png",
title: "还可以下载证书,手动部署",
descriptions: ["如果还没有好用的部署插件,没办法自动部署,你还可以下载证书,手动部署"],
},
],
},
{
title: "设置定时执行和邮件通知",
description: "自动运行",
items: [
{
image: "/static/doc/images/14-timer.png",
title: "设置定时执行",
descriptions: ["流水线测试成功,接下来配置定时触发,以后每天定时执行就不用管了", "推荐配置每天运行一次,在到期前35天才会重新申请新证书并部署,没到期前会自动跳过,不会重复申请。"],
},
{
image: "/static/doc/images/15-1-email.png",
title: "设置邮件通知",
descriptions: ["建议选择监听'错误时'和'错误转成功'两种即可,在意外失败时可以尽快去排查问题,(基础版需要配置邮件服务器)"],
},
{
title: "教程结束",
descriptions: ["感谢观看,希望对你有所帮助"],
title: t("guide.addDeployTask.items.pluginsTitle"),
descriptions: [t("guide.addDeployTask.items.pluginsDesc")],
},
],
},
{
title: t('guide.runAndTestTask.runAndTestTitle'),
description: t('guide.runAndTestTask.runAndTestDescription'),
items: [
{
image: "/static/doc/images/9-start.png",
title: t('guide.runAndTestTask.runTestOnce'),
descriptions: [t('guide.runAndTestTask.clickManualTriggerToTest')],
},
{
image: "/static/doc/images/10-1-log.png",
title: t('guide.runAndTestTask.viewLogs'),
descriptions: [t('guide.runAndTestTask.clickTaskToViewStatusAndLogs')],
},
{
image: "/static/doc/images/11-1-error.png",
title: t('guide.runAndTestTask.howToTroubleshootFailure'),
descriptions: [t('guide.runAndTestTask.viewErrorLogs')],
},
{
image: "/static/doc/images/11-2-error.png",
title: t('guide.runAndTestTask.howToTroubleshootFailure'),
descriptions: [
t('guide.runAndTestTask.viewErrorLogs'),
t('guide.runAndTestTask.nginxContainerNotExistFix'),
],
},
{
image: "/static/doc/images/12-1-log-success.png",
title: t('guide.runAndTestTask.executionSuccess'),
descriptions: [t('guide.runAndTestTask.retryAfterFix')],
},
{
image: "/static/doc/images/12-2-skip-log.png",
title: t('guide.runAndTestTask.autoSkipAfterSuccess'),
descriptions: [t('guide.runAndTestTask.successSkipExplanation')],
},
{
image: "/static/doc/images/13-1-result.png",
title: t('guide.runAndTestTask.viewCertDeploymentSuccess'),
descriptions: [t('guide.runAndTestTask.visitNginxToSeeCert')],
},
{
image: "/static/doc/images/13-3-download.png",
title: t('guide.runAndTestTask.downloadCertManualDeploy'),
descriptions: [t('guide.runAndTestTask.downloadIfNoAutoDeployPlugin')],
},
],
},
{
title: t('guide.scheduleAndEmailTask.title'),
description: t('guide.scheduleAndEmailTask.description'),
items: [
{
image: "/static/doc/images/14-timer.png",
title: t('guide.scheduleAndEmailTask.setSchedule'),
descriptions: [
t('guide.scheduleAndEmailTask.pipelineSuccessThenSchedule'),
t('guide.scheduleAndEmailTask.recommendDailyRun'),
],
},
{
image: "/static/doc/images/15-1-email.png",
title: t('guide.scheduleAndEmailTask.setEmailNotification'),
descriptions: [
t('guide.scheduleAndEmailTask.suggestErrorAndRecoveryEmails'),
t('guide.scheduleAndEmailTask.basicVersionNeedsMailServer'),
],
},
{
title: t('guide.scheduleAndEmailTask.tutorialEndTitle'),
descriptions: [t('guide.scheduleAndEmailTask.thanksForWatching')],
},
],
}
]);
const current = ref(0);
@@ -1,15 +1,15 @@
<template>
<div v-if="!settingStore.isComm || userStore.isAdmin" class="layout-vip isPlus" @click="openUpgrade">
<contextHolder />
<fs-icon icon="mingcute:vip-1-line" :title="text.title" />
<div v-if="!settingStore.isComm || userStore.isAdmin" class="layout-vip isPlus" @click="openUpgrade">
<contextHolder />
<fs-icon icon="mingcute:vip-1-line" :title="text.title" />
<div v-if="mode !== 'icon'" class="text hidden md:block ml-0.5">
<a-tooltip>
<template #title> {{ text.title }}</template>
<span class="">{{ text.name }}</span>
</a-tooltip>
</div>
</div>
<div v-if="mode !== 'icon'" class="text hidden md:block ml-0.5">
<a-tooltip>
<template #title> {{ text.title }}</template>
<span class="">{{ text.name }}</span>
</a-tooltip>
</div>
</div>
</template>
<script lang="tsx" setup>
import { computed, onMounted, reactive } from "vue";
@@ -20,6 +20,9 @@ import { useSettingStore } from "/@/store/settings";
import { useRouter } from "vue-router";
import { useUserStore } from "/@/store/user";
import { mitter } from "/@/utils/util.mitt";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const settingStore = useSettingStore();
const props = withDefaults(
@@ -39,56 +42,56 @@ const text = computed<Text>(() => {
const map = {
isComm: {
comm: {
name: `${vipLabel}已开通`,
title: "到期时间:" + expireTime.value,
name: t("vip.comm.name", { vipLabel }),
title: t("vip.comm.title", { expire: expireTime.value }),
},
button: {
name: `${vipLabel}已开通`,
title: "到期时间:" + expireTime.value,
name: t("vip.comm.name", { vipLabel }),
title: t("vip.comm.title", { expire: expireTime.value }),
},
icon: {
name: "",
title: `${vipLabel}已开通`,
title: t("vip.comm.name", { vipLabel }),
},
nav: {
name: `${vipLabel}`,
title: "到期时间:" + expireTime.value,
name: t("vip.comm.nav", { vipLabel }),
title: t("vip.comm.title", { expire: expireTime.value }),
},
},
isPlus: {
comm: {
name: "商业版功能",
title: "升级商业版,获取商业授权",
name: t("vip.plus.name"),
title: t("vip.plus.title"),
},
button: {
name: `${vipLabel}已开通`,
title: "到期时间:" + expireTime.value,
name: t("vip.comm.name", { vipLabel }),
title: t("vip.comm.title", { expire: expireTime.value }),
},
icon: {
name: "",
title: `${vipLabel}已开通`,
title: t("vip.comm.name", { vipLabel }),
},
nav: {
name: `${vipLabel}`,
title: "到期时间:" + expireTime.value,
name: t("vip.comm.nav", { vipLabel }),
title: t("vip.comm.title", { expire: expireTime.value }),
},
},
free: {
comm: {
name: "商业版功能",
title: "升级商业版,获取商业授权",
name: t("vip.free.comm.name"),
title: t("vip.free.comm.title"),
},
button: {
name: "专业版功能",
title: "升级专业版,享受更多VIP特权",
name: t("vip.free.button.name"),
title: t("vip.free.button.title"),
},
icon: {
name: "",
title: "专业版功能",
title: t("vip.free.button.name"),
},
nav: {
name: "基础版",
title: "升级专业版,享受更多VIP特权",
name: t("vip.free.nav.name"),
title: t("vip.free.nav.title"),
},
},
};
@@ -101,6 +104,7 @@ const text = computed<Text>(() => {
}
});
const expireTime = computed(() => {
if (settingStore.isPlus) {
return dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD");
@@ -125,22 +129,24 @@ const formState = reactive({
const router = useRouter();
async function doActive() {
if (!formState.code) {
message.error("请输入激活码");
throw new Error("请输入激活码");
message.error(t("vip.enterCode"));
throw new Error(t("vip.enterCode"));
}
const res = await api.doActive(formState);
if (res) {
await settingStore.init();
const vipLabel = settingStore.vipLabel;
Modal.success({
title: "激活成功",
content: `您已成功激活${vipLabel},有效期至:${dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD")}`,
title: t("vip.successTitle"),
content: t("vip.successContent", {
vipLabel,
expireDate: dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD"),
}),
onOk() {
if (!(settingStore.installInfo.bindUserId > 0)) {
//未绑定账号
Modal.confirm({
title: "是否绑定袖手账号",
content: "绑定账号后,可以避免License丢失,强烈建议绑定",
title: t("vip.bindAccountTitle"),
content: t("vip.bindAccountContent"),
onOk() {
router.push("/sys/account");
},
@@ -151,6 +157,7 @@ async function doActive() {
}
}
const computedSiteId = computed(() => settingStore.installInfo?.siteId);
const [modal, contextHolder] = Modal.useModal();
const userStore = useUserStore();
@@ -162,16 +169,17 @@ function goAccount() {
async function getVipTrial() {
const res = await api.getVipTrial();
message.success(`恭喜,您已获得专业版${res.duration}天试用`);
message.success(t('vip.congratulations_vip_trial', { duration: res.duration }));
await settingStore.init();
}
function openTrialModal() {
Modal.destroyAll();
modal.confirm({
title: "7天专业版试用获取",
okText: "立即获取",
title: t('vip.trial_modal_title'),
okText: t('vip.trial_modal_ok_text'),
onOk() {
getVipTrial();
},
@@ -179,14 +187,15 @@ function openTrialModal() {
content: () => {
return (
<div class="flex-col mt-10 mb-10">
<div>感谢您对开源项目的支持</div>
<div>点击确认即可获取7天专业版试用</div>
<div>{t('vip.trial_modal_thanks')}</div>
<div>{t('vip.trial_modal_click_confirm')}</div>
</div>
);
},
});
}
function openStarModal() {
Modal.destroyAll();
const goGithub = () => {
@@ -194,8 +203,8 @@ function openStarModal() {
};
modal.confirm({
title: "7天专业版试用获取",
okText: "立即去Star",
title: t("vip.get_7_day_pro_trial"),
okText: t("vip.star_now"),
onOk() {
goGithub();
openTrialModal();
@@ -204,7 +213,7 @@ function openStarModal() {
content: () => {
return (
<div class="flex mt-10 mb-10">
<div>可以先请您帮忙点个star吗感谢感谢</div>
<div>{t("vip.please_help_star")}</div>
<img class="ml-5" src="https://img.shields.io/github/stars/certd/certd?logo=github" />
</div>
);
@@ -212,174 +221,207 @@ function openStarModal() {
});
}
function openUpgrade() {
if (!userStore.isAdmin) {
message.info("仅限管理员操作");
return;
}
const placeholder = "请输入激活码";
const isPlus = settingStore.isPlus;
let title = "激活专业版/商业版";
if (settingStore.isComm) {
title = "续期商业版";
} else if (settingStore.isPlus) {
title = "续期专业版/升级商业版";
}
if (!userStore.isAdmin) {
message.info(t("vip.admin_only_operation"));
return;
}
const placeholder = t("vip.enter_activation_code");
const isPlus = settingStore.isPlus;
let title = t("vip.activate_pro_business");
if (settingStore.isComm) {
title = t("vip.renew_business");
} else if (settingStore.isPlus) {
title = t("vip.renew_pro_upgrade_business");
}
const productInfo = settingStore.productInfo;
const vipTypeDefine = {
free: {
title: "基础版",
desc: "社区免费版",
type: "free",
icon: "lucide:package-open",
privilege: ["证书申请无限制", "域名数量无限制", "证书流水线数量无限制", "常用的主机、云平台、cdn、宝塔、1Panel等部署插件", "邮件、webhook通知方式"],
},
plus: {
title: "专业版",
desc: "开源需要您的赞助支持",
type: "plus",
privilege: ["可加VIP群,您的需求将优先实现", "站点证书监控无限制", "更多通知方式", "插件全开放,群辉等更多插件"],
trial: {
title: "点击获取7天试用",
click: () => {
openStarModal();
},
},
icon: "stash:thumb-up",
price: productInfo.plus.price,
price3: `¥${productInfo.plus.price3}/3年`,
tooltip: productInfo.plus.tooltip,
get() {
return (
<a-tooltip title="爱发电赞助“VIP会员”后获取一年期专业版激活码,开源需要您的支持">
<a-button size="small" type="primary" href="https://afdian.com/a/greper" target="_blank">
爱发电赞助后获取
</a-button>
</a-tooltip>
);
},
},
comm: {
title: "商业版",
desc: "商业授权,可对外运营",
type: "comm",
icon: "vaadin:handshake",
privilege: ["拥有专业版所有特权", "允许商用,可修改logo、标题", "数据统计", "插件管理", "多用户无限制", "支持用户支付"],
price: productInfo.comm.price,
price3: `¥${productInfo.comm.price3}/3年`,
tooltip: productInfo.comm.tooltip,
get() {
return <a-button size="small">请联系作者获取试用</a-button>;
},
},
};
const modalRef = modal.confirm({
title,
async onOk() {
return await doActive();
const productInfo = settingStore.productInfo;
const vipTypeDefine = {
free: {
title: t("vip.basic_edition"),
desc: t("vip.community_free_version"),
type: "free",
icon: "lucide:package-open",
privilege: [
t("vip.unlimited_certificate_application"),
t("vip.unlimited_domain_count"),
t("vip.unlimited_certificate_pipelines"),
t("vip.common_deployment_plugins"),
t("vip.email_webhook_notifications"),
],
},
plus: {
title: t("vip.professional_edition"),
desc: t("vip.open_source_support"),
type: "plus",
privilege: [
t("vip.vip_group_priority"),
t("vip.unlimited_site_certificate_monitoring"),
t("vip.more_notification_methods"),
t("vip.plugins_fully_open"),
],
trial: {
title: t("vip.click_to_get_7_day_trial"),
click: () => {
openStarModal();
},
},
maskClosable: true,
okText: "激活",
width: 1000,
content: () => {
let activationCodeGetWay = (
<span>
<a href="https://afdian.com/a/greper" target="_blank">
爱发电赞助VIP会员后获取一年期专业版激活码
</a>
<span> 商业版请直接联系作者</span>
</span>
icon: "stash:thumb-up",
price: productInfo.plus.price,
price3: `¥${productInfo.plus.price3}/3${t("vip.years")}`,
tooltip: productInfo.plus.tooltip,
get() {
return (
<a-tooltip title={t("vip.afdian_support_vip")}>
<a-button size="small" type="primary" href="https://afdian.com/a/greper" target="_blank">
{t("vip.get_after_support")}
</a-button>
</a-tooltip>
);
const vipLabel = settingStore.vipLabel;
const slots = [];
for (const key in vipTypeDefine) {
// @ts-ignore
const item = vipTypeDefine[key];
const vipBlockClass = `vip-block ${key === settingStore.plusInfo.vipType ? "current" : ""}`;
slots.push(
<a-col span={8}>
<div class={vipBlockClass}>
<h3 class="block-header ">
<span class="flex-o">{item.title}</span>
{item.trial && (
<span class="trial">
<a-tooltip title={item.trial.message}>
<a onClick={item.trial.click}>{item.trial.title}</a>
},
},
comm: {
title: t("vip.business_edition"),
desc: t("vip.commercial_license"),
type: "comm",
icon: "vaadin:handshake",
privilege: [
t("vip.all_pro_privileges"),
t("vip.allow_commercial_use_modify_logo_title"),
t("vip.data_statistics"),
t("vip.plugin_management"),
t("vip.unlimited_multi_users"),
t("vip.support_user_payment"),
],
price: productInfo.comm.price,
price3: `¥${productInfo.comm.price3}/3${t("vip.years")}`,
tooltip: productInfo.comm.tooltip,
get() {
return <a-button size="small">{t("vip.contact_author_for_trial")}</a-button>;
},
},
};
const modalRef = modal.confirm({
title,
async onOk() {
return await doActive();
},
maskClosable: true,
okText: t("vip.activate"),
width: 1000,
content: () => {
let activationCodeGetWay = (
<span>
<a href="https://afdian.com/a/greper" target="_blank">
{t("vip.get_pro_code_after_support")}
</a>
<span> {t("vip.business_contact_author")}</span>
</span>
);
const vipLabel = settingStore.vipLabel;
const slots = [];
for (const key in vipTypeDefine) {
// @ts-ignore
const item = vipTypeDefine[key];
const vipBlockClass = `vip-block ${key === settingStore.plusInfo.vipType ? "current" : ""}`;
slots.push(
<a-col span={8}>
<div class={vipBlockClass}>
<h3 class="block-header ">
<span class="flex-o">{item.title}</span>
{item.trial && (
<span class="trial">
<a-tooltip title={item.trial.message}>
<a onClick={item.trial.click}>{item.trial.title}</a>
</a-tooltip>
</span>
)}
</h3>
<div style="color:green" class="flex-o">
<fs-icon icon={item.icon} class="fs-16 flex-o" />
{item.desc}
</div>
<ul class="flex-1 privilege">
{item.privilege.map((p: string) => (
<li class="flex-baseline">
<fs-icon class="color-green" icon="ion:checkmark-sharp" />
{p}
</li>
))}
</ul>
<div class="footer flex-between flex-vc">
<div class="price-show">
{item.price && (
<span class="flex">
<span class="-text">¥{item.price}</span>
<span>/</span>
{t("vip.year")}
<a-tooltip class="ml-5" title={item.price3}>
<fs-icon class="pointer color-red" icon="ic:outline-discount"></fs-icon>
</a-tooltip>
</span>
)}
</h3>
<div style="color:green" class="flex-o">
<fs-icon icon={item.icon} class="fs-16 flex-o" />
{item.desc}
</div>
<ul class="flex-1 privilege">
{item.privilege.map((p: string) => (
<li class="flex-baseline">
<fs-icon class="color-green" icon="ion:checkmark-sharp" />
{p}
</li>
))}
</ul>
<div class="footer flex-between flex-vc">
<div class="price-show">
{item.price && (
<span class="flex">
<span class="-text">¥{item.price}</span>
<span>/</span>
<a-tooltip class="ml-5" title={item.price3}>
<fs-icon class="pointer color-red" icon="ic:outline-discount"></fs-icon>
</a-tooltip>
</span>
)}
{!item.price && (
<span>
<span class="price-text">免费</span>
</span>
)}
</div>
<div class="get-show">{item.get && <div>{item.get()}</div>}</div>
{!item.price && (
<span>
<span class="price-text">{t("vip.freee")}</span>
</span>
)}
</div>
<div class="get-show">{item.get && <div>{item.get()}</div>}</div>
</div>
</a-col>
);
}
return (
<div class="mt-10 mb-10 vip-active-modal">
{productInfo.notice && (
<div class="mb-10">
<a-alert type="error" message={productInfo.notice}></a-alert>
</div>
)}
<div class="vip-type-vs">
<a-row gutter={20}>{slots}</a-row>
</div>
</a-col>
);
}
return (
<div class="mt-10 mb-10 vip-active-modal">
{productInfo.notice && (
<div class="mb-10">
<a-alert type="error" message={productInfo.notice}></a-alert>
</div>
)}
<div class="vip-type-vs">
<a-row gutter={20}>{slots}</a-row>
</div>
<div class="mt-10">
<h3 class="block-header">{isPlus ? t("vip.renew") : t("vip.activate_immediately")}</h3>
<div>
{isPlus
? `${t("vip.current")} ${vipLabel} ${t("vip.activated_expire_time")}` +
dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD")
: ""}
</div>
<div class="mt-10">
<h3 class="block-header">{isPlus ? "续期" : "立刻激活"}</h3>
<div>{isPlus ? `当前${vipLabel}已激活,到期时间` + dayjs(settingStore.plusInfo.expireTime).format("YYYY-MM-DD") : ""}</div>
<div class="mt-10">
<div class="flex-o w-100">
<span>站点ID</span>
<fs-copyable class="flex-1" v-model={computedSiteId.value}></fs-copyable>
</div>
<a-input class="mt-10" v-model:value={formState.code} placeholder={placeholder} />
<a-input class="mt-10" v-model:value={formState.inviteCode} placeholder={"邀请码【选填】,可额外获得专业版30天/商业版15天时长"} />
<div class="flex-o w-100">
<span>{t("vip.site_id")}</span>
<fs-copyable class="flex-1" v-model={computedSiteId.value}></fs-copyable>
</div>
<a-input class="mt-10" v-model:value={formState.code} placeholder={placeholder} />
<a-input
class="mt-10"
v-model:value={formState.inviteCode}
placeholder={t("vip.invite_code_optional")}
/>
</div>
<div class="mt-10">
没有激活码
{activationCodeGetWay}
</div>
<div class="mt-10">
激活码使用过一次之后不可再次使用如果要更换站点<a onClick={goAccount}>绑定账号</a>然后"转移VIP"即可
</div>
<div class="mt-10">
{t("vip.no_activation_code")}
{activationCodeGetWay}
</div>
<div class="mt-10">
{t("vip.activation_code_one_use")}<a onClick={goAccount}>{t("vip.bind_account")}</a>
{t("vip.transfer_vip")}
</div>
</div>
);
},
});
</div>
);
},
});
}
onMounted(() => {
mitter.on("openVipModal", () => {
@@ -392,69 +434,75 @@ onMounted(() => {
<style lang="less">
.layout-vip {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.isPlus {
color: #c5913f;
}
&.isPlus {
color: #c5913f;
}
.text {
}
.text {}
}
.vip-active-modal {
.vip-block {
display: flex;
flex-direction: column;
padding: 10px;
border: 1px solid #eee;
border-radius: 5px;
height: 250px;
//background-color: rgba(250, 237, 167, 0.79);
&.current {
border-color: green;
}
.block-header {
padding: 0px;
display: flex;
justify-content: space-between;
.trial {
font-size: 12px;
font-wight: 400;
}
}
.vip-block {
display: flex;
flex-direction: column;
padding: 10px;
border: 1px solid #eee;
border-radius: 5px;
height: 250px;
.footer {
padding-top: 5px;
margin-top: 0px;
border-top: 1px solid #eee;
.price-text {
font-size: 18px;
color: red;
}
}
}
//background-color: rgba(250, 237, 167, 0.79);
&.current {
border-color: green;
}
ul {
list-style-type: unset;
margin-left: 0px;
padding: 0;
}
.color-green {
color: green;
}
.vip-type-vs {
.privilege {
.fs-icon {
color: green;
}
}
.fs-icon {
margin-right: 5px;
}
}
.block-header {
padding: 0px;
display: flex;
justify-content: space-between;
.trial {
font-size: 12px;
font-wight: 400;
}
}
.footer {
padding-top: 5px;
margin-top: 0px;
border-top: 1px solid #eee;
.price-text {
font-size: 18px;
color: red;
}
}
}
ul {
list-style-type: unset;
margin-left: 0px;
padding: 0;
}
.color-green {
color: green;
}
.vip-type-vs {
.privilege {
.fs-icon {
color: green;
}
}
.fs-icon {
margin-right: 5px;
}
}
}
</style>