mirror of
https://github.com/certd/certd.git
synced 2026-04-24 12:27:25 +08:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user