增加自定义绑定域名
This commit is contained in:
@@ -2,10 +2,16 @@
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>我的域名</h2>
|
||||
<el-button type="primary" @click="dialogVisible = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
申请二级域名
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="applyDialogVisible = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
申请二级域名
|
||||
</el-button>
|
||||
<el-button type="success" @click="openBindDialog">
|
||||
<el-icon><Link /></el-icon>
|
||||
绑定自有域名
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
@@ -19,36 +25,89 @@
|
||||
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-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="'http://' + scope.row.full_domain"
|
||||
: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 +122,21 @@
|
||||
</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 +149,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 +164,120 @@
|
||||
<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">
|
||||
平台尚未配置解析目标,请联系平台管理员补全后再绑定。
|
||||
</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";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
// 域名类型:与后端 models.DomainTypeCustom 对应
|
||||
const DOMAIN_TYPE_CUSTOM = 2;
|
||||
|
||||
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 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 +289,58 @@ 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 fetchDomains = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 获取我的域名列表
|
||||
const res = await getMyDomains({ tid: authStore.user.tid });
|
||||
// 我的域名列表:租户 ID 由后端从登录态取,前端不再传 tid
|
||||
const res = await getMyDomains();
|
||||
if (res.code === 200) {
|
||||
tableData.value = res.data || [];
|
||||
}
|
||||
|
||||
// 获取可选主域名
|
||||
const domainRes = await getEnabledDomains();
|
||||
if (domainRes.code === 200) {
|
||||
domainList.value = domainRes.data || [];
|
||||
@@ -168,37 +350,92 @@ 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,
|
||||
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({ 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: "确定",
|
||||
@@ -222,6 +459,7 @@ const handleDelete = async (row: any) => {
|
||||
|
||||
onMounted(() => {
|
||||
fetchDomains();
|
||||
fetchGuide();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -242,10 +480,16 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.domain-preview {
|
||||
@@ -255,4 +499,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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user