增加自定义绑定域名

This commit is contained in:
2026-09-02 15:11:32 +08:00
parent db519d9af2
commit f644c278c9
23 changed files with 2966 additions and 212 deletions
+26
View File
@@ -83,6 +83,32 @@ export function applyTenantDomain(data) {
})
}
// 绑定自有域名(提交后需按提示配置解析,再调 checkCustomDns)
export function bindCustomDomain(data) {
return request({
url: '/platform/domain/tenant/bindCustom',
method: 'post',
data
})
}
// 检测自有域名解析,通过后后端自动申请 HTTPS 证书
export function checkCustomDomainDns(id) {
return request({
url: '/platform/domain/tenant/checkCustomDns',
method: 'post',
data: { id }
})
}
// 获取自有域名解析配置指引(CNAME 目标 / A 记录 IP)
export function getCustomDomainGuide() {
return request({
url: '/platform/domain/tenant/customDomainGuide',
method: 'get'
})
}
// 审核租户域名
export function auditTenantDomain(data) {
return request({
+126 -18
View File
@@ -14,13 +14,17 @@
<div class="search-box">
<el-input
v-model="searchForm.sub_domain"
placeholder="搜索二级域名"
placeholder="搜索二级前缀或完整域名"
clearable
style="width: 200px; margin-right: 10px;"
style="width: 220px; margin-right: 10px;"
@keyup.enter="handleSearch"
/>
<el-select v-model="searchForm.domain_type" placeholder="类型" clearable style="width: 150px; margin-right: 10px;">
<el-option label="平台二级域名" :value="1" />
<el-option label="自有域名" :value="2" />
</el-select>
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width: 120px; margin-right: 10px;">
<el-option label="审核中" :value="0" />
<el-option label="待生效/审核中" :value="0" />
<el-option label="已生效" :value="1" />
<el-option label="已禁用" :value="2" />
</el-select>
@@ -37,20 +41,63 @@
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="tenant_name" label="租户名称" min-width="150" />
<el-table-column prop="sub_domain" label="二级域名前缀" width="150" />
<el-table-column prop="main_domain" label="主域名" min-width="150" />
<el-table-column prop="full_domain" label="完整域名" min-width="200" />
<el-table-column prop="status" label="状态" width="100" align="center">
<el-table-column label="类型" width="130" align="center">
<template #default="scope">
<el-tag v-if="scope.row.status === 0" type="warning">审核中</el-tag>
<el-tag v-else-if="scope.row.status === 1" type="success">已生效</el-tag>
<el-tag v-else type="danger">已禁用</el-tag>
<el-tag v-if="isCustom(scope.row)" type="warning">自有域名</el-tag>
<el-tag v-else type="info">平台二级域名</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="180" />
<el-table-column label="操作" width="200" fixed="right" align="center">
<el-table-column prop="full_domain" label="完整域名" min-width="200" />
<el-table-column prop="status" label="状态" width="120" align="center">
<template #default="scope">
<template v-if="scope.row.status === 0">
<el-tag v-if="scope.row.status === 1" type="success">已生效</el-tag>
<el-tag v-else-if="scope.row.status === 2" type="danger">已禁用</el-tag>
<el-tag v-else type="warning">{{ isCustom(scope.row) ? '待生效' : '审核中' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="解析检测" width="120" align="center">
<template #default="scope">
<span v-if="!isCustom(scope.row)" class="text-muted">—</span>
<el-tooltip v-else-if="scope.row.verify_msg" :content="scope.row.verify_msg" placement="top">
<el-tag :type="verifyTagType(scope.row.verify_status)">{{ verifyText(scope.row.verify_status) }}</el-tag>
</el-tooltip>
<el-tag v-else :type="verifyTagType(scope.row.verify_status)">{{ verifyText(scope.row.verify_status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="HTTPS 证书" width="130" align="center">
<template #default="scope">
<span v-if="!isCustom(scope.row)" class="text-muted">平台证书</span>
<el-tooltip v-else-if="scope.row.ssl_status === 3 && scope.row.ssl_error" :content="scope.row.ssl_error" placement="top">
<el-tag type="danger">签发失败</el-tag>
</el-tooltip>
<el-tooltip v-else-if="scope.row.ssl_status === 2 && scope.row.ssl_expire_at" :content="'到期时间:' + scope.row.ssl_expire_at" placement="top">
<el-tag type="success">已签发</el-tag>
</el-tooltip>
<el-tag v-else :type="sslTagType(scope.row.ssl_status)">{{ sslText(scope.row.ssl_status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="170" />
<el-table-column label="操作" width="230" fixed="right" align="center">
<template #default="scope">
<!-- 自有域名无人工审核:解析与证书就绪后自动生效,这里只提供重新检测与禁用 -->
<template v-if="isCustom(scope.row)">
<el-button
size="small"
text
type="primary"
:loading="checkingId === scope.row.id"
@click="handleCheckDns(scope.row)"
>
检测解析
</el-button>
<el-button v-if="scope.row.status === 1" size="small" text type="danger" @click="handleDisable(scope.row)">
禁用
</el-button>
<el-button v-else-if="scope.row.status === 2" size="small" text type="success" @click="handleEnable(scope.row)">
启用
</el-button>
</template>
<template v-else-if="scope.row.status === 0">
<el-button size="small" type="success" @click="handleAudit(scope.row, 'approve')">
通过
</el-button>
@@ -87,13 +134,23 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { getTenantDomainList, auditTenantDomain, toggleTenantDomainStatus } from '@/api/domain'
import {
getTenantDomainList,
auditTenantDomain,
toggleTenantDomainStatus,
checkCustomDomainDns
} from '@/api/domain'
// 域名类型:与后端 models.DomainTypeCustom 对应
const DOMAIN_TYPE_CUSTOM = 2
const loading = ref(false)
const checkingId = ref<number | null>(null)
const searchForm = reactive({
sub_domain: '',
status: ''
status: '',
domain_type: ''
})
const pagination = reactive({
@@ -104,6 +161,20 @@ const pagination = reactive({
const tableData = ref<any[]>([])
const isCustom = (row: any) => row.domain_type === DOMAIN_TYPE_CUSTOM
const verifyText = (status: number) =>
status === 1 ? '已通过' : status === 2 ? '未通过' : '未检测'
const verifyTagType = (status: number) =>
status === 1 ? 'success' : status === 2 ? 'danger' : 'info'
const sslText = (status: number) =>
status === 2 ? '已签发' : status === 1 ? '签发中' : status === 3 ? '签发失败' : '待处理'
const sslTagType = (status: number) =>
status === 2 ? 'success' : status === 1 ? 'warning' : status === 3 ? 'danger' : 'info'
const fetchData = async () => {
loading.value = true
try {
@@ -129,7 +200,7 @@ const handleSearch = () => {
const handleAudit = async (row: any, action: string) => {
const msg = action === 'approve' ? '确定要通过该域名申请吗?' : '确定要拒绝该域名申请吗?'
await ElMessageBox.confirm(msg, '提示', { type: 'warning' })
const res = await auditTenantDomain({ id: row.id, action })
if (res.code === 200) {
ElMessage.success(res.msg)
@@ -140,8 +211,8 @@ const handleAudit = async (row: any, action: string) => {
}
const handleDisable = async (row: any) => {
await ElMessageBox.confirm('确定要禁用该域名吗?', '提示', { type: 'warning' })
await ElMessageBox.confirm('确定要禁用该域名吗?禁用后该域名将无法访问官网。', '提示', { type: 'warning' })
const res = await toggleTenantDomainStatus(row.id)
if (res.code === 200) {
ElMessage.success(res.msg)
@@ -151,6 +222,39 @@ const handleDisable = async (row: any) => {
}
}
const handleEnable = async (row: any) => {
await ElMessageBox.confirm('确定要重新启用该域名吗?', '提示', { type: 'warning' })
const res = await toggleTenantDomainStatus(row.id)
if (res.code === 200) {
ElMessage.success(res.msg)
fetchData()
} else {
ElMessage.error(res.msg || '操作失败')
}
}
// 自有域名:重新检测解析,通过后后端自动申请证书
const handleCheckDns = async (row: any) => {
checkingId.value = row.id
try {
const res = await checkCustomDomainDns(row.id)
if (res.code === 200) {
const data = res.data || {}
if (data.verify_status === 1) {
ElMessage.success(data.ssl_message || data.verify_msg || '解析检测通过')
} else {
ElMessage.warning(data.verify_msg || '解析尚未生效')
}
fetchData()
} else {
ElMessage.error(res.msg || '检测失败')
}
} finally {
checkingId.value = null
}
}
onMounted(() => {
fetchData()
})
@@ -177,6 +281,10 @@ onMounted(() => {
margin-bottom: 20px;
}
.text-muted {
color: #999;
}
.pagination-wrap {
margin-top: 20px;
display: flex;
@@ -1,17 +1,41 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>我的域名</h2>
<el-button type="primary" @click="dialogVisible = true">
<el-icon><Plus /></el-icon>
申请二级域名
</el-button>
<h2>租户域名</h2>
<div class="header-actions">
<el-select
v-model="currentTid"
placeholder="请选择租户"
filterable
clearable
style="width: 240px"
@change="fetchDomains"
>
<el-option
v-for="item in tenantOptions"
:key="item.id"
:label="item.tenant_name || item.tenant_short_name"
:value="item.id"
/>
</el-select>
<el-button type="primary" :disabled="!currentTid" @click="applyDialogVisible = true">
<el-icon><Plus /></el-icon>
申请二级域名
</el-button>
<el-button type="success" :disabled="!currentTid" @click="openBindDialog">
<el-icon><Link /></el-icon>
绑定自有域名
</el-button>
</div>
</div>
<el-divider></el-divider>
<!-- 我的域名列表 -->
<el-empty v-if="!currentTid" description="请先在上方选择要管理的租户" />
<!-- 该租户的域名列表 -->
<el-table
v-else
:data="tableData"
style="width: 100%"
border
@@ -19,36 +43,85 @@
element-loading-text="正在加载..."
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="sub_domain" label="二级域名前缀" width="150" />
<el-table-column prop="main_domain" label="主域名" min-width="150" />
<el-table-column prop="full_domain" label="完整域名" min-width="200">
<el-table-column label="类型" width="130" align="center">
<template #default="scope">
<el-link
type="primary"
:href="'http://' + scope.row.full_domain"
target="_blank"
>
<el-tag v-if="isCustom(scope.row)" type="warning">自有域名</el-tag>
<el-tag v-else type="info">平台二级域名</el-tag>
</template>
</el-table-column>
<el-table-column prop="full_domain" label="完整域名" min-width="220">
<template #default="scope">
<el-link type="primary" :href="domainUrl(scope.row)" target="_blank">
{{ scope.row.full_domain }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<el-table-column prop="status" label="状态" width="110" align="center">
<template #default="scope">
<el-tag v-if="scope.row.status === 0" type="warning">审核中</el-tag>
<el-tag v-else-if="scope.row.status === 1" type="success"
>已生效</el-tag
>
<el-tag v-else type="danger">已禁用</el-tag>
<el-tag v-if="scope.row.status === 1" type="success">已生效</el-tag>
<el-tag v-else-if="scope.row.status === 2" type="danger">已禁用</el-tag>
<el-tag v-else type="warning">{{
isCustom(scope.row) ? "待生效" : "审核中"
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="180" />
<el-table-column label="操作" width="180" fixed="right" align="center">
<el-table-column label="解析检测" width="120" align="center">
<template #default="scope">
<span v-if="!isCustom(scope.row)" class="text-muted">—</span>
<el-tooltip
v-else-if="scope.row.verify_msg"
:content="scope.row.verify_msg"
placement="top"
>
<el-tag :type="verifyTagType(scope.row.verify_status)">
{{ verifyText(scope.row.verify_status) }}
</el-tag>
</el-tooltip>
<el-tag v-else :type="verifyTagType(scope.row.verify_status)">
{{ verifyText(scope.row.verify_status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="HTTPS 证书" width="130" align="center">
<template #default="scope">
<span v-if="!isCustom(scope.row)" class="text-muted">平台证书</span>
<el-tooltip
v-else-if="scope.row.ssl_status === 3 && scope.row.ssl_error"
:content="scope.row.ssl_error"
placement="top"
>
<el-tag type="danger">签发失败</el-tag>
</el-tooltip>
<el-tooltip
v-else-if="scope.row.ssl_status === 2 && scope.row.ssl_expire_at"
:content="'到期时间:' + scope.row.ssl_expire_at"
placement="top"
>
<el-tag type="success">已签发</el-tag>
</el-tooltip>
<el-tag v-else :type="sslTagType(scope.row.ssl_status)">
{{ sslText(scope.row.ssl_status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="170" />
<el-table-column label="操作" width="230" fixed="right" align="center">
<template #default="scope">
<el-button
v-if="isCustom(scope.row)"
size="small"
text
type="primary"
:loading="checkingId === scope.row.id"
@click="handleCheckDns(scope.row)"
>
检测解析
</el-button>
<el-button
size="small"
text
type="primary"
@click="handleCopy(scope.row.full_domain)"
@click="handleCopy(scope.row)"
>
复制
</el-button>
@@ -63,13 +136,17 @@
</template>
</el-table-column>
</el-table>
<!-- 申请域名弹窗 -->
<el-dialog v-model="dialogVisible" title="申请二级域名" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<!-- 申请二级域名弹窗 -->
<el-dialog v-model="applyDialogVisible" title="申请二级域名" width="500px">
<el-form
:model="applyForm"
:rules="applyRules"
ref="applyFormRef"
label-width="100px"
>
<el-form-item label="选择主域名" prop="main_domain">
<el-select
v-model="form.main_domain"
v-model="applyForm.main_domain"
placeholder="请选择主域名"
style="width: 100%"
>
@@ -82,9 +159,12 @@
</el-select>
</el-form-item>
<el-form-item label="二级前缀" prop="sub_domain">
<el-input v-model="form.sub_domain" placeholder="请输入二级域名前缀">
<el-input
v-model="applyForm.sub_domain"
placeholder="请输入二级域名前缀"
>
<template #append>{{
form.main_domain ? "." + form.main_domain : ""
applyForm.main_domain ? "." + applyForm.main_domain : ""
}}</template>
</el-input>
<div class="form-tip">
@@ -94,50 +174,128 @@
<el-form-item label="预览">
<div class="domain-preview">
{{
form.sub_domain
? form.sub_domain + "." + (form.main_domain || "example.com")
applyForm.sub_domain
? applyForm.sub_domain +
"." +
(applyForm.main_domain || "example.com")
: "请填写上方信息"
}}
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading"
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleApplySubmit"
:loading="applyLoading"
>提交申请</el-button
>
</template>
</el-dialog>
<!-- 绑定自有域名弹窗 -->
<el-dialog v-model="bindDialogVisible" title="绑定自有域名" width="620px">
<el-form
:model="bindForm"
:rules="bindRules"
ref="bindFormRef"
label-width="90px"
>
<el-form-item label="域名" prop="domain">
<el-input
v-model="bindForm.domain"
placeholder="请输入完整域名,如 www.example.com"
/>
<div class="form-tip">
不要带 http:// 与路径。根域名(example.com)与 www
视为两个域名,需分别绑定。
</div>
</el-form-item>
</el-form>
<el-alert type="info" :closable="false" class="guide-box">
<template #title>绑定后请让租户到域名服务商处添加解析</template>
<div v-if="guide.configured" class="guide-body">
<p v-if="guide.cname_target">
<strong>子域名(如 www)</strong>:添加 CNAME 记录,记录值
<code>{{ guide.cname_target }}</code>
</p>
<p v-if="guide.a_ips && guide.a_ips.length">
<strong>根域名(如 example.com)</strong>:添加 A 记录,记录值
<code>{{ guide.a_ips.join(" 或 ") }}</code>
</p>
<p class="guide-note">
解析生效后点击「检测解析」,系统会自动申请 HTTPS
证书,签发成功后域名即生效。
</p>
</div>
<div v-else class="guide-body">
平台尚未配置解析目标,请先在 app.conf 中填写
customdomain_cname_target / customdomain_a_ips。
</div>
</el-alert>
<template #footer>
<el-button @click="bindDialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleBindSubmit"
:loading="bindLoading"
>确认绑定</el-button
>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import { Plus, Link } from "@element-plus/icons-vue";
import {
getMyDomains,
applyTenantDomain,
getEnabledDomains,
deleteTenantDomain,
bindCustomDomain,
checkCustomDomainDns,
getCustomDomainGuide,
} from "@/api/domain";
import { useAuthStore } from "@/stores/auth";
import { getTenantList } from "@/api/tenant";
// 域名类型:与后端 models.DomainTypeCustom 对应
const DOMAIN_TYPE_CUSTOM = 2;
const authStore = useAuthStore();
const loading = ref(false);
const submitLoading = ref(false);
const dialogVisible = ref(false);
const formRef = ref();
const applyLoading = ref(false);
const bindLoading = ref(false);
const checkingId = ref<number | null>(null);
const applyDialogVisible = ref(false);
const bindDialogVisible = ref(false);
const applyFormRef = ref();
const bindFormRef = ref();
// 平台端账号不归属租户,域名操作必须显式指定租户
const currentTid = ref<number | null>(null);
const tenantOptions = ref<any[]>([]);
const tableData = ref<any[]>([]);
const domainList = ref<any[]>([]);
const guide = reactive<{
cname_target: string;
a_ips: string[];
configured: boolean;
}>({ cname_target: "", a_ips: [], configured: false });
const form = reactive({
const applyForm = reactive({
main_domain: "",
sub_domain: "",
});
const rules = {
const bindForm = reactive({
domain: "",
});
const applyRules = {
main_domain: [{ required: true, message: "请选择主域名", trigger: "change" }],
sub_domain: [
{ required: true, message: "请输入二级域名前缀", trigger: "blur" },
@@ -149,16 +307,68 @@ const rules = {
],
};
const bindRules = {
domain: [
{ required: true, message: "请输入要绑定的域名", trigger: "blur" },
{
pattern: /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/,
message: "请输入合法的完整域名,如 www.example.com",
trigger: "blur",
},
],
};
const isCustom = (row: any) => row.domain_type === DOMAIN_TYPE_CUSTOM;
// 已签发证书的自有域名用 https 打开,其余用 http 避免证书告警
const domainUrl = (row: any) => {
const scheme = !isCustom(row) || row.ssl_status === 2 ? "https" : "http";
return `${scheme}://${row.full_domain}`;
};
const verifyText = (status: number) =>
status === 1 ? "已通过" : status === 2 ? "未通过" : "未检测";
const verifyTagType = (status: number) =>
status === 1 ? "success" : status === 2 ? "danger" : "info";
const sslText = (status: number) =>
status === 2
? "已签发"
: status === 1
? "签发中"
: status === 3
? "签发失败"
: "待处理";
const sslTagType = (status: number) =>
status === 2
? "success"
: status === 1
? "warning"
: status === 3
? "danger"
: "info";
const fetchTenants = async () => {
const res = await getTenantList({ page: 1, pageSize: 1000 });
if (res.code === 200) {
tenantOptions.value = res.data?.list || res.data || [];
}
};
const fetchDomains = async () => {
if (!currentTid.value) {
tableData.value = [];
return;
}
loading.value = true;
try {
// 获取我的域名列表
const res = await getMyDomains({ tid: authStore.user.tid });
const res = await getMyDomains({ tid: currentTid.value });
if (res.code === 200) {
tableData.value = res.data || [];
}
// 获取可选主域名
const domainRes = await getEnabledDomains();
if (domainRes.code === 200) {
domainList.value = domainRes.data || [];
@@ -168,37 +378,96 @@ const fetchDomains = async () => {
}
};
const handleSubmit = async () => {
await formRef.value.validate();
submitLoading.value = true;
const fetchGuide = async () => {
const res = await getCustomDomainGuide();
if (res.code === 200 && res.data) {
guide.cname_target = res.data.cname_target || "";
guide.a_ips = res.data.a_ips || [];
guide.configured = !!res.data.configured;
}
};
const openBindDialog = async () => {
bindForm.domain = "";
bindDialogVisible.value = true;
await fetchGuide();
};
const handleApplySubmit = async () => {
await applyFormRef.value.validate();
applyLoading.value = true;
try {
const res = await applyTenantDomain({
tid: authStore.user.tid,
main_domain: form.main_domain,
sub_domain: form.sub_domain,
tid: currentTid.value,
main_domain: applyForm.main_domain,
sub_domain: applyForm.sub_domain,
});
if (res.code === 200) {
ElMessage.success(res.msg);
dialogVisible.value = false;
form.sub_domain = "";
applyDialogVisible.value = false;
applyForm.sub_domain = "";
fetchDomains();
} else {
ElMessage.error(res.msg || "申请失败");
}
} finally {
submitLoading.value = false;
applyLoading.value = false;
}
};
const handleCopy = (domain: string) => {
navigator.clipboard.writeText("http://" + domain);
const handleBindSubmit = async () => {
await bindFormRef.value.validate();
bindLoading.value = true;
try {
const res = await bindCustomDomain({
tid: currentTid.value,
domain: bindForm.domain,
});
if (res.code === 200) {
ElMessage.success(res.msg || "绑定成功");
bindDialogVisible.value = false;
bindForm.domain = "";
fetchDomains();
} else {
ElMessage.error(res.msg || "绑定失败");
}
} finally {
bindLoading.value = false;
}
};
const handleCheckDns = async (row: any) => {
checkingId.value = row.id;
try {
const res = await checkCustomDomainDns(row.id);
if (res.code === 200) {
const data = res.data || {};
if (data.verify_status === 1) {
ElMessage.success(data.ssl_message || data.verify_msg || "解析检测通过");
} else {
ElMessage.warning(data.verify_msg || "解析尚未生效,请稍后重试");
}
fetchDomains();
} else {
ElMessage.error(res.msg || "检测失败");
}
} finally {
checkingId.value = null;
}
};
const handleCopy = (row: any) => {
navigator.clipboard.writeText(domainUrl(row));
ElMessage.success("链接已复制到剪贴板");
};
const handleDelete = async (row: any) => {
try {
const extra = isCustom(row)
? "删除后该域名将无法访问,已签发的证书也不再续期。"
: "";
await ElMessageBox.confirm(
`确定要删除域名 "${row.full_domain}" 吗?`,
`确定要删除域名 "${row.full_domain}" 吗?${extra}`,
"提示",
{
confirmButtonText: "确定",
@@ -221,7 +490,8 @@ const handleDelete = async (row: any) => {
};
onMounted(() => {
fetchDomains();
fetchTenants();
fetchGuide();
});
</script>
@@ -242,10 +512,17 @@ onMounted(() => {
}
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.form-tip {
font-size: 12px;
color: #999;
margin-top: 5px;
line-height: 1.6;
}
.domain-preview {
@@ -255,4 +532,34 @@ onMounted(() => {
font-size: 14px;
color: #409eff;
}
.text-muted {
color: #999;
}
.guide-box {
margin-top: 4px;
}
.guide-body {
font-size: 13px;
line-height: 1.9;
p {
margin: 0;
}
code {
padding: 1px 6px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 3px;
color: #409eff;
}
}
.guide-note {
color: #909399;
}
</style>