mirror of https://github.com/allinssl/allinssl
【修复】申请配置证书CA列表,授权api新增新增btdomain
parent
51548f6788
commit
b91fd107ee
File diff suppressed because one or more lines are too long
|
|
@ -103,10 +103,9 @@ export default defineComponent({
|
|||
language="custom-logs"
|
||||
trim={false}
|
||||
fontSize={14}
|
||||
lineHeight={1.5}
|
||||
class="h-full" // NLog 充满 NSpin
|
||||
style={{
|
||||
// height: '500px', // 改为 flex 布局后,由父容器控制高度
|
||||
lineHeight: '1.5rem',
|
||||
border: '1px solid var(--n-border-color)',
|
||||
borderRadius: 'var(--n-border-radius)', // 使用 Naive UI 变量
|
||||
padding: '10px',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
.flowContainer {
|
||||
@apply flex relative box-border w-full h-[calc(100vh-19rem)] overflow-x-auto overflow-y-auto p-[1rem] justify-center;
|
||||
@apply flex relative box-border w-full h-[calc(100vh-19rem)] overflow-hidden p-[1rem] justify-center;
|
||||
user-select: none; /* 防止拖拽时选中文本 */
|
||||
cursor: grab; /* 默认鼠标样式 */
|
||||
}
|
||||
|
||||
.flowContainer:active {
|
||||
cursor: grabbing; /* 拖拽时的鼠标样式 */
|
||||
}
|
||||
|
||||
.flowProcess {
|
||||
|
|
@ -7,7 +13,7 @@
|
|||
}
|
||||
|
||||
.flowZoom {
|
||||
@apply flex fixed items-center justify-between h-[4rem] w-[12.5rem] bottom-[4rem] z-[99];
|
||||
@apply flex fixed items-center justify-between h-[4rem] w-[16rem] bottom-[6rem] z-[99];
|
||||
}
|
||||
|
||||
.flowZoomIcon {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NButton, NIcon, NInput } from 'naive-ui'
|
||||
import { SaveOutlined, ArrowLeftOutlined } from '@vicons/antd'
|
||||
import { SaveOutlined, ArrowLeftOutlined, ReloadOutlined } from "@vicons/antd";
|
||||
import { $t } from '@locales/index'
|
||||
import SvgIcon from '@components/SvgIcon'
|
||||
|
||||
|
|
@ -14,47 +14,188 @@ import type { FlowNode, FlowNodeProps } from './types'
|
|||
import { useThemeCssVar } from '@baota/naive-ui/theme'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FlowChart',
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'quick' | 'advanced'>,
|
||||
default: 'quick',
|
||||
},
|
||||
node: {
|
||||
type: Object as PropType<FlowNode>,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 任务节点列表
|
||||
taskComponents: {
|
||||
type: Object as PropType<Record<string, Component>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props: FlowNodeProps, { slots }) {
|
||||
const cssVars = useThemeCssVar([
|
||||
'borderColor',
|
||||
'dividerColor',
|
||||
'textColor1',
|
||||
'textColor2',
|
||||
'primaryColor',
|
||||
'primaryColorHover',
|
||||
'bodyColor',
|
||||
])
|
||||
const { flowData, selectedNodeId, flowZoom, resetFlowData } = useStore()
|
||||
const { initData, handleSaveConfig, handleZoom, goBack } = useController({
|
||||
type: props?.type,
|
||||
node: props?.node,
|
||||
isEdit: props?.isEdit,
|
||||
})
|
||||
// 提供任务节点组件映射给后代组件使用
|
||||
provide('taskComponents', props.taskComponents)
|
||||
onMounted(initData)
|
||||
onUnmounted(resetFlowData)
|
||||
return () => (
|
||||
name: "FlowChart",
|
||||
props: {
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<"quick" | "advanced">,
|
||||
default: "quick",
|
||||
},
|
||||
node: {
|
||||
type: Object as PropType<FlowNode>,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 任务节点列表
|
||||
taskComponents: {
|
||||
type: Object as PropType<Record<string, Component>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props: FlowNodeProps, { slots }) {
|
||||
const cssVars = useThemeCssVar([
|
||||
"borderColor",
|
||||
"dividerColor",
|
||||
"textColor1",
|
||||
"textColor2",
|
||||
"primaryColor",
|
||||
"primaryColorHover",
|
||||
"bodyColor",
|
||||
]);
|
||||
const { flowData, selectedNodeId, flowZoom, resetFlowData, setZoomValue } =
|
||||
useStore();
|
||||
const { initData, handleSaveConfig, handleZoom, goBack } = useController({
|
||||
type: props?.type,
|
||||
node: props?.node,
|
||||
isEdit: props?.isEdit,
|
||||
});
|
||||
|
||||
// 拖拽状态管理
|
||||
const dragState = reactive({
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
});
|
||||
|
||||
// 画布容器引用
|
||||
const canvasRef = ref<HTMLElement | null>(null);
|
||||
|
||||
// 画布样式(通过transform实现拖拽)
|
||||
const canvasStyle = computed(() => ({
|
||||
transform: `translate(${dragState.offsetX}px, ${dragState.offsetY}px)`,
|
||||
}));
|
||||
|
||||
// 鼠标按下事件
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
// 只有左键点击才触发拖拽
|
||||
if (e.button !== 0) return;
|
||||
|
||||
dragState.isDragging = true;
|
||||
dragState.startX = e.clientX;
|
||||
dragState.startY = e.clientY;
|
||||
|
||||
// 添加鼠标样式反馈
|
||||
document.body.style.cursor = "grabbing";
|
||||
};
|
||||
|
||||
// 鼠标移动事件
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!dragState.isDragging || !isComponentMounted.value) return;
|
||||
|
||||
try {
|
||||
// 计算位移差
|
||||
const deltaX = e.clientX - dragState.startX;
|
||||
const deltaY = e.clientY - dragState.startY;
|
||||
|
||||
// 更新偏移量
|
||||
dragState.offsetX += deltaX;
|
||||
dragState.offsetY += deltaY;
|
||||
|
||||
// 更新起始位置(用于连续计算)
|
||||
dragState.startX = e.clientX;
|
||||
dragState.startY = e.clientY;
|
||||
} catch (error) {
|
||||
console.warn("拖拽移动处理出错:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标抬起事件
|
||||
const handleMouseUp = () => {
|
||||
if (dragState.isDragging) {
|
||||
dragState.isDragging = false;
|
||||
document.body.style.cursor = ""; // 恢复鼠标样式
|
||||
}
|
||||
};
|
||||
|
||||
// 重置缩放和位置
|
||||
const handleReset = () => {
|
||||
try {
|
||||
// 重置缩放为100%
|
||||
setZoomValue(100);
|
||||
// 重置拖拽位置
|
||||
dragState.offsetX = 0;
|
||||
dragState.offsetY = 0;
|
||||
} catch (error) {
|
||||
console.warn("重置处理出错:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 滚轮缩放事件(带节流优化)
|
||||
let wheelTimeout: number | null = null;
|
||||
const isComponentMounted = ref(true);
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
// 阻止默认滚动行为
|
||||
e.preventDefault();
|
||||
|
||||
// 如果组件已卸载,不处理事件
|
||||
if (!isComponentMounted.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 节流处理,避免频繁触发
|
||||
if (wheelTimeout) {
|
||||
clearTimeout(wheelTimeout);
|
||||
}
|
||||
|
||||
wheelTimeout = window.setTimeout(() => {
|
||||
// 再次检查组件是否仍然挂载
|
||||
if (!isComponentMounted.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const delta = e.deltaY > 0 ? -10 : 10; // 向下滚动缩小,向上滚动放大
|
||||
const newZoom = Math.max(50, Math.min(300, flowZoom.value + delta));
|
||||
|
||||
if (newZoom !== flowZoom.value) {
|
||||
setZoomValue(newZoom); // 直接设置缩放值
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("滚轮缩放处理出错:", error);
|
||||
}
|
||||
}, 16);
|
||||
};
|
||||
|
||||
// 提供任务节点组件映射给后代组件使用
|
||||
provide("taskComponents", props.taskComponents);
|
||||
|
||||
onMounted(() => {
|
||||
initData();
|
||||
// 绑定全局事件
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
// 绑定滚轮事件到画布容器
|
||||
if (canvasRef.value) {
|
||||
canvasRef.value.addEventListener("wheel", handleWheel, {
|
||||
passive: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 标记组件已卸载
|
||||
isComponentMounted.value = false;
|
||||
|
||||
resetFlowData();
|
||||
// 解绑事件,避免内存泄漏
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
// 解绑滚轮事件
|
||||
if (canvasRef.value) {
|
||||
canvasRef.value.removeEventListener("wheel", handleWheel);
|
||||
}
|
||||
// 清理定时器
|
||||
if (wheelTimeout) {
|
||||
clearTimeout(wheelTimeout);
|
||||
wheelTimeout = null;
|
||||
}
|
||||
});
|
||||
return () => (
|
||||
<div class="flex flex-col w-full h-full" style={cssVars.value}>
|
||||
<div class="w-full h-[6rem] px-[2rem] mb-[2rem] rounded-lg flex items-center gap-2 justify-between">
|
||||
<div class="flex items-center">
|
||||
|
|
@ -85,13 +226,25 @@ export default defineComponent({
|
|||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.flowContainer}>
|
||||
<div
|
||||
class={styles.flowContainer}
|
||||
ref={canvasRef}
|
||||
onMousedown={handleMouseDown}
|
||||
onMouseleave={handleMouseUp}
|
||||
>
|
||||
{/* 左侧流程容器 */}
|
||||
<div class="flex min-w-0">
|
||||
{/* 流程容器*/}
|
||||
<div
|
||||
class={styles.flowProcess}
|
||||
style={{ transform: `scale(${flowZoom.value / 100})` }}
|
||||
style={{
|
||||
transform: `scale(${flowZoom.value / 100}) ${
|
||||
canvasStyle.value.transform
|
||||
}`,
|
||||
transition: dragState.isDragging
|
||||
? "none"
|
||||
: "transform 0.05s ease-out",
|
||||
}}
|
||||
>
|
||||
{/* 渲染流程节点 */}
|
||||
<NodeWrap node={flowData.value.childNode} />
|
||||
|
|
@ -117,10 +270,19 @@ export default defineComponent({
|
|||
color="#5a5e66"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class={styles.flowZoomIcon}
|
||||
onClick={handleReset}
|
||||
title="重置视图"
|
||||
>
|
||||
<NIcon size="16" color="#5a5e66">
|
||||
<ReloadOutlined />
|
||||
</NIcon>
|
||||
</div>
|
||||
</div>
|
||||
{/* 保留原有插槽 */}
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
})
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,29 +20,29 @@ import { $t } from '@locales/index'
|
|||
* 用于管理流程图的状态、缩放等数据
|
||||
*/
|
||||
export const useFlowStore = defineStore('flow-store', () => {
|
||||
const flowData = ref<FlowNode>({
|
||||
id: '',
|
||||
name: '',
|
||||
childNode: {
|
||||
id: 'start-1',
|
||||
name: '开始',
|
||||
type: 'start',
|
||||
config: {
|
||||
exec_type: 'manual',
|
||||
},
|
||||
childNode: null,
|
||||
},
|
||||
}) // 流程图数据
|
||||
const flowZoom = ref(100) // 流程图缩放比例
|
||||
const advancedOptions = ref(false) // 高级选项
|
||||
const addNodeSelectList = ref<NodeSelect[]>([]) // 添加节点选项列表
|
||||
const excludeNodeSelectList = ref<NodeNum[]>([]) // 排除的节点选项列表
|
||||
const addNodeBtnRef = ref<HTMLElement | null>(null) // 添加节点按钮
|
||||
const addNodeSelectRef = ref<HTMLElement | null>(null) // 添加节点选择框
|
||||
const addNodeSelectPostion = ref<number | null>(null) // 添加节点选择框位置
|
||||
const selectedNodeId = ref<string | null>(null) // 当前选中的节点ID
|
||||
const isRefreshNode = ref<string | null>(null) // 是否刷新节点
|
||||
const startNodeSavedByUser = ref<boolean>(false); // 开始节点是否已被用户手动保存过
|
||||
const flowData = ref<FlowNode>({
|
||||
id: "",
|
||||
name: "",
|
||||
childNode: {
|
||||
id: "start-1",
|
||||
name: "开始",
|
||||
type: "start",
|
||||
config: {
|
||||
exec_type: "manual",
|
||||
},
|
||||
childNode: null,
|
||||
},
|
||||
}); // 流程图数据
|
||||
const flowZoom = ref(100); // 流程图缩放比例
|
||||
const advancedOptions = ref(false); // 高级选项
|
||||
const addNodeSelectList = ref<NodeSelect[]>([]); // 添加节点选项列表
|
||||
const excludeNodeSelectList = ref<NodeNum[]>([]); // 排除的节点选项列表
|
||||
const addNodeBtnRef = ref<HTMLElement | null>(null); // 添加节点按钮
|
||||
const addNodeSelectRef = ref<HTMLElement | null>(null); // 添加节点选择框
|
||||
const addNodeSelectPostion = ref<number | null>(null); // 添加节点选择框位置
|
||||
const selectedNodeId = ref<string | null>(null); // 当前选中的节点ID
|
||||
const isRefreshNode = ref<string | null>(null); // 是否刷新节点
|
||||
const startNodeSavedByUser = ref<boolean>(false); // 开始节点是否已被用户手动保存过
|
||||
|
||||
// 计算添加节点选项列表,排除的节点选项列表
|
||||
const nodeSelectList = computed(() => {
|
||||
|
|
@ -606,16 +606,30 @@ export const useFlowStore = defineStore('flow-store', () => {
|
|||
/**
|
||||
* 设置流程图缩放比例
|
||||
* 控制流程图的显示大小
|
||||
* @param {number} type - 缩放类型:1 表示缩小,2 表示放大
|
||||
* @param {number} type - 缩放类型:1 表示缩小,2 表示放大,或者直接传入缩放值(0.5-3.0)
|
||||
*/
|
||||
const setflowZoom = (type: number) => {
|
||||
if (type === 1 && flowZoom.value > 50) {
|
||||
// 如果传入的是小数(0.5-3.0),直接设置缩放值
|
||||
if (type < 1) {
|
||||
const zoomValue = Math.round(type * 100);
|
||||
flowZoom.value = Math.max(50, Math.min(300, zoomValue));
|
||||
} else if (type === 1 && flowZoom.value > 50) {
|
||||
// 缩小
|
||||
flowZoom.value -= 10;
|
||||
} else if (type === 2 && flowZoom.value < 300) {
|
||||
// 放大
|
||||
flowZoom.value += 10;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 直接设置缩放值(用于滚轮缩放)
|
||||
* @param {number} zoomValue - 缩放值(50-300)
|
||||
*/
|
||||
const setZoomValue = (zoomValue: number) => {
|
||||
flowZoom.value = Math.max(50, Math.min(300, zoomValue));
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置开始节点已被用户保存的状态
|
||||
* @param {boolean} saved - 是否已被保存
|
||||
|
|
@ -632,7 +646,7 @@ export const useFlowStore = defineStore('flow-store', () => {
|
|||
startNodeSavedByUser.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
return {
|
||||
// 数据
|
||||
flowData, // 流程图数据
|
||||
flowZoom, // 流程图缩放比例
|
||||
|
|
@ -649,6 +663,7 @@ export const useFlowStore = defineStore('flow-store', () => {
|
|||
getResultData, // 获取流程图数据
|
||||
updateFlowData, // 更新流程图数据
|
||||
setflowZoom, // 设置流程图缩放比例
|
||||
setZoomValue, // 直接设置缩放值(用于滚轮缩放)
|
||||
setStartNodeSavedByUser, // 设置开始节点已被用户保存的状态
|
||||
resetStartNodeSavedState, // 重置开始节点保存状态
|
||||
|
||||
|
|
|
|||
|
|
@ -271,6 +271,13 @@ export const ApiProjectConfig: Record<string, ApiProjectType> = {
|
|||
hostRelated: { default: { name: "Spaceship" } },
|
||||
sort: 32,
|
||||
},
|
||||
btdomain: {
|
||||
name: "BTDomain",
|
||||
icon: "btdomain",
|
||||
type: ["dns"],
|
||||
hostRelated: { default: { name: "BTDomain" } },
|
||||
sort: 33,
|
||||
},
|
||||
plugin: {
|
||||
name: "插件",
|
||||
icon: "plugin",
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export interface AddAccessParams<
|
|||
| ConstellixAccessConfig
|
||||
| WebhookAccessConfig
|
||||
| SpaceshipAccessConfig
|
||||
| BTDomainAccessConfig
|
||||
> {
|
||||
name: string;
|
||||
type: string;
|
||||
|
|
@ -102,6 +103,7 @@ export interface UpdateAccessParams<
|
|||
| ConstellixAccessConfig
|
||||
| WebhookAccessConfig
|
||||
| SpaceshipAccessConfig
|
||||
| BTDomainAccessConfig
|
||||
> extends AddAccessParams<T> {
|
||||
id: string;
|
||||
}
|
||||
|
|
@ -322,6 +324,12 @@ export interface SpaceshipAccessConfig {
|
|||
api_secret: string;
|
||||
}
|
||||
|
||||
export interface BTDomainAccessConfig {
|
||||
access_key: string;
|
||||
secret_key: string;
|
||||
account_id: string;
|
||||
}
|
||||
|
||||
/** 删除授权请求参数 */
|
||||
export interface DeleteAccessParams {
|
||||
id: string
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import type {
|
|||
ConstellixAccessConfig,
|
||||
WebhookAccessConfig,
|
||||
SpaceshipAccessConfig,
|
||||
BTDomainAccessConfig,
|
||||
} from "@/types/access";
|
||||
import type { VNode, Ref } from "vue";
|
||||
import { testAccess, getPlugins } from "@/api/access";
|
||||
|
|
@ -494,7 +495,7 @@ export const useApiFormController = (
|
|||
value: string,
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (!value.length) {
|
||||
if (!value || !value.length) {
|
||||
const mapTips = {
|
||||
cloudflare: $t("t_0_1747042966820"),
|
||||
btpanel: $t("t_1_1747042969705"),
|
||||
|
|
@ -523,6 +524,25 @@ export const useApiFormController = (
|
|||
const mapTips = {
|
||||
godaddy: $t("t_1_1747984133312"),
|
||||
spaceship: "请输入 Spaceship API Secret",
|
||||
btdomain: "请输入 BTDomain Secret Key",
|
||||
};
|
||||
return callback(
|
||||
new Error(mapTips[param.value.type as keyof typeof mapTips])
|
||||
);
|
||||
}
|
||||
callback();
|
||||
},
|
||||
},
|
||||
account_id: {
|
||||
trigger: "input",
|
||||
validator: (
|
||||
rule: FormItemRule,
|
||||
value: string,
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (!value) {
|
||||
const mapTips = {
|
||||
btdomain: "请输入 BTDomain Account ID",
|
||||
};
|
||||
return callback(
|
||||
new Error(mapTips[param.value.type as keyof typeof mapTips])
|
||||
|
|
@ -621,6 +641,7 @@ export const useApiFormController = (
|
|||
volcengine: $t("t_3_1747365600828"),
|
||||
qiniu: $t("t_3_1747984134586"),
|
||||
doge: $t("t_0_1750320239265"),
|
||||
btdomain: "请输入 BTDomain Access Key",
|
||||
};
|
||||
return callback(
|
||||
new Error(mapTips[param.value.type as keyof typeof mapTips])
|
||||
|
|
@ -636,7 +657,7 @@ export const useApiFormController = (
|
|||
value: string,
|
||||
callback: (error?: Error) => void
|
||||
) => {
|
||||
if (!value.length) {
|
||||
if (!value || !value.length) {
|
||||
const mapTips = {
|
||||
tencentcloud: $t("t_2_1747042967277"),
|
||||
huawei: $t("t_3_1747042967608"),
|
||||
|
|
@ -644,6 +665,7 @@ export const useApiFormController = (
|
|||
volcengine: $t("t_4_1747365600137"),
|
||||
doge: $t("t_1_1750320241427"),
|
||||
constellix: "请输入Secret Key",
|
||||
btdomain: "请输入 BTDomain Secret Key",
|
||||
};
|
||||
return callback(
|
||||
new Error(mapTips[param.value.type as keyof typeof mapTips])
|
||||
|
|
@ -1230,6 +1252,21 @@ export const useApiFormController = (
|
|||
})
|
||||
);
|
||||
break;
|
||||
case "btdomain":
|
||||
items.push(
|
||||
useFormInput("Access Key", "config.access_key", {
|
||||
allowInput: noSideSpace,
|
||||
}),
|
||||
useFormInput("Secret Key", "config.secret_key", {
|
||||
type: "password",
|
||||
showPasswordOn: "click",
|
||||
allowInput: noSideSpace,
|
||||
}),
|
||||
useFormInput("Account ID", "config.account_id", {
|
||||
allowInput: noSideSpace,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "plugin":
|
||||
items.push(
|
||||
useFormCustom(() => {
|
||||
|
|
@ -1478,7 +1515,14 @@ export const useApiFormController = (
|
|||
api_key: "",
|
||||
api_secret: "",
|
||||
} as SpaceshipAccessConfig;
|
||||
break;
|
||||
break;
|
||||
case "btdomain":
|
||||
param.value.config = {
|
||||
access_key: "",
|
||||
secret_key: "",
|
||||
account_id: "",
|
||||
} as BTDomainAccessConfig;
|
||||
break;
|
||||
case "plugin":
|
||||
param.value.config = {
|
||||
name: pluginList.value[0]?.value || "",
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ export default defineComponent({
|
|||
const caOptions = ref<
|
||||
Array<{ label: string; value: string; icon: string }>
|
||||
>([]);
|
||||
const emailOptions = ref<string[]>([]);
|
||||
const emailOptions = ref<
|
||||
Array<{ label: string; value: string; id: number; email: string }>
|
||||
>([]);
|
||||
const isLoadingCA = ref(false);
|
||||
const isLoadingEmails = ref(false);
|
||||
const showEmailDropdown = ref(false);
|
||||
|
|
@ -88,7 +90,7 @@ export default defineComponent({
|
|||
isLoadingCA.value = true;
|
||||
try {
|
||||
const { data } = await getEabList({
|
||||
ca: param.value.ca,
|
||||
ca: "",
|
||||
p: 1,
|
||||
limit: 1000,
|
||||
}).fetch();
|
||||
|
|
@ -144,7 +146,14 @@ export default defineComponent({
|
|||
try {
|
||||
const { data } = await getEabList({ ca, p: 1, limit: 1000 }).fetch();
|
||||
emailOptions.value =
|
||||
data?.map((item) => item.email).filter(Boolean) || [];
|
||||
data
|
||||
?.map((item) => ({
|
||||
label: item.email,
|
||||
value: `${item.id}`, // 使用 id 作为 value 确保唯一性
|
||||
id: item.id,
|
||||
email: item.email,
|
||||
}))
|
||||
.filter((item) => item.email) || [];
|
||||
|
||||
// 检查是否为编辑模式且有外部传入的邮箱
|
||||
if (isEdit.value && routeEmail.value) {
|
||||
|
|
@ -154,15 +163,19 @@ export default defineComponent({
|
|||
// 非编辑模式:保持原有逻辑
|
||||
if (!emailOptions.value.length) {
|
||||
param.value.email = "";
|
||||
param.value.eabId = "";
|
||||
} else {
|
||||
// 如果邮箱数组有内容,自动填充第一个邮箱地址
|
||||
// 移除 !param.value.email 条件,让切换CA时总是更新为第一个选项
|
||||
if (emailOptions.value[0]) {
|
||||
param.value.email = emailOptions.value[0].email;
|
||||
param.value.eabId = emailOptions.value[0].id.toString();
|
||||
}
|
||||
}
|
||||
// 如果邮箱数组有内容且当前邮箱为空,自动填充第一个邮箱地址
|
||||
if (
|
||||
emailOptions.value.length > 0 &&
|
||||
emailOptions.value[0] &&
|
||||
!param.value.email
|
||||
) {
|
||||
param.value.email = emailOptions.value[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (example.value) {
|
||||
example.value.restoreValidation();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载邮件选项失败:", error);
|
||||
|
|
@ -239,17 +252,35 @@ export default defineComponent({
|
|||
|
||||
// 创建邮箱下拉选项
|
||||
const emailDropdownOptions = computed(() => {
|
||||
return emailOptions.value.map((email) => ({
|
||||
label: email,
|
||||
key: email,
|
||||
return emailOptions.value.map((item) => ({
|
||||
label: item.email,
|
||||
key: item.email,
|
||||
}));
|
||||
});
|
||||
|
||||
// 计算输入框宽度用于下拉菜单
|
||||
const inputWidth = computed(() => {
|
||||
if (emailInputRef.value?.$el) {
|
||||
return emailInputRef.value.$el.offsetWidth;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// 判断是否需要输入框(letsencrypt、buypass、zerossl)
|
||||
const shouldUseInputForEmail = computed(() => {
|
||||
return ["letsencrypt", "buypass", "zerossl"].includes(param.value.ca);
|
||||
});
|
||||
|
||||
// 计算当前选中的邮箱选项的 value(用于 NSelect)
|
||||
const currentEmailValue = computed(() => {
|
||||
if (!param.value.eabId) return null;
|
||||
// 优先使用 eabId 来查找匹配的选项
|
||||
const matchedOption = emailOptions.value.find(
|
||||
(item) => item.id.toString() === param.value.eabId
|
||||
);
|
||||
return matchedOption ? matchedOption.value : null;
|
||||
});
|
||||
|
||||
// 表单渲染配置
|
||||
const config = computed(() => {
|
||||
// 基本选项
|
||||
|
|
@ -343,7 +374,20 @@ export default defineComponent({
|
|||
options={emailDropdownOptions.value}
|
||||
onSelect={handleSelectEmail}
|
||||
placement="bottom-start"
|
||||
style="width: 100%"
|
||||
menu-props={() => ({
|
||||
style: {
|
||||
width: `${inputWidth.value}px`,
|
||||
maxHeight: "40rem",
|
||||
overflowY: "auto",
|
||||
},
|
||||
})}
|
||||
node-props={(option: any) => ({
|
||||
style: {
|
||||
padding: "8px 12px",
|
||||
cursor: "pointer",
|
||||
},
|
||||
class: "hover:bg-gray-50",
|
||||
})}
|
||||
>
|
||||
<NInput
|
||||
ref={emailInputRef}
|
||||
|
|
@ -358,16 +402,26 @@ export default defineComponent({
|
|||
</NDropdown>
|
||||
) : (
|
||||
<NSelect
|
||||
v-model:value={param.value.email}
|
||||
options={emailOptions.value.map((email) => ({
|
||||
label: email,
|
||||
value: email,
|
||||
}))}
|
||||
value={currentEmailValue.value}
|
||||
options={emailOptions.value}
|
||||
placeholder={$t("t_2_1748052862259")}
|
||||
clearable
|
||||
filterable
|
||||
loading={isLoadingEmails.value}
|
||||
class="w-full"
|
||||
onUpdateValue={(value: string) => {
|
||||
// 根据选择的 id 找到对应的邮箱地址和 eabId
|
||||
const selectedOption = emailOptions.value.find(
|
||||
(item) => item.value === value
|
||||
);
|
||||
if (selectedOption) {
|
||||
param.value.email = selectedOption.email;
|
||||
param.value.eabId = selectedOption.id.toString();
|
||||
} else {
|
||||
param.value.email = value;
|
||||
param.value.eabId = "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</NFormItem>
|
||||
|
|
@ -545,6 +599,7 @@ export default defineComponent({
|
|||
} else {
|
||||
emailOptions.value = [];
|
||||
param.value.email = "";
|
||||
param.value.eabId = "";
|
||||
showEmailDropdown.value = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -564,14 +619,14 @@ export default defineComponent({
|
|||
advancedOptions.value = false;
|
||||
await loadCAOptions();
|
||||
|
||||
// 如果当前已经有CA值,主动加载对应的邮件选项
|
||||
if (param.value.ca) {
|
||||
await loadEmailOptions(param.value.ca);
|
||||
}
|
||||
|
||||
// 如果是编辑模式且有外部传入的邮箱,直接设置邮箱值
|
||||
if (isEdit.value && routeEmail.value) {
|
||||
param.value.email = routeEmail.value;
|
||||
} else {
|
||||
// 非编辑模式:如果当前已经有CA值,主动加载对应的邮件选项
|
||||
if (param.value.ca) {
|
||||
await loadEmailOptions(param.value.ca);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除重复调用,让 watch 监听器处理 CA 值变化
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { defineComponent, ref, computed, watch } from 'vue';
|
||||
import { defineComponent, ref, computed, watch } from "vue";
|
||||
import {
|
||||
NForm,
|
||||
NFormItem,
|
||||
|
|
@ -127,7 +127,7 @@ export default defineComponent({
|
|||
const selectedRootCa = rootCaList.value.find(
|
||||
(ca) => ca.id.toString() === newRootId
|
||||
);
|
||||
if (selectedRootCa) {
|
||||
if (selectedRootCa && selectedRootCa.algorithm) {
|
||||
addForm.value.algorithm = selectedRootCa.algorithm;
|
||||
if (selectedRootCa.algorithm === "ecdsa") {
|
||||
addForm.value.key_length = "256";
|
||||
|
|
@ -269,45 +269,43 @@ export default defineComponent({
|
|||
</div>
|
||||
</NDivider>
|
||||
</div>
|
||||
{showAdvancedConfig.value && (
|
||||
<div class="space-y-4 mt-4">
|
||||
<NFormItem label="组织(O)">
|
||||
<NInput
|
||||
v-model:value={addForm.value.o}
|
||||
placeholder="请输入组织名称"
|
||||
/>
|
||||
</NFormItem>
|
||||
<div class="mt-4" v-show={showAdvancedConfig.value}>
|
||||
<NFormItem label="组织(O)">
|
||||
<NInput
|
||||
v-model:value={addForm.value.o}
|
||||
placeholder="请输入组织名称"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="国家(C)" path="c" required>
|
||||
<NSelect
|
||||
v-model:value={addForm.value.c}
|
||||
options={countryOptions}
|
||||
placeholder="请选择国家"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="国家(C)" path="c" required>
|
||||
<NSelect
|
||||
v-model:value={addForm.value.c}
|
||||
options={countryOptions}
|
||||
placeholder="请选择国家"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="组织单位(OU)">
|
||||
<NInput
|
||||
v-model:value={addForm.value.ou}
|
||||
placeholder="请输入组织单位"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="组织单位(OU)">
|
||||
<NInput
|
||||
v-model:value={addForm.value.ou}
|
||||
placeholder="请输入组织单位"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="省份">
|
||||
<NInput
|
||||
v-model:value={addForm.value.province}
|
||||
placeholder="请输入省份"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="省份">
|
||||
<NInput
|
||||
v-model:value={addForm.value.province}
|
||||
placeholder="请输入省份"
|
||||
/>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="城市">
|
||||
<NInput
|
||||
v-model:value={addForm.value.locality}
|
||||
placeholder="请输入城市"
|
||||
/>
|
||||
</NFormItem>
|
||||
</div>
|
||||
)}
|
||||
<NFormItem label="城市">
|
||||
<NInput
|
||||
v-model:value={addForm.value.locality}
|
||||
placeholder="请输入城市"
|
||||
/>
|
||||
</NFormItem>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<NButton onClick={handleCancel}>取消</NButton>
|
||||
|
|
|
|||
|
|
@ -251,8 +251,10 @@ export const useController = () => {
|
|||
if (data.value?.status === true) {
|
||||
rootCaList.value = data.value.data;
|
||||
if (createType.value === 'intermediate' && rootCaList.value.length > 0 && rootCaList.value[0]) {
|
||||
addForm.value.root_id = rootCaList.value[0].id.toString();
|
||||
}
|
||||
addForm.value.root_id = rootCaList.value[0].id.toString();
|
||||
addForm.value.algorithm = rootCaList.value[0].algorithm;
|
||||
addForm.value.key_length = rootCaList.value[0].key_length.toString();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取根证书列表失败:', error);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* @fileoverview API密钥管理相关 API 接口
|
||||
* @description 提供API密钥的创建、更新、删除、查询等管理功能
|
||||
*/
|
||||
|
||||
import { useApi } from '@api/index'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { CreateApiKeyRequest, CreateApiKeyResponse, ApiKeyListRequest, ApiKeyListResponse, UpdateApiKeyRequest, UpdateApiKeyResponse, RegenerateApiKeyRequest, RegenerateApiKeyResponse, DeleteApiKeyRequest, DeleteApiKeyResponse } from '@/types/api-key'
|
||||
|
||||
/**
|
||||
* @description 创建API密钥
|
||||
* @param {CreateApiKeyRequest} params 创建参数,包含密钥名称和IP白名单
|
||||
* @returns {useAxiosReturn<ApiResponse<CreateApiKeyResponse>, CreateApiKeyRequest>} 返回创建操作结果
|
||||
*/
|
||||
export const createApi = (params: CreateApiKeyRequest) =>
|
||||
useApi<ApiResponse<CreateApiKeyResponse>, CreateApiKeyRequest>('/v1/user/apiconfig/create', params)
|
||||
|
||||
/**
|
||||
* @description 获取API密钥列表
|
||||
* @param {ApiKeyListRequest} params 查询参数,包含分页和筛选条件
|
||||
* @returns {useAxiosReturn<ApiResponse<ApiKeyListResponse>, ApiKeyListRequest>} 返回列表数据
|
||||
*/
|
||||
export const getApiKeyList = (params: ApiKeyListRequest) =>
|
||||
useApi<ApiResponse<ApiKeyListResponse>, ApiKeyListRequest>('/v1/user/apiconfig/list', params)
|
||||
|
||||
/**
|
||||
* @description 更新API密钥
|
||||
* @param {UpdateApiKeyRequest} params 更新参数,包含密钥ID、名称、状态和IP白名单
|
||||
* @returns {useAxiosReturn<ApiResponse<UpdateApiKeyResponse>, UpdateApiKeyRequest>} 返回更新操作结果
|
||||
*/
|
||||
export const updateApiKey = (params: UpdateApiKeyRequest) =>
|
||||
useApi<ApiResponse<UpdateApiKeyResponse>, UpdateApiKeyRequest>('/v1/user/apiconfig/update', params)
|
||||
|
||||
/**
|
||||
* @description 重新生成API密钥
|
||||
* @param {RegenerateApiKeyRequest} params 重新生成参数,包含密钥ID
|
||||
* @returns {useAxiosReturn<ApiResponse<RegenerateApiKeyResponse>, RegenerateApiKeyRequest>} 返回重新生成操作结果
|
||||
*/
|
||||
export const regenerateApiKey = (params: RegenerateApiKeyRequest) =>
|
||||
useApi<ApiResponse<RegenerateApiKeyResponse>, RegenerateApiKeyRequest>('/v1/user/apiconfig/regenerate', params)
|
||||
|
||||
/**
|
||||
* @description 删除API密钥
|
||||
* @param {DeleteApiKeyRequest} params 删除参数,包含密钥ID
|
||||
* @returns {useAxiosReturn<ApiResponse<DeleteApiKeyResponse>, DeleteApiKeyRequest>} 返回删除操作结果
|
||||
*/
|
||||
export const deleteApiKey = (params: DeleteApiKeyRequest) =>
|
||||
useApi<ApiResponse<DeleteApiKeyResponse>, DeleteApiKeyRequest>('/v1/user/apiconfig/delete', params)
|
||||
|
||||
|
|
@ -18,6 +18,9 @@ import type {
|
|||
DomainPriceQueryRequest,
|
||||
DomainPriceQueryResponse,
|
||||
DomainAutoRenewRequest,
|
||||
DomainRealNameUpdateRequest,
|
||||
PrivacyRequest,
|
||||
PrivacyPriceRequest,
|
||||
} from '@/types/domain'
|
||||
import type {
|
||||
DomainTransferListRequest,
|
||||
|
|
@ -47,7 +50,14 @@ export const fetchDomainDetail = (params: DomainDetailRequest) =>
|
|||
useApi<DomainDetailResponse, DomainDetailRequest>(
|
||||
"/v1/domain/manage/detail",
|
||||
params,
|
||||
);
|
||||
);
|
||||
/**
|
||||
* @description 修改域名实名模板
|
||||
* @param {DomainRealNameUpdateRequest} params 更新参数,包含域名ID和新的实名模板id
|
||||
* @returns {useAxiosReturn<ApiResponse, DomainRealNameUpdateRequest>} 返回更新结果
|
||||
*/
|
||||
export const updateDomainRealName = (params: DomainRealNameUpdateRequest) =>
|
||||
useApi<ApiResponse, DomainRealNameUpdateRequest>('/v1/domain/manage/update_real_name_tpl', params)
|
||||
|
||||
/**
|
||||
* @description 更新域名DNS服务器
|
||||
|
|
@ -107,4 +117,50 @@ export const fetchDomainAutoRenew = (params: DomainAutoRenewRequest) =>
|
|||
|
||||
// 下载域名证书
|
||||
export const downloadDomainCertificate = (params: { domain_id: number }) =>
|
||||
useApi<ApiResponse, { domain_id: number }>('/v1/domain/manage/get_cert', params)
|
||||
useApi<ApiResponse, { domain_id: number }>('/v1/domain/manage/get_cert', params)
|
||||
|
||||
// 域名隐私保护
|
||||
export const privacyOrder = (params: PrivacyRequest) => useApi<ApiResponse, PrivacyRequest>('/v1/order/privacy', params)
|
||||
|
||||
// 隐私保护价格查询
|
||||
export const privacyPrice = (params: PrivacyPriceRequest) =>
|
||||
useApi<ApiResponse, PrivacyPriceRequest>('/v1/domain/privacy/price', params)
|
||||
|
||||
// 添加DNSSEC DS记录
|
||||
export const addDnssecDsRecord = (params: {
|
||||
domain_id: string
|
||||
key_tag: number
|
||||
alg: number
|
||||
digest_type: number
|
||||
digest: string
|
||||
}) =>
|
||||
useApi<ApiResponse, typeof params>('/v1/dns/dnssec/add_ds', params)
|
||||
|
||||
// 获取DNSSEC DS记录列表
|
||||
export const getDnssecDsList = (params: {
|
||||
domain_id: string
|
||||
}) =>
|
||||
useApi<{
|
||||
code: number
|
||||
data: Array<{
|
||||
alg: number
|
||||
created_at: number
|
||||
digest: string
|
||||
digest_type: number
|
||||
id: number
|
||||
key_tag: number
|
||||
status: number
|
||||
updated_at: number
|
||||
}>
|
||||
msg: string
|
||||
status: boolean
|
||||
}, typeof params>('/v1/dns/dnssec/get_ds_list', params)
|
||||
|
||||
// 删除DNSSEC DS记录
|
||||
export const deleteDnssecDsRecord = (params: {
|
||||
ds_id: number
|
||||
}) =>
|
||||
useApi<ApiResponse, typeof params>('/v1/dns/dnssec/del_ds', params)
|
||||
|
||||
export const syncDnssecDsRecord = (params: { domain_id: string }) =>
|
||||
useApi<ApiResponse, typeof params>('/v1/dns/dnssec/sync_ds', params)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import { useApi } from '@api/index'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
|
||||
/** 接口返回的 data 结构(成功时) */
|
||||
export interface OperateLogData {
|
||||
created_at: string
|
||||
level: string
|
||||
log_id: number
|
||||
message: string
|
||||
module: string
|
||||
remote_addr: string
|
||||
request_method: string
|
||||
request_url: string
|
||||
}
|
||||
export interface OperateLogListData {
|
||||
data: OperateLogData[]
|
||||
count: number
|
||||
row: string
|
||||
}
|
||||
|
||||
export type OperateLogResponse = ApiResponse<OperateLogListData>
|
||||
|
||||
// 操作日志查询参数接口
|
||||
export interface OperateLogRequest {
|
||||
/** 页码,从 1 开始 */
|
||||
p?: number
|
||||
/** 每页条数 */
|
||||
rows?: number
|
||||
/** 关键字搜索,作用于日志 */
|
||||
keyword?: string
|
||||
}
|
||||
/**
|
||||
* @description 查询是否完成实名以及实名信息
|
||||
*/
|
||||
export const getOperateLog = (params: OperateLogRequest) =>
|
||||
useApi<OperateLogResponse, OperateLogRequest>('/v1/user/get_log', params)
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @fileoverview 域名解析相关 API 接口
|
||||
* @description 提供域名解析列表、域名管理等功能
|
||||
*/
|
||||
|
||||
import { useApi } from '@api/index'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { ResolveListRequest, ResolveItem } from '@/views/domain-resolve/types'
|
||||
import type {
|
||||
GetDomainInfoRequest,
|
||||
DomainInfoData,
|
||||
AddExternalDomainRequest,
|
||||
AddExternalDomainResponse,
|
||||
RemoveDomainRequest,
|
||||
RemoveDomainResponse,
|
||||
ResolveListResponse,
|
||||
CheckDomainStatusRequest,
|
||||
CheckDomainStatusResponse
|
||||
} from '@/types/resolve'
|
||||
|
||||
/**
|
||||
* @description 获取域名解析列表
|
||||
* @param {ResolveListRequest} params 查询参数,包含分页、状态、关键词等
|
||||
* @returns {useAxiosReturn<ApiResponse<ResolveListResponse>, ResolveListRequest>} 返回域名解析列表数据
|
||||
*/
|
||||
export const getResolveList = (params: ResolveListRequest) =>
|
||||
useApi<ApiResponse<ResolveListResponse>, ResolveListRequest>('/v1/dns/manage/list_domains', params)
|
||||
|
||||
/**
|
||||
* @description 添加外部域名
|
||||
* @param {AddExternalDomainRequest} params 添加域名参数,包含域名和备注
|
||||
* @returns {useAxiosReturn<ApiResponse<AddExternalDomainResponse>, AddExternalDomainRequest>} 返回添加结果
|
||||
*/
|
||||
export const addExternalDomain = (params: AddExternalDomainRequest) =>
|
||||
useApi<ApiResponse<AddExternalDomainResponse>, AddExternalDomainRequest>('/v1/dns/manage/add_external_domain', params)
|
||||
|
||||
/**
|
||||
* @description 删除外部域名
|
||||
* @param {RemoveDomainRequest} params 删除域名参数,包含域名ID
|
||||
* @returns {useAxiosReturn<ApiResponse<RemoveDomainResponse>, RemoveDomainRequest>} 返回删除结果
|
||||
*/
|
||||
export const removeDomain = (params: RemoveDomainRequest) =>
|
||||
useApi<ApiResponse<RemoveDomainResponse>, RemoveDomainRequest>('/v1/dns/manage/remove_domain', params)
|
||||
|
||||
/**
|
||||
* @description 检测域名状态
|
||||
* @param {CheckDomainStatusRequest} params 检测参数,包含域名ID和域名类型
|
||||
* @returns {useAxiosReturn<ApiResponse<CheckDomainStatusResponse>, CheckDomainStatusRequest>} 返回检测结果
|
||||
*/
|
||||
export const checkDomainStatus = (params: CheckDomainStatusRequest) =>
|
||||
useApi<ApiResponse<CheckDomainStatusResponse>, CheckDomainStatusRequest>('/v1/dns/manage/check_domain_status', params)
|
||||
|
|
@ -141,6 +141,14 @@ export default defineComponent({
|
|||
|
||||
{/* 桌面端右侧:功能按钮 */}
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
class="text-[#20a53a] hover:text-[#20a53a]-800 text-sm no-underline cursor-pointer"
|
||||
href="https://qm.qq.com/q/fxbto4wZkk"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
加入QQ群
|
||||
</a>
|
||||
<NButton ghost onClick={() => handleBackToOfficial(true)}>
|
||||
返回堡塔后台
|
||||
</NButton>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ import {
|
|||
ChromeReaderModeTwotone,
|
||||
ShoppingCartOutlined,
|
||||
RepeatOutlined,
|
||||
InsertDriveFileOutlined,
|
||||
} from '@vicons/material'
|
||||
import { ShieldCheckmarkOutline, GlobeOutline } from '@vicons/ionicons5'
|
||||
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
|
||||
|
|
@ -42,6 +44,9 @@ const iconMap: Record<string, any> = {
|
|||
'shopping-cart': ShoppingCartOutlined,
|
||||
wallet: MoneyRound,
|
||||
transfer: RepeatOutlined,
|
||||
security: ShieldCheckmarkOutline,
|
||||
resolve: GlobeOutline,
|
||||
operation: InsertDriveFileOutlined,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ const RealNameTemplate = () => import('../views/real-name/index')
|
|||
const RealNameCenter = () => import('../views/real-name-center/index')
|
||||
const RechargeManage = () => import('../views/recharge/index')
|
||||
const OrderList = () => import('../views/order/index')
|
||||
const DomainSecurity = () => import('../views/domain-security/index')
|
||||
const DomainResolve = () => import('../views/domain-resolve/index')
|
||||
const OperationLog = () => import('../views/operation-log/index')
|
||||
|
||||
// 主布局组件
|
||||
const MainLayout = () => import('@components/layout/index')
|
||||
|
|
@ -92,7 +95,27 @@ const routes: RouteRecordRaw[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: 'domain-resolve',
|
||||
name: 'DomainResolve',
|
||||
component: DomainResolve,
|
||||
meta: {
|
||||
title: '域名解析',
|
||||
icon: 'resolve',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'detail/:id',
|
||||
name: 'DomainResolveDetail',
|
||||
component: () => import('../views/domain-resolve/components/DomainResolveDetail'),
|
||||
meta: {
|
||||
title: '域名解析详情',
|
||||
icon: 'resolve',
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'real-name',
|
||||
name: 'RealNameTemplate',
|
||||
|
|
@ -131,6 +154,24 @@ const routes: RouteRecordRaw[] = [
|
|||
icon: 'user-check',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'domain-security',
|
||||
name: 'DomainSecurity',
|
||||
component: DomainSecurity,
|
||||
meta: {
|
||||
title: '域名安全',
|
||||
icon: 'security',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'operation-log',
|
||||
name: 'OperationLog',
|
||||
component: OperationLog,
|
||||
meta: {
|
||||
title: '操作日志',
|
||||
icon: 'operation',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* @fileoverview API密钥管理相关类型定义
|
||||
* @description 定义API密钥创建、查询、更新等操作的数据类型
|
||||
*/
|
||||
|
||||
// API密钥创建请求参数
|
||||
export interface CreateApiKeyRequest {
|
||||
/** 密钥名称 */
|
||||
name: string
|
||||
/** IP白名单数组 */
|
||||
ip_whitelist?: string[]
|
||||
}
|
||||
|
||||
// API密钥创建响应数据
|
||||
export interface CreateApiKeyResponse {
|
||||
/** API密钥ID */
|
||||
id: number
|
||||
/** 密钥名称 */
|
||||
name: string
|
||||
/** Access Key */
|
||||
access_key: string
|
||||
/** Account ID */
|
||||
account_id: string
|
||||
/** Secret Key */
|
||||
secret_key: string
|
||||
/** IP白名单 */
|
||||
ip_whitelist?: string[]
|
||||
/** 状态:1-启用,0-禁用 */
|
||||
status: number
|
||||
/** 创建时间戳 */
|
||||
created_at: number
|
||||
}
|
||||
|
||||
// API密钥列表查询请求参数
|
||||
export interface ApiKeyListRequest {
|
||||
/** 页码 */
|
||||
p?: number
|
||||
/** 每页数量 */
|
||||
rows?: number
|
||||
/** 关键词搜索 */
|
||||
keyword?: string
|
||||
/** 状态筛选:1-启用,0-禁用 */
|
||||
status?: number | string
|
||||
}
|
||||
|
||||
// API密钥列表项
|
||||
export interface ApiKeyItem {
|
||||
/** API密钥ID */
|
||||
id: number
|
||||
/** 密钥名称 */
|
||||
name: string
|
||||
/** Access Key */
|
||||
access_key: string
|
||||
/** Secret Key */
|
||||
secret_key: string
|
||||
/** Account ID */
|
||||
account_id: string
|
||||
/** IP白名单 */
|
||||
ip_whitelist?: string[]
|
||||
/** 状态:1-启用,0-禁用 */
|
||||
status: number
|
||||
/** 状态文本 */
|
||||
status_text?: string
|
||||
/** 最后使用时间 - 字符串格式 */
|
||||
last_used_at?: string | null
|
||||
/** 最后使用IP */
|
||||
last_used_ip?: string
|
||||
/** 创建时间 */
|
||||
created_at: string
|
||||
/** 更新时间 */
|
||||
updated_at: string
|
||||
/** UID */
|
||||
uid?: number
|
||||
}
|
||||
|
||||
// API密钥列表响应数据
|
||||
export interface ApiKeyListResponse {
|
||||
/** 数据列表 */
|
||||
data: ApiKeyItem[]
|
||||
/** 总数 */
|
||||
total: number
|
||||
}
|
||||
|
||||
// API密钥删除请求参数
|
||||
export interface DeleteApiKeyRequest {
|
||||
/** API密钥ID */
|
||||
config_id: number
|
||||
}
|
||||
|
||||
// API密钥更新请求参数
|
||||
export interface UpdateApiKeyRequest {
|
||||
/** API密钥ID */
|
||||
config_id: number
|
||||
/** 密钥名称 */
|
||||
name: string
|
||||
/** 状态:1-启用,0-禁用 */
|
||||
status: number
|
||||
/** IP白名单数组 */
|
||||
ip_whitelist?: string[]
|
||||
}
|
||||
|
||||
// API密钥更新响应数据
|
||||
export interface UpdateApiKeyResponse {
|
||||
/** 更新结果消息 */
|
||||
message?: string
|
||||
}
|
||||
|
||||
// API密钥重新生成请求参数
|
||||
export interface RegenerateApiKeyRequest {
|
||||
/** API密钥ID */
|
||||
config_id: number
|
||||
}
|
||||
|
||||
// API密钥重新生成响应数据
|
||||
export interface RegenerateApiKeyResponse {
|
||||
/** Access Key */
|
||||
access_key: string
|
||||
/** Account ID */
|
||||
account_id: string
|
||||
/** Secret Key */
|
||||
secret_key: string
|
||||
/** 重新生成结果消息 */
|
||||
message?: string
|
||||
}
|
||||
|
||||
// API密钥删除响应数据
|
||||
export interface DeleteApiKeyResponse {
|
||||
/** 删除结果消息 */
|
||||
message?: string
|
||||
}
|
||||
|
|
@ -81,94 +81,104 @@ export interface DnsRecord {
|
|||
* 获取解析记录列表请求参数
|
||||
*/
|
||||
export interface GetDnsRecordListRequest {
|
||||
/** 域名ID */
|
||||
domain_id: number;
|
||||
/** 搜索关键字字段 */
|
||||
searchKey?: string;
|
||||
/** 搜索值 */
|
||||
searchValue?: string;
|
||||
/** 页码 */
|
||||
p?: number;
|
||||
/** 每页条数 */
|
||||
row?: number;
|
||||
/** 域名ID */
|
||||
domain_id: number
|
||||
/** 域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
domain_type?: number
|
||||
/** 搜索关键字字段 */
|
||||
searchKey?: string
|
||||
/** 搜索值 */
|
||||
searchValue?: string
|
||||
/** 页码 */
|
||||
p?: number
|
||||
/** 每页条数 */
|
||||
row?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解析记录列表响应
|
||||
*/
|
||||
export interface GetDnsRecordListResponse {
|
||||
/** 总记录数 */
|
||||
count: number;
|
||||
/** 当前页码 */
|
||||
page: number;
|
||||
/** 每页条数 */
|
||||
row: number;
|
||||
/** 记录列表 */
|
||||
data: DnsRecordItem[];
|
||||
/** 总记录数 */
|
||||
count: number
|
||||
/** 当前页码 */
|
||||
page: number
|
||||
/** 每页条数 */
|
||||
row: number
|
||||
/** 记录列表 */
|
||||
data: DnsRecordItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建解析记录请求参数
|
||||
*/
|
||||
export interface CreateDnsRecordRequest {
|
||||
/** 域名ID */
|
||||
domain_id: number;
|
||||
/** 解析值 */
|
||||
value: string;
|
||||
/** 主机记录 */
|
||||
record: string;
|
||||
/** 记录类型 */
|
||||
type: string;
|
||||
/** MX优先级 */
|
||||
mx: number;
|
||||
/** 生存时间 */
|
||||
ttl: number;
|
||||
/** 备注信息 */
|
||||
remark: string;
|
||||
/** 线路ID */
|
||||
viewId: number;
|
||||
/** 域名ID */
|
||||
domain_id: number
|
||||
/** 域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
domain_type?: number
|
||||
/** 解析值 */
|
||||
value: string
|
||||
/** 主机记录 */
|
||||
record: string
|
||||
/** 记录类型 */
|
||||
type: string
|
||||
/** MX优先级 */
|
||||
mx: number
|
||||
/** 生存时间 */
|
||||
ttl: number
|
||||
/** 备注信息 */
|
||||
remark: string
|
||||
/** 线路ID */
|
||||
viewId: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除解析记录请求参数
|
||||
*/
|
||||
export interface DeleteDnsRecordRequest {
|
||||
/** 记录ID */
|
||||
record_id: number | string;
|
||||
/** 域名ID */
|
||||
domain_id: number;
|
||||
/** 记录ID */
|
||||
record_id: number | string
|
||||
/** 域名ID */
|
||||
domain_id: number
|
||||
/** 域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
domain_type?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新解析记录请求参数
|
||||
*/
|
||||
export interface UpdateDnsRecordRequest {
|
||||
/** 记录ID */
|
||||
record_id: number | string;
|
||||
/** 域名ID */
|
||||
domain_id: number;
|
||||
/** 主机记录 */
|
||||
record?: string;
|
||||
/** 解析值 */
|
||||
value?: string;
|
||||
/** 记录类型 */
|
||||
type?: string;
|
||||
/** MX优先级 */
|
||||
mx?: number;
|
||||
/** 生存时间 */
|
||||
ttl?: number;
|
||||
/** 备注信息 */
|
||||
remark?: string;
|
||||
/** 线路ID */
|
||||
viewId?: number;
|
||||
/** 记录ID */
|
||||
record_id: number | string
|
||||
/** 域名ID */
|
||||
domain_id: number
|
||||
/** 域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
domain_type?: number
|
||||
/** 主机记录 */
|
||||
record?: string
|
||||
/** 解析值 */
|
||||
value?: string
|
||||
/** 记录类型 */
|
||||
type?: string
|
||||
/** MX优先级 */
|
||||
mx?: number
|
||||
/** 生存时间 */
|
||||
ttl?: number
|
||||
/** 备注信息 */
|
||||
remark?: string
|
||||
/** 线路ID */
|
||||
viewId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停/启用解析记录请求参数
|
||||
*/
|
||||
export interface ToggleDnsRecordRequest {
|
||||
/** 记录ID */
|
||||
record_id: number | string;
|
||||
/** 域名ID */
|
||||
domain_id: number;
|
||||
/** 记录ID */
|
||||
record_id: number | string
|
||||
/** 域名ID */
|
||||
domain_id: number
|
||||
/** 域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
domain_type?: number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ export type DomainListResponse = ApiResponse<DomainListData>
|
|||
export interface DomainDetailRequest {
|
||||
/** 域名 ID */
|
||||
domain_id: number
|
||||
/** 域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
domain_type?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -182,6 +184,8 @@ export interface DomainInfo {
|
|||
updated_at: number
|
||||
/** 是否自动续费(1 是,0 否) */
|
||||
auto_renew: number
|
||||
/** 隐私保护(0 否,1 是) */
|
||||
privacy: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -244,6 +248,7 @@ export interface RealNameInfo {
|
|||
verify_time: string
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 域名详情响应数据
|
||||
*/
|
||||
|
|
@ -254,6 +259,10 @@ export interface DomainDetailData {
|
|||
domain_info: DomainInfo
|
||||
/** 实名模板信息 */
|
||||
real_name_info: RealNameInfo
|
||||
/** 实名信息更新状态 */
|
||||
real_name_update_info: RealNameInfo
|
||||
/** 隐私保护信息 */
|
||||
privacy_info: PrivacyInfo
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -317,6 +326,15 @@ export interface SetDomainSecurityData {
|
|||
type: 'update' | 'transfer'
|
||||
}
|
||||
|
||||
export interface PrivacyInfo {
|
||||
/** 邮箱 */
|
||||
email: string
|
||||
/** 结束时间 */
|
||||
end_time: number
|
||||
/** 开始时间 */
|
||||
start_time: number
|
||||
}
|
||||
|
||||
export type SetDomainSecurityResponse = ApiResponse<SetDomainSecurityData>
|
||||
|
||||
// 手动刷新域名注册状态
|
||||
|
|
@ -397,4 +415,46 @@ export type DomainPriceQueryResponse = ApiResponse<DomainPriceQueryData>;
|
|||
export type DomainAutoRenewRequest = {
|
||||
domain_id: number
|
||||
status: 0 | 1
|
||||
}
|
||||
|
||||
|
||||
export interface DomainRealNameUpdateRequest {
|
||||
/** 域名 ID */
|
||||
domain_id: number
|
||||
/** 新的实名模板 ID */
|
||||
new_registrant_id: string
|
||||
}
|
||||
|
||||
export interface PrivacyRequest {
|
||||
/** 1:新购 2:续费 */
|
||||
type: number
|
||||
/** 域名 */
|
||||
domain: string
|
||||
/** 年份 */
|
||||
year: number
|
||||
/** 邮箱 */
|
||||
email?: string
|
||||
}
|
||||
/** 保护隐私下单返回数据 */
|
||||
export interface PrivacyData {
|
||||
create_time: string
|
||||
discount_price: number
|
||||
domain_count: number
|
||||
expire_time: string
|
||||
order_id: number
|
||||
order_no: string
|
||||
original_price: number
|
||||
payment_url: string
|
||||
total_price: number
|
||||
ali: string // 支付宝二维码或链接
|
||||
wx: string // 微信二维码或链接
|
||||
}
|
||||
|
||||
export type PrivacyResponse = ApiResponse<PrivacyRequest>
|
||||
|
||||
export interface PrivacyPriceRequest {
|
||||
/** 1:新购 2:续费 */
|
||||
type: number
|
||||
/** 年份 */
|
||||
year: number
|
||||
}
|
||||
|
|
@ -221,7 +221,7 @@ export type ContactDetailResponse = ApiResponse<{
|
|||
* 删除用户联系人请求参数
|
||||
*/
|
||||
export interface DelUserDetailRequest {
|
||||
/** 注册者标识 ID */
|
||||
registrant_id: string
|
||||
/** 实名模板 ID */
|
||||
id: number
|
||||
}
|
||||
export type DelUserDetailResponse = ApiResponse<{}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* @fileoverview 域名解析相关类型定义
|
||||
* @description 提供域名解析、域名管理等功能相关的TypeScript类型定义
|
||||
*/
|
||||
|
||||
import type { ResolveItem } from '@/views/domain-resolve/types'
|
||||
|
||||
/**
|
||||
* 获取域名信息请求参数
|
||||
*/
|
||||
export interface GetDomainInfoRequest {
|
||||
/** 域名ID */
|
||||
domain_id: number
|
||||
/** 域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
domain_type?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 域名信息响应数据
|
||||
*/
|
||||
export interface DomainInfoData {
|
||||
/** 域名ID */
|
||||
id: number
|
||||
/** 完整域名 */
|
||||
full_domain: string
|
||||
/** 域名类型 */
|
||||
domain_type: number
|
||||
/** 创建时间 */
|
||||
created_at: string
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加外部域名请求参数
|
||||
*/
|
||||
export interface AddExternalDomainRequest {
|
||||
/** 域名 */
|
||||
full_domain: string
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加外部域名响应数据
|
||||
*/
|
||||
export interface AddExternalDomainResponse {
|
||||
/** 域名ID */
|
||||
id: number
|
||||
/** 完整域名 */
|
||||
full_domain: string
|
||||
/** 域名类型 */
|
||||
domain_type: number
|
||||
/** 创建时间 */
|
||||
created_at: string
|
||||
/** 备注 */
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除外部域名请求参数
|
||||
*/
|
||||
export interface RemoveDomainRequest {
|
||||
/** 域名ID */
|
||||
domain_id: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除外部域名响应数据
|
||||
*/
|
||||
export interface RemoveDomainResponse {
|
||||
/** 删除结果 */
|
||||
success: boolean
|
||||
/** 消息 */
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测域名状态请求参数
|
||||
*/
|
||||
export interface CheckDomainStatusRequest {
|
||||
/** 域名ID */
|
||||
domain_id: number
|
||||
/** 域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
domain_type?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测域名状态响应数据
|
||||
*/
|
||||
export interface CheckDomainStatusResponse {
|
||||
/** 检测结果 */
|
||||
success: boolean
|
||||
/** 域名状态:0=未设置,1=已生效,2=未生效 */
|
||||
ns_status: number
|
||||
/** 消息 */
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 域名解析列表响应数据
|
||||
*/
|
||||
export interface ResolveListResponse {
|
||||
/** 数据列表 */
|
||||
data: ResolveItem[]
|
||||
/** 总数 */
|
||||
total: number
|
||||
}
|
||||
|
|
@ -325,7 +325,7 @@ export default defineComponent({
|
|||
</NButton>
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<div class="text-lg font-bold text-[#f0a020]">
|
||||
总计:¥{Number(cartListInfo.value.total_price || 0).toFixed(0)}
|
||||
总计:¥{cartListInfo.value.total_price}
|
||||
</div>
|
||||
<NButton type="success" onClick={handleCheckout}>
|
||||
结算
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ import {
|
|||
NButtonGroup,
|
||||
} from 'naive-ui'
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useMessage, useModal } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { updateDomainDnsServers, setDomainSecurity,fetchDomainAutoRenew,downloadDomainCertificate } from '@/api/domain'
|
||||
import { domainUtils } from '../config'
|
||||
|
||||
import type { DomainInfo } from '@/types/domain'
|
||||
import type { DomainInfo,PrivacyInfo } from '@/types/domain'
|
||||
import { useApp } from '@/components/layout/useStore'
|
||||
import { AddOutline, RemoveOutline } from '@vicons/ionicons5'
|
||||
import { useDomainDetailState } from '../useStore'
|
||||
|
||||
/**
|
||||
* 域名基本信息组件
|
||||
|
|
@ -41,6 +41,10 @@ export default defineComponent({
|
|||
type: Object as PropType<DomainInfo | null>,
|
||||
default: null,
|
||||
},
|
||||
privacyInfo: {
|
||||
type: Object as PropType<PrivacyInfo | null>,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
@ -55,6 +59,8 @@ export default defineComponent({
|
|||
const { handleError } = useError()
|
||||
const { isMobile } = useApp()
|
||||
|
||||
const { openPrivacyDialog } = useDomainDetailState()
|
||||
|
||||
// DNS服务器设置相关状态
|
||||
const showDnsModal = ref(false)
|
||||
const dnsForm = ref({
|
||||
|
|
@ -101,7 +107,7 @@ export default defineComponent({
|
|||
key,
|
||||
label: `NS服务器${i}`,
|
||||
value: String(dnsForm.value[key] || ''),
|
||||
required: i <= 2 // 前两个必填
|
||||
required: i <= 2, // 前两个必填
|
||||
})
|
||||
}
|
||||
return fields
|
||||
|
|
@ -112,12 +118,12 @@ export default defineComponent({
|
|||
return [
|
||||
{
|
||||
label: 'NS服务器1:',
|
||||
value: props.domainInfo?.ns1 || ''
|
||||
value: props.domainInfo?.ns1 || '',
|
||||
},
|
||||
{
|
||||
label: 'NS服务器2:',
|
||||
value: props.domainInfo?.ns2 || ''
|
||||
}
|
||||
value: props.domainInfo?.ns2 || '',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -126,11 +132,7 @@ export default defineComponent({
|
|||
<div>
|
||||
{getDefaultDnsFields().map((field, index) => (
|
||||
<NFormItem key={index} label={field.label}>
|
||||
<NInput
|
||||
value={field.value}
|
||||
readonly
|
||||
placeholder={field.value || '暂无'}
|
||||
/>
|
||||
<NInput value={field.value} readonly placeholder={field.value || '暂无'} />
|
||||
</NFormItem>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -180,12 +182,17 @@ export default defineComponent({
|
|||
ns6: props.domainInfo.ns6 || '',
|
||||
domain_id: props.domainInfo.id,
|
||||
}
|
||||
|
||||
|
||||
// 计算当前需要显示的字段数量
|
||||
const filledCount = [
|
||||
dnsForm.value.ns1, dnsForm.value.ns2, dnsForm.value.ns3,
|
||||
dnsForm.value.ns4, dnsForm.value.ns5, dnsForm.value.ns6
|
||||
].findLastIndex(v => v && v.trim()) + 1
|
||||
const filledCount =
|
||||
[
|
||||
dnsForm.value.ns1,
|
||||
dnsForm.value.ns2,
|
||||
dnsForm.value.ns3,
|
||||
dnsForm.value.ns4,
|
||||
dnsForm.value.ns5,
|
||||
dnsForm.value.ns6,
|
||||
].findLastIndex((v) => v && v.trim()) + 1
|
||||
visibleDnsCount.value = Math.max(filledCount, 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -193,7 +200,12 @@ export default defineComponent({
|
|||
// 打开DNS设置弹窗
|
||||
const openDnsModal = () => {
|
||||
if (props.domainInfo) {
|
||||
dnsMode.value = 'default' // 默认打开默认模式
|
||||
//如果ns数量大于2,则默认打开自定义模式
|
||||
if (props.domainInfo?.ns3) {
|
||||
switchToCustomMode()
|
||||
} else {
|
||||
dnsMode.value = 'default' // 默认打开默认模式
|
||||
}
|
||||
showDnsModal.value = true
|
||||
}
|
||||
}
|
||||
|
|
@ -205,20 +217,24 @@ export default defineComponent({
|
|||
message.error('NS服务器1和NS服务器2为必填项')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 顺序校验:不能跳跃填写
|
||||
const dnsValues = [
|
||||
dnsForm.value.ns1, dnsForm.value.ns2, dnsForm.value.ns3,
|
||||
dnsForm.value.ns4, dnsForm.value.ns5, dnsForm.value.ns6
|
||||
dnsForm.value.ns1,
|
||||
dnsForm.value.ns2,
|
||||
dnsForm.value.ns3,
|
||||
dnsForm.value.ns4,
|
||||
dnsForm.value.ns5,
|
||||
dnsForm.value.ns6,
|
||||
]
|
||||
|
||||
|
||||
for (let i = 0; i < dnsValues.length - 1; i++) {
|
||||
if (!dnsValues[i] && dnsValues[i + 1]) {
|
||||
message.error(`请按顺序填写DNS服务器,NS服务器${i + 1}为空但NS服务器${i + 2}有值`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// DNS格式校验
|
||||
const dnsRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/
|
||||
for (let i = 0; i < dnsValues.length; i++) {
|
||||
|
|
@ -273,17 +289,41 @@ export default defineComponent({
|
|||
}
|
||||
// 自动续费
|
||||
const toggleAutoRenew = async () => {
|
||||
const { fetch, message,data } = fetchDomainAutoRenew({
|
||||
const { fetch, message, data } = fetchDomainAutoRenew({
|
||||
domain_id: props.domainInfo!.id,
|
||||
status: props.domainInfo?.auto_renew === 1 ? 0 : 1,
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
|
||||
|
||||
if (data.value.status) {
|
||||
props.onRefresh?.()
|
||||
}
|
||||
}
|
||||
// 刷新数据
|
||||
function refreshFetch() {
|
||||
props.onRefresh?.()
|
||||
}
|
||||
/**
|
||||
* 打开域名隐私保护弹窗
|
||||
* @param row 域名数据
|
||||
*/
|
||||
function openCnDomainPrivacyModal(row: DomainInfo | null) {
|
||||
openPrivacyDialog.value = useModal({
|
||||
title: '.CN/.中国专属域名隐私保护',
|
||||
area: '520px',
|
||||
component: () => import('./PrivacyProtection'),
|
||||
componentProps: {
|
||||
domain: row,
|
||||
privacy: props.privacyInfo,
|
||||
refresh: refreshFetch, // 传递列表刷新函数
|
||||
onClose: () => {
|
||||
openPrivacyDialog.value?.close()
|
||||
},
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 切换安全设置
|
||||
const toggleSecurity = async (type: 'dns_lock' | 'transfer_lock' | 'update_lock', currentStatus: number) => {
|
||||
|
|
@ -362,7 +402,9 @@ export default defineComponent({
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const isPrivacy = computed(() => {
|
||||
return !!props.domainInfo?.privacy
|
||||
})
|
||||
return () => (
|
||||
<div class="py-2">
|
||||
<NGrid cols="1 m:2" xGap="16" yGap="16" responsive="screen">
|
||||
|
|
@ -416,6 +458,26 @@ export default defineComponent({
|
|||
{renderInfoItem('到期时间', formatDate(props.domainInfo?.expire_time))}
|
||||
{renderInfoItem('创建时间', formatDate(props.domainInfo?.created_at || 0))}
|
||||
{renderInfoItem('更新时间', formatDate(props.domainInfo?.updated_at || 0))}
|
||||
{(props.domainInfo?.suffix === 'cn' || props.domainInfo?.suffix === '中国') && (
|
||||
<div class="mb-2 flex items-center h-10 hover:bg-gray-100">
|
||||
<div class="text-gray-500 font-bold w-25 mr-5 ml-1">隐私保护</div>
|
||||
<div>
|
||||
<NTag type={isPrivacy.value ? 'success' : 'warning'} bordered={false}>
|
||||
{isPrivacy.value
|
||||
? `已开启(到期:${formatDate(props.privacyInfo?.end_time, 'yyyy-MM-dd')})`
|
||||
: '未开启'}
|
||||
</NTag>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
onClick={() => props.domainInfo && openCnDomainPrivacyModal(props.domainInfo)}
|
||||
>
|
||||
{isPrivacy.value ? '延续隐私保护' : '.CN/.中国专属域名隐私保护'}
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
|
||||
|
|
@ -424,10 +486,10 @@ export default defineComponent({
|
|||
<NCard title="DNS信息" header-style="font-size:16px;font-weight:500" class="h-[280px]">
|
||||
{renderInfoItem('NS服务器1', props.domainInfo?.ns1)}
|
||||
{renderInfoItem('NS服务器2', props.domainInfo?.ns2)}
|
||||
{renderInfoItem('DNS状态', null, 'tag', {
|
||||
{/* {renderInfoItem('DNS状态', null, 'tag', {
|
||||
type: props.domainInfo?.ns_status === 1 ? 'success' : 'warning',
|
||||
text: props.domainInfo?.ns_status === 1 ? '正常' : '异常',
|
||||
})}
|
||||
text: props.domainInfo?.ns_status === 1 ? '正常' : '未生效',
|
||||
})} */}
|
||||
<NButton size="small" ghost class="mt-4" onClick={openDnsModal} disabled={props.loading}>
|
||||
修改DNS服务器
|
||||
</NButton>
|
||||
|
|
|
|||
|
|
@ -808,20 +808,6 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 处理分析搜索
|
||||
* @param field 字段
|
||||
* @param recordId 记录ID
|
||||
* @returns
|
||||
*/
|
||||
const handleAnalysisSearch = () => {
|
||||
if (isAdding.value || isEditing.value !== '') {
|
||||
message.warning('正在编辑/添加记录,请先保存后搜索')
|
||||
return
|
||||
}
|
||||
fetchRecords()
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染展开行 - TSX 版本
|
||||
* @param rowData 行数据
|
||||
|
|
|
|||
|
|
@ -62,9 +62,6 @@ export function useDnsAnalysisController(props: DnsAnalysisControllerProps) {
|
|||
const editingRecord = ref<DnsRecordItem | null>();
|
||||
const isEditing = ref<string>("");
|
||||
const isAdding = ref<boolean>(false);
|
||||
|
||||
// dns解析弹窗
|
||||
const dnsAnalysisDialog = ref<boolean>(false);
|
||||
|
||||
// 新记录表单
|
||||
const newRecord = reactive<DnsRecordForm>({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* DNSSEC管理内容组件
|
||||
* 职责:管理DNSSEC记录,包括添加、删除、同步DS记录
|
||||
*/
|
||||
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NSpace,
|
||||
NText,
|
||||
NAlert,
|
||||
} from 'naive-ui'
|
||||
import { AddOutline, SyncOutline } from '@vicons/ionicons5'
|
||||
import { useController } from './useController'
|
||||
import { useDnssecManagementState } from './useStore'
|
||||
import type { DnssecManagementProps } from './types.d'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DnssecManagement',
|
||||
props: {
|
||||
domainId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
domainName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
onClose: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props: DnssecManagementProps) {
|
||||
// 获取store状态
|
||||
const store = useDnssecManagementState()
|
||||
|
||||
// 获取控制器
|
||||
const {
|
||||
loading,
|
||||
records,
|
||||
recordsCount,
|
||||
canAddMore,
|
||||
hasRecords,
|
||||
isMaxRecords,
|
||||
DnssecTable,
|
||||
handleAddRecord,
|
||||
handleDeleteRecord,
|
||||
handleSyncRecords,
|
||||
} = useController(props)
|
||||
|
||||
return () => (
|
||||
<div class="space-y-4">
|
||||
{/* 操作按钮 */}
|
||||
<div class="flex justify-between items-center">
|
||||
<NSpace>
|
||||
<NButton type="primary" onClick={handleAddRecord} disabled={!canAddMore} class="!px-4">
|
||||
添加DS记录
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
ghost
|
||||
onClick={async () => {
|
||||
await handleSyncRecords()
|
||||
}}
|
||||
disabled={store.syncLoading.value}
|
||||
loading={store.syncLoading.value}
|
||||
class="!px-4"
|
||||
>
|
||||
同步DS记录
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</div>
|
||||
|
||||
{/* DS记录表格 */}
|
||||
{hasRecords ? (
|
||||
<DnssecTable loading={loading.value} maxHeight="300px" scrollX="800px" class="border rounded" />
|
||||
) : (
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="text-lg mb-2">暂无DS记录</div>
|
||||
<div class="text-sm">点击"添加DS记录"按钮开始添加</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用说明 */}
|
||||
<NAlert>
|
||||
<div class="space-y-2">
|
||||
<div class="font-medium text-sm">DNSSEC功能使用说明:</div>
|
||||
<div class="text-sm space-y-1">
|
||||
<div>
|
||||
1.
|
||||
域名系统安全扩展(DNSSEC)是添加到域名的DNS域名系统确定源域名的可靠性数字签名,并有助于防止恶意活动缓存中毒、域欺骗和拦截中的攻击。
|
||||
</div>
|
||||
<div>2.我司DNS暂时不支持DNSSEC功能,若需要使用该功能,DNSSEC信息请在DNS服务商获取。</div>
|
||||
<div>3. 一个域名最多可添加8条DS记录。</div>
|
||||
</div>
|
||||
</div>
|
||||
</NAlert>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
83
frontend/apps/domain-management-backend/src/views/domain-details/components/DnssecMgt/types.d.ts
vendored
Normal file
83
frontend/apps/domain-management-backend/src/views/domain-details/components/DnssecMgt/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* DNSSEC管理组件类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* DNSSEC DS记录
|
||||
*/
|
||||
export interface DnssecRecord {
|
||||
/** 记录ID */
|
||||
id: number
|
||||
/** 密钥标签 */
|
||||
keyTag: number
|
||||
/** 加密算法 */
|
||||
algorithm: number
|
||||
/** 摘要类型 */
|
||||
digestType: number
|
||||
/** 摘要值 */
|
||||
digest: string
|
||||
/** 创建时间 */
|
||||
createdAt?: string
|
||||
/** 更新时间 */
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* DNSSEC管理组件Props
|
||||
*/
|
||||
export interface DnssecManagementProps {
|
||||
/** 域名ID */
|
||||
domainId: number
|
||||
/** 域名名称 */
|
||||
domainName: string
|
||||
/** 是否显示模态框 */
|
||||
visible: boolean
|
||||
/** 关闭回调 */
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加DS记录表单数据
|
||||
*/
|
||||
export interface AddDnssecRecordForm {
|
||||
/** 域名ID */
|
||||
domainId: number
|
||||
/** 密钥标签 */
|
||||
keyTag: number | null
|
||||
/** 加密算法 */
|
||||
algorithm: number
|
||||
/** 摘要类型 */
|
||||
digestType: number
|
||||
/** 摘要值 */
|
||||
digest: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步DS记录请求参数
|
||||
*/
|
||||
export interface SyncDnssecRecordsRequest {
|
||||
/** 域名ID */
|
||||
domainId: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除DS记录请求参数
|
||||
*/
|
||||
export interface DeleteDnssecRecordRequest {
|
||||
/** 记录ID */
|
||||
recordId: number
|
||||
/** 域名ID */
|
||||
domainId: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格列配置
|
||||
*/
|
||||
export interface DnssecTableColumn {
|
||||
title: string
|
||||
key: string
|
||||
width?: number
|
||||
align?: 'left' | 'center' | 'right'
|
||||
ellipsis?: { tooltip: boolean }
|
||||
render?: (row: DnssecRecord) => any
|
||||
}
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
/**
|
||||
* DNSSEC管理控制器
|
||||
* 职责:处理业务逻辑、事件响应和生命周期管理
|
||||
*/
|
||||
|
||||
import { ref, computed, watch, defineComponent } from 'vue'
|
||||
import { NButton, NIcon, NForm, NFormItem, NInput, NSelect, NGrid, NGridItem } from 'naive-ui'
|
||||
import { AddOutline, SyncOutline, TrashOutline } from '@vicons/ionicons5'
|
||||
import { useDnssecManagementState } from './useStore'
|
||||
import { useModal, useModalHooks, useMessage, useForm, useFormHooks, useTable, useLoadingMask } from '@baota/naive-ui/hooks'
|
||||
import { useDialog } from 'naive-ui'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import type { DnssecRecord, DnssecManagementProps, DnssecTableColumn } from './types.d'
|
||||
|
||||
/**
|
||||
* 添加DS记录模态框组件
|
||||
*/
|
||||
const createAddDnssecRecordModal = (store: any, domainId: number, onRefresh: () => Promise<void>) => defineComponent({
|
||||
name: 'AddDnssecRecordModal',
|
||||
setup() {
|
||||
const { close } = useModalHooks()
|
||||
const closeModal = close()
|
||||
const message = useMessage()
|
||||
const { handleError } = useError()
|
||||
const { useFormInput, useFormSelect } = useFormHooks()
|
||||
|
||||
const algorithmOptions = [
|
||||
{ label: 'RSA/MD5', value: 1 },
|
||||
{ label: 'Diffie-Hellman', value: 2 },
|
||||
{ label: 'DSA/SHA-1', value: 3 },
|
||||
{ label: 'RSA/SHA-1', value: 5 },
|
||||
{ label: 'DSA-NSEC3-SHA1', value: 6 },
|
||||
{ label: 'RSASHA1-NSEC3-SHA1', value: 7 },
|
||||
{ label: 'RSA/SHA-256', value: 8 },
|
||||
{ label: 'RSA/SHA-512', value: 10 },
|
||||
{ label: 'GOST R 34.10-2001', value: 12 },
|
||||
{ label: 'ECDSA Curve P-256 with SHA-256', value: 13 },
|
||||
{ label: 'ECDSA Curve P-384 with SHA-384', value: 14 },
|
||||
{ label: 'Ed25519', value: 15 },
|
||||
{ label: 'Ed448', value: 16 },
|
||||
{ label: 'Reserved for Indirect Keys', value: 252 },
|
||||
{ label: 'private algorithm', value: 253 },
|
||||
{ label: 'private algorithm OID', value: 254 },
|
||||
]
|
||||
|
||||
const digestTypeOptions = [
|
||||
{ label: 'SHA-1', value: 1 },
|
||||
{ label: 'SHA-256', value: 2 },
|
||||
{ label: 'GOST R 34.11-94', value: 3 },
|
||||
{ label: 'SHA-384', value: 4 },
|
||||
]
|
||||
|
||||
// 表单配置
|
||||
const formConfig = [
|
||||
useFormInput(
|
||||
'密钥标签',
|
||||
'keyTag',
|
||||
{
|
||||
placeholder: '请输入密钥标签 (0-65535)',
|
||||
clearable: true,
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
showFeedback: true,
|
||||
labelWidth: '80px',
|
||||
rule: {
|
||||
required: true,
|
||||
trigger: ['input', 'blur'],
|
||||
validator: (rule: any, value: any) => {
|
||||
if (!value || value === '') {
|
||||
return new Error('请输入密钥标签')
|
||||
}
|
||||
if (!/^\d+$/.test(value)) {
|
||||
return new Error('密钥标签格式错误')
|
||||
}
|
||||
const num = Number(value)
|
||||
if (isNaN(num) || !Number.isInteger(num)) {
|
||||
return new Error('密钥标签必须是整数')
|
||||
}
|
||||
if (num < 0 || num > 65535) {
|
||||
return new Error('密钥标签必须是0-65535之间的整数')
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
useFormSelect(
|
||||
'加密算法',
|
||||
'algorithm',
|
||||
algorithmOptions,
|
||||
{
|
||||
placeholder: '请选择加密算法',
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
showFeedback: true,
|
||||
labelWidth: '80px',
|
||||
},
|
||||
),
|
||||
useFormSelect(
|
||||
'摘要类型',
|
||||
'digestType',
|
||||
digestTypeOptions,
|
||||
{
|
||||
placeholder: '请选择摘要类型',
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
showFeedback: true,
|
||||
labelWidth: '80px',
|
||||
},
|
||||
),
|
||||
useFormInput(
|
||||
'摘要',
|
||||
'digest',
|
||||
{
|
||||
placeholder: '请输入摘要内容 (十六进制字符串)',
|
||||
type: 'textarea',
|
||||
autosize: { minRows: 3, maxRows: 6 },
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
showFeedback: true,
|
||||
labelWidth: '80px',
|
||||
rule: {
|
||||
required: true,
|
||||
message: '请填写摘要内容',
|
||||
trigger: ['blur'],
|
||||
},
|
||||
},
|
||||
),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => (
|
||||
<div class="flex justify-end gap-2 mt-6">
|
||||
<NButton onClick={closeModal}>取消</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
disabled={formLoading.value}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await submitForm()
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error)
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认添加
|
||||
</NButton>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
// 表单实例
|
||||
const { component: FormComponent, fetch: submitForm, loading: formLoading } = useForm({
|
||||
config: formConfig,
|
||||
defaultValue: {
|
||||
keyTag: '',
|
||||
algorithm: 8, // RSA/SHA-256
|
||||
digestType: 2, // SHA-256
|
||||
digest: '',
|
||||
},
|
||||
request: async (formData: any) => {
|
||||
try {
|
||||
const submitData = {
|
||||
...formData,
|
||||
keyTag: Number(formData.keyTag),
|
||||
domainId: domainId
|
||||
}
|
||||
const result = await store.addDnssecRecord(submitData)
|
||||
if (result.success) {
|
||||
await onRefresh()
|
||||
closeModal()
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return () => (
|
||||
<div class="max-w-2xl">
|
||||
<FormComponent />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* DNSSEC管理控制器
|
||||
*/
|
||||
export function useController(props: DnssecManagementProps) {
|
||||
const store = useDnssecManagementState()
|
||||
const { handleError } = useError()
|
||||
|
||||
const dialog = useDialog()
|
||||
|
||||
/**
|
||||
* 表格列配置
|
||||
*/
|
||||
const createColumns = [
|
||||
{
|
||||
title: '密钥标签',
|
||||
key: 'keyTag',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (row: DnssecRecord) => row.keyTag.toString(),
|
||||
},
|
||||
{
|
||||
title: '加密算法',
|
||||
key: 'algorithm',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
render: (row: DnssecRecord) => {
|
||||
const algorithmMap: Record<number, string> = {
|
||||
1: 'RSA/MD5',
|
||||
2: 'Diffie-Hellman',
|
||||
3: 'DSA/SHA-1',
|
||||
5: 'RSA/SHA-1',
|
||||
6: 'DSA-NSEC3-SHA1',
|
||||
7: 'RSASHA1-NSEC3-SHA1',
|
||||
8: 'RSA/SHA-256',
|
||||
10: 'RSA/SHA-512',
|
||||
12: 'GOST R 34.10-2001',
|
||||
13: 'ECDSA Curve P-256 with SHA-256',
|
||||
14: 'ECDSA Curve P-384 with SHA-384',
|
||||
15: 'Ed25519',
|
||||
16: 'Ed448',
|
||||
252: 'Reserved for Indirect Keys',
|
||||
253: 'private algorithm',
|
||||
254: 'private algorithm OID',
|
||||
}
|
||||
return algorithmMap[row.algorithm] || `算法 ${row.algorithm}`
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '摘要类型',
|
||||
key: 'digestType',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (row: DnssecRecord) => {
|
||||
const digestTypeMap: Record<number, string> = {
|
||||
1: 'SHA-1',
|
||||
2: 'SHA-256',
|
||||
3: 'GOST R 34.11-94',
|
||||
4: 'SHA-384',
|
||||
}
|
||||
return digestTypeMap[row.digestType] || `类型 ${row.digestType}`
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '摘要',
|
||||
key: 'digest',
|
||||
width: 300,
|
||||
ellipsis: { tooltip: true },
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (row: DnssecRecord) => (
|
||||
<NButton
|
||||
size="small"
|
||||
type="error"
|
||||
ghost
|
||||
class="!px-2"
|
||||
onClick={() => handleDeleteRecord(row.id)}
|
||||
>
|
||||
删除
|
||||
</NButton>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
// 表格实例
|
||||
const {
|
||||
TableComponent: DnssecTable,
|
||||
loading,
|
||||
fetch: fetchDnssecRecords,
|
||||
data: tableData,
|
||||
} = useTable({
|
||||
config: createColumns as any,
|
||||
request: async (params: any) => {
|
||||
const result = await store.fetchDnssecRecords({ domainId: params.domainId || props.domainId })
|
||||
return result as any
|
||||
},
|
||||
defaultValue: { domainId: props.domainId },
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const hasRecords = computed(() => {
|
||||
const data = tableData.value as any
|
||||
return data?.list?.length > 0 || data?.length > 0
|
||||
})
|
||||
const isMaxRecords = computed(() => {
|
||||
const data = tableData.value as any
|
||||
const length = data?.list?.length || data?.length || 0
|
||||
return length >= 8
|
||||
})
|
||||
const recordsCount = computed(() => {
|
||||
const data = tableData.value as any
|
||||
return data?.list?.length || data?.length || 0
|
||||
})
|
||||
|
||||
// 事件处理
|
||||
const handleAddRecord = () => {
|
||||
const AddDnssecRecordModalComponent = createAddDnssecRecordModal(store, props.domainId, fetchDnssecRecords)
|
||||
useModal({
|
||||
title: '添加DS记录',
|
||||
area: '600px',
|
||||
component: AddDnssecRecordModalComponent,
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteRecord = async (recordId: number) => {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这条DS记录吗?删除后无法恢复。',
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await store.deleteDnssecRecord(recordId)
|
||||
await fetchDnssecRecords()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleSyncRecords = async () => {
|
||||
try {
|
||||
await store.syncDnssecRecords({ domainId: props.domainId })
|
||||
await fetchDnssecRecords()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听模态框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
fetchDnssecRecords()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
records: tableData,
|
||||
recordsCount,
|
||||
canAddMore: computed(() => !isMaxRecords.value),
|
||||
hasRecords,
|
||||
isMaxRecords,
|
||||
|
||||
// 表格组件
|
||||
DnssecTable,
|
||||
|
||||
// 事件处理
|
||||
handleAddRecord,
|
||||
handleDeleteRecord,
|
||||
handleSyncRecords,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
* DNSSEC管理状态管理
|
||||
* 职责:管理DNSSEC记录的状态、数据获取和操作
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useMessage, useLoadingMask } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { addDnssecDsRecord, getDnssecDsList, deleteDnssecDsRecord, syncDnssecDsRecord } from '@/api/domain'
|
||||
import type { DnssecRecord, AddDnssecRecordForm, SyncDnssecRecordsRequest, DeleteDnssecRecordRequest } from './types.d'
|
||||
|
||||
/**
|
||||
* DNSSEC管理Store
|
||||
*/
|
||||
export const useDnssecManagementStore = defineStore('dnssecManagement', () => {
|
||||
const { handleError } = useError()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const syncLoading = ref(false)
|
||||
const records = ref<DnssecRecord[]>([])
|
||||
const currentDomainId = ref<number | null>(null)
|
||||
|
||||
// 计算属性
|
||||
const recordsCount = computed(() => records.value.length)
|
||||
const canAddMore = computed(() => recordsCount.value < 8) // 最多8条记录
|
||||
|
||||
/**
|
||||
* 设置当前域名ID
|
||||
*/
|
||||
const setCurrentDomainId = (domainId: number) => {
|
||||
currentDomainId.value = domainId
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取DNSSEC记录列表数据(供useTable使用)
|
||||
*/
|
||||
const fetchDnssecRecords = async (params: { domainId: number } = { domainId: 0 }) => {
|
||||
try {
|
||||
loading.value = true
|
||||
setCurrentDomainId(params.domainId)
|
||||
const { fetch, data } = getDnssecDsList({ domain_id: params.domainId.toString() })
|
||||
await fetch()
|
||||
if (data.value?.status) {
|
||||
const apiData = data.value.data || []
|
||||
const mappedData = apiData.map((item: any) => ({
|
||||
id: item.id,
|
||||
keyTag: item.key_tag,
|
||||
algorithm: item.alg,
|
||||
digestType: item.digest_type,
|
||||
digest: item.digest,
|
||||
}))
|
||||
records.value = mappedData
|
||||
return {
|
||||
list: mappedData,
|
||||
total: mappedData.length,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加DS记录
|
||||
*/
|
||||
const addDnssecRecord = async (formData: AddDnssecRecordForm): Promise<{ success: boolean }> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { fetch, data, message } = addDnssecDsRecord({
|
||||
domain_id: formData.domainId.toString(),
|
||||
key_tag: formData.keyTag!,
|
||||
alg: formData.algorithm,
|
||||
digest_type: formData.digestType,
|
||||
digest: formData.digest
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
if (data.value?.status) {
|
||||
return { success: true }
|
||||
} else {
|
||||
throw new Error(data.value?.msg || '添加DS记录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
return { success: false }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除DS记录
|
||||
*/
|
||||
const deleteDnssecRecord = async (recordId: number): Promise<void> => {
|
||||
const { open: openLoad, close: closeLoad } = useLoadingMask({ text: '正在删除DS记录,请稍后...', zIndex: 9999 })
|
||||
openLoad()
|
||||
try {
|
||||
const { fetch, data, message } = deleteDnssecDsRecord({ ds_id: recordId })
|
||||
message.value = true
|
||||
await fetch()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
closeLoad()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步DS记录
|
||||
*/
|
||||
const syncDnssecRecords = async (params: SyncDnssecRecordsRequest): Promise<void> => {
|
||||
try {
|
||||
syncLoading.value = true
|
||||
const { fetch, data, message } = syncDnssecDsRecord({ domain_id: params.domainId.toString() })
|
||||
message.value = true
|
||||
await fetch()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
syncLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空记录
|
||||
*/
|
||||
const clearRecords = () => {
|
||||
records.value = []
|
||||
currentDomainId.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
syncLoading,
|
||||
records,
|
||||
currentDomainId,
|
||||
|
||||
// 计算属性
|
||||
recordsCount,
|
||||
canAddMore,
|
||||
|
||||
// 方法
|
||||
setCurrentDomainId,
|
||||
fetchDnssecRecords,
|
||||
addDnssecRecord,
|
||||
deleteDnssecRecord,
|
||||
syncDnssecRecords,
|
||||
clearRecords,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 导出Store实例
|
||||
*/
|
||||
export const useDnssecManagementState = () => {
|
||||
const store = useDnssecManagementStore()
|
||||
const { loading, syncLoading, records, currentDomainId, recordsCount, canAddMore } = storeToRefs(store)
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
syncLoading,
|
||||
records,
|
||||
currentDomainId,
|
||||
|
||||
// 计算属性
|
||||
recordsCount,
|
||||
canAddMore,
|
||||
|
||||
// 方法
|
||||
setCurrentDomainId: store.setCurrentDomainId,
|
||||
fetchDnssecRecords: store.fetchDnssecRecords,
|
||||
addDnssecRecord: store.addDnssecRecord,
|
||||
deleteDnssecRecord: store.deleteDnssecRecord,
|
||||
syncDnssecRecords: store.syncDnssecRecords,
|
||||
clearRecords: store.clearRecords,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
/**
|
||||
* 域名隐私保护弹窗组件
|
||||
* 提供 .CN/中国专属域名隐私保护服务
|
||||
*/
|
||||
|
||||
import { defineComponent, computed, type PropType } from 'vue'
|
||||
import {
|
||||
NSteps,
|
||||
NStep,
|
||||
NAlert,
|
||||
NSpace,
|
||||
NButton,
|
||||
NSelect,
|
||||
NInput,
|
||||
NRadioGroup,
|
||||
NRadioButton,
|
||||
NQrCode,
|
||||
NFlex,
|
||||
NGrid,
|
||||
NGridItem,
|
||||
NTag
|
||||
} from 'naive-ui'
|
||||
import { usePrivacyProtectionController } from './useController'
|
||||
import { useRechargeState } from "@/views/recharge/useStore"
|
||||
import type { DomainInfo, PrivacyInfo } from '@/types/domain'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PrivacyProtectionModal',
|
||||
props: {
|
||||
domain: {
|
||||
type: Object as PropType<DomainInfo>,
|
||||
required: true,
|
||||
},
|
||||
privacy: {
|
||||
type: Object as PropType<PrivacyInfo>,
|
||||
default: null,
|
||||
},
|
||||
refresh: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
},
|
||||
onClose: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const recharge = useRechargeState()
|
||||
const {
|
||||
protectionForm,
|
||||
currentStep,
|
||||
orderInfo,
|
||||
priceLoading,
|
||||
priceError,
|
||||
paymentMethod,
|
||||
orderCreating,
|
||||
orderCreated,
|
||||
qrCodeUrl,
|
||||
handleNext,
|
||||
handleCancel,
|
||||
handleBalancePayment,
|
||||
switchPaymentMethod,
|
||||
} = usePrivacyProtectionController(props)
|
||||
|
||||
const canPayByBalance = computed(
|
||||
() => Number(recharge.overview.value?.balance || 0) >= Number(orderInfo.value?.data?.total_price || 0),
|
||||
)
|
||||
|
||||
return () => (
|
||||
<div class="privacy-protection-modal">
|
||||
{/* 步骤指示器 */}
|
||||
<NSteps current={currentStep.value} class="mb-6">
|
||||
<NStep title="确认保护信息" />
|
||||
<NStep title="选择支付方式" />
|
||||
</NSteps>
|
||||
|
||||
{/* 步骤1:确认保护信息 */}
|
||||
{currentStep.value === 1 && (
|
||||
<div class="step-content">
|
||||
<NAlert type="info" class="mb-6">
|
||||
.CN/.中国专属域名隐私保护服务可以保护个人信息不被公开查询
|
||||
</NAlert>
|
||||
|
||||
<div class="form-container space-y-4 mb-8">
|
||||
{/* 域名 */}
|
||||
<div class="form-item flex items-center">
|
||||
<label class="form-label w-24 text-gray-700">域名</label>
|
||||
<span class="form-value text-gray-900 font-medium">{protectionForm.value.domain}</span>
|
||||
</div>
|
||||
|
||||
{/* 隐私保护时长 */}
|
||||
<div class="form-item flex items-center">
|
||||
<label class="form-label w-24 text-gray-700">隐私保护时长</label>
|
||||
<div class="form-control">
|
||||
<NSelect
|
||||
v-model:value={protectionForm.value.protectionTime}
|
||||
style={{ width: '120px' }}
|
||||
options={[
|
||||
{ label: '1年', value: 1 },
|
||||
{ label: '2年', value: 2 },
|
||||
{ label: '3年', value: 3 },
|
||||
{ label: '5年', value: 5 },
|
||||
{ label: '10年', value: 10 },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 联系邮箱 */}
|
||||
<div class="form-item flex items-center">
|
||||
<label class="form-label w-24 text-gray-700">联系邮箱</label>
|
||||
<div class="form-control">
|
||||
<NInput
|
||||
v-model:value={protectionForm.value.contactEmail}
|
||||
placeholder="请输入联系邮箱(可选)"
|
||||
style={{ width: '320px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 保护价格 */}
|
||||
<div class="form-item flex items-center">
|
||||
<label class="form-label w-24 text-gray-700">保护价格</label>
|
||||
<span class="form-value text-red-500 font-bold text-xl">
|
||||
{priceLoading.value
|
||||
? '--'
|
||||
: priceError.value
|
||||
? '--'
|
||||
: protectionForm.value.price > 0
|
||||
? `¥${protectionForm.value.price}`
|
||||
: '--'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 只在步骤一显示底部操作按钮 */}
|
||||
<div class="flex justify-end">
|
||||
<NSpace>
|
||||
<NButton onClick={handleCancel}>取消</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
onClick={handleNext}
|
||||
disabled={priceLoading.value || orderCreating.value}
|
||||
loading={orderCreating.value}
|
||||
>
|
||||
{orderCreating.value ? '正在创建订单...' : '下一步'}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 步骤2:选择支付方式 */}
|
||||
{currentStep.value === 2 && (
|
||||
<div class="step-content">
|
||||
<NGrid cols="1" xGap={12} yGap={20}>
|
||||
{/* 订单确认信息 */}
|
||||
<NGridItem>
|
||||
<div class="order-summary mb-6">
|
||||
<div class="order-info-list space-y-3">
|
||||
<div class="order-item flex justify-between items-center">
|
||||
<span class="order-label text-gray-700">域名:</span>
|
||||
<span class="order-value text-gray-900 font-medium">{protectionForm.value.domain}</span>
|
||||
</div>
|
||||
<div class="order-item flex justify-between items-center">
|
||||
<span class="order-label text-gray-700">保护时长:</span>
|
||||
<span class="order-value text-gray-900">{protectionForm.value.protectionTime}年</span>
|
||||
</div>
|
||||
<div class="order-item flex justify-between items-center">
|
||||
<span class="order-label text-gray-700">联系邮箱:</span>
|
||||
<span class="order-value text-gray-900">{protectionForm.value.contactEmail || '未填写'}</span>
|
||||
</div>
|
||||
<div class="order-item flex justify-between items-center">
|
||||
<span class="order-label text-gray-700">保护费用:</span>
|
||||
<div class="flex items-center">
|
||||
{/* 原价 */}
|
||||
<span class="text-gray-500 text-sm mr-2" style={{ textDecoration: 'line-through' }}>原价:¥{orderInfo.value?.data?.original_price}</span>
|
||||
<span class="order-value text-red-500 font-bold text-lg">
|
||||
¥
|
||||
{orderCreated.value && orderInfo.value?.data?.total_price
|
||||
? orderInfo.value.data.total_price
|
||||
: protectionForm.value.price}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NGridItem>
|
||||
|
||||
{/* 支付方式选择 */}
|
||||
<NGridItem class="flex items-center">
|
||||
<strong class="text-sm mr-4">选择支付方式</strong>
|
||||
<NRadioGroup value={paymentMethod.value} size="small" onUpdateValue={switchPaymentMethod}>
|
||||
<NRadioButton value="wechat">微信支付</NRadioButton>
|
||||
<NRadioButton value="alipay">支付宝</NRadioButton>
|
||||
<NRadioButton value="balance">余额支付</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NGridItem>
|
||||
|
||||
{/* 支付内容展示 */}
|
||||
<NGridItem>
|
||||
{orderCreating.value ? (
|
||||
<NFlex vertical align="center" class="py-8">
|
||||
<div class="text-gray-600">正在创建订单...</div>
|
||||
</NFlex>
|
||||
) : !orderCreated.value ? (
|
||||
<NFlex vertical align="center" class="py-8">
|
||||
<div class="text-red-500">订单创建失败,请重试</div>
|
||||
</NFlex>
|
||||
) : paymentMethod.value !== 'balance' ? (
|
||||
<NFlex vertical align="center">
|
||||
<NQrCode value={qrCodeUrl.value || ''} size={180} />
|
||||
<div class="text-xs text-gray-600">
|
||||
请使用{paymentMethod.value === 'wechat' ? '微信' : '支付宝'}扫码支付
|
||||
</div>
|
||||
</NFlex>
|
||||
) : (
|
||||
<NFlex vertical align="center" class="py-6">
|
||||
<NTag type={canPayByBalance.value ? 'success' : 'warning'} bordered={false}>
|
||||
可用余额:¥{Number(recharge.overview.value?.balance || 0).toFixed(2)} / 需支付:¥
|
||||
{Number(orderInfo.value?.data?.total_price || protectionForm.value.price).toFixed(2)}
|
||||
</NTag>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="large"
|
||||
disabled={!canPayByBalance.value}
|
||||
onClick={handleBalancePayment}
|
||||
class="mt-4"
|
||||
>
|
||||
立即支付
|
||||
</NButton>
|
||||
</NFlex>
|
||||
)}
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
|
||||
{/* 步骤二没有底部操作按钮 */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* 隐私保护弹窗相关类型定义
|
||||
*/
|
||||
|
||||
import type { DomainItem } from '@/types/domain'
|
||||
|
||||
/**
|
||||
* 隐私保护弹窗组件属性
|
||||
*/
|
||||
export interface PrivacyProtectionModalProps {
|
||||
/** 域名信息 */
|
||||
domain: DomainItem
|
||||
/** 刷新回调函数 */
|
||||
refresh: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐私保护表单数据
|
||||
*/
|
||||
export interface PrivacyProtectionFormData {
|
||||
/** 域名 */
|
||||
domain: string
|
||||
/** 保护时长(年) */
|
||||
protectionTime: number
|
||||
/** 联系邮箱 */
|
||||
contactEmail: string
|
||||
/** 保护价格 */
|
||||
price: number
|
||||
/** 支付方式 */
|
||||
paymentMethod: 'wechat' | 'alipay' | 'balance'
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐私保护步骤类型
|
||||
*/
|
||||
export type PrivacyProtectionStep = 1 | 2
|
||||
|
||||
/**
|
||||
* 支付方式选项
|
||||
*/
|
||||
export interface PaymentMethodOption {
|
||||
label: string
|
||||
value: 'wechat' | 'alipay' | 'balance'
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 保护时长选项
|
||||
*/
|
||||
export interface ProtectionTimeOption {
|
||||
label: string
|
||||
value: number
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
/**
|
||||
* 隐私保护弹窗控制器
|
||||
* 负责处理业务逻辑、事件响应和状态管理
|
||||
*/
|
||||
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { usePrivacyProtectionState } from './useStore'
|
||||
import { buyByBalance } from '@/api/order'
|
||||
import type { DomainInfo, PrivacyInfo } from '@/types/domain'
|
||||
import { useRechargeController } from '@/views/recharge/useController'
|
||||
|
||||
interface PrivacyProtectionProps {
|
||||
domain: DomainInfo
|
||||
privacy: PrivacyInfo
|
||||
refresh: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐私保护弹窗控制器
|
||||
*/
|
||||
export function usePrivacyProtectionController(props: PrivacyProtectionProps) {
|
||||
const message = useMessage()
|
||||
const { loadAccountBalance } = useRechargeController()
|
||||
|
||||
// 获取状态管理
|
||||
const {
|
||||
currentProps,
|
||||
protectionForm,
|
||||
currentStep,
|
||||
orderLoading,
|
||||
orderInfo,
|
||||
priceLoading,
|
||||
priceError,
|
||||
paymentMethod,
|
||||
orderCreating,
|
||||
orderCreated,
|
||||
qrCodeUrl,
|
||||
isPolling,
|
||||
paymentSuccess,
|
||||
queryPrivacyPrice,
|
||||
createPrivacyOrder,
|
||||
switchPaymentMethod,
|
||||
startPaymentPolling,
|
||||
stopPaymentPolling,
|
||||
resetForm,
|
||||
} = usePrivacyProtectionState()
|
||||
|
||||
// 1: 新购 2: 续费
|
||||
const privacyQueryType = ref(1)
|
||||
|
||||
// 初始化表单数据
|
||||
onMounted(async () => {
|
||||
protectionForm.value.domain = props.domain.full_domain
|
||||
protectionForm.value.contactEmail = props.privacy?.email || ''
|
||||
currentProps.value = props.domain
|
||||
privacyQueryType.value = props.privacy === null ? 1 : 2
|
||||
// 查询默认价格(1年,新购)
|
||||
await queryPrivacyPrice(privacyQueryType.value, 1)
|
||||
})
|
||||
|
||||
// 组件卸载时重置表单和停止轮询
|
||||
onUnmounted(() => {
|
||||
stopPaymentPolling()
|
||||
resetForm()
|
||||
})
|
||||
|
||||
// 监听保护时长变化,重新查询价格
|
||||
watch(() => protectionForm.value.protectionTime, async (newYear) => {
|
||||
if (newYear) {
|
||||
await queryPrivacyPrice(privacyQueryType.value, newYear) // 1表示新购
|
||||
}
|
||||
})
|
||||
|
||||
// 监听步骤和支付方式变化,控制轮询
|
||||
watch([currentStep, paymentMethod, orderCreated], () => {
|
||||
const orderNo = orderInfo.value?.data?.order_no
|
||||
|
||||
if (currentStep.value === 2 && orderCreated.value && orderNo) {
|
||||
if (paymentMethod.value !== 'balance') {
|
||||
// 扫码支付时启动轮询
|
||||
startPaymentPolling(orderNo)
|
||||
} else {
|
||||
// 余额支付时停止轮询
|
||||
stopPaymentPolling()
|
||||
}
|
||||
} else {
|
||||
// 不在支付步骤时停止轮询
|
||||
stopPaymentPolling()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听支付成功状态变化
|
||||
watch(paymentSuccess, (success) => {
|
||||
if (success && currentStep.value === 2) {
|
||||
message.success('支付成功!隐私保护已开启')
|
||||
props.refresh?.() // 刷新域名信息
|
||||
props.onClose?.() // 关闭弹窗
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 下一步操作
|
||||
*/
|
||||
const handleNext = async () => {
|
||||
// 邮箱不为空,检查是否符合邮箱格式
|
||||
if (protectionForm.value.contactEmail) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(protectionForm.value.contactEmail)) {
|
||||
message.error('邮箱格式不正确')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取账户余额
|
||||
await loadAccountBalance()
|
||||
|
||||
// 创建隐私保护订单
|
||||
await createPrivacyOrder()
|
||||
|
||||
// 订单创建成功后,切换到步骤二
|
||||
currentStep.value = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 上一步操作
|
||||
*/
|
||||
const handleBack = () => {
|
||||
currentStep.value = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消操作
|
||||
*/
|
||||
const handleCancel = () => {
|
||||
resetForm()
|
||||
// 触发关闭弹窗
|
||||
props.onClose?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换支付方式并控制轮询
|
||||
*/
|
||||
const handleSwitchPaymentMethod = (method: 'wechat' | 'alipay' | 'balance') => {
|
||||
switchPaymentMethod(method)
|
||||
|
||||
const orderNo = orderInfo.value?.data?.order_no
|
||||
if (orderNo && currentStep.value === 2 && orderCreated.value) {
|
||||
if (method === 'balance') {
|
||||
stopPaymentPolling()
|
||||
} else if (!isPolling.value) {
|
||||
startPaymentPolling(orderNo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 余额支付处理
|
||||
*/
|
||||
const handleBalancePayment = async () => {
|
||||
const orderNo = orderInfo.value?.data?.order_no
|
||||
if (!orderNo) {
|
||||
message.error('订单号不存在')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 这里调用余额支付API
|
||||
// 参考续费模块的 payRenewByBalance 实现
|
||||
const { fetch, data } = await buyByBalance({ order_no: orderNo })
|
||||
await fetch()
|
||||
const response = data.value
|
||||
// 检查支付结果
|
||||
if (response?.status) {
|
||||
paymentSuccess.value = true
|
||||
} else {
|
||||
message.error(response?.msg || '支付失败,请重试')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
message.error('支付失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
protectionForm,
|
||||
currentStep,
|
||||
orderLoading,
|
||||
orderInfo,
|
||||
priceLoading,
|
||||
priceError,
|
||||
paymentMethod,
|
||||
orderCreating,
|
||||
orderCreated,
|
||||
qrCodeUrl,
|
||||
isPolling,
|
||||
|
||||
// 方法
|
||||
handleNext,
|
||||
handleBack,
|
||||
handleCancel,
|
||||
handleBalancePayment,
|
||||
switchPaymentMethod: handleSwitchPaymentMethod,
|
||||
resetForm,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
/**
|
||||
* 隐私保护弹窗状态管理
|
||||
* 负责管理表单数据、步骤状态和API调用
|
||||
*/
|
||||
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { privacyOrder, privacyPrice } from '@/api/domain'
|
||||
import { queryPaymentStatus } from '@/api/order'
|
||||
import type { PrivacyRequest, PrivacyPriceRequest } from '@/types/domain'
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import type { DomainInfo } from '@/types/domain'
|
||||
|
||||
const { handleError } = useError()
|
||||
|
||||
/** 轮询定时器 */
|
||||
let paymentPollTimer: any = null
|
||||
|
||||
/**
|
||||
* 隐私保护表单数据接口
|
||||
*/
|
||||
interface PrivacyProtectionForm {
|
||||
/** 域名 */
|
||||
domain: string
|
||||
/** 保护时长(年) */
|
||||
protectionTime: number
|
||||
/** 联系邮箱 */
|
||||
contactEmail: string
|
||||
/** 保护价格 */
|
||||
price: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐私保护状态Store
|
||||
*/
|
||||
export const usePrivacyProtectionStore = defineStore('privacy-protection-store', () => {
|
||||
// -------------------- 状态定义 --------------------
|
||||
|
||||
/** 表单数据 */
|
||||
const protectionForm = reactive<PrivacyProtectionForm>({
|
||||
domain: '',
|
||||
protectionTime: 1,
|
||||
contactEmail: '',
|
||||
price: 0,
|
||||
})
|
||||
/** 当前row */
|
||||
const currentProps = ref<DomainInfo | null>(null)
|
||||
/** 当前步骤 */
|
||||
const currentStep = ref<1 | 2>(1)
|
||||
|
||||
/** 订单状态 */
|
||||
const orderLoading = ref(false)
|
||||
const orderInfo = ref<ApiResponse | null>(null)
|
||||
|
||||
/** 价格查询状态 */
|
||||
const priceLoading = ref(false)
|
||||
const priceError = ref(false)
|
||||
|
||||
/** 支付方式 */
|
||||
const paymentMethod = ref<'wechat' | 'alipay' | 'balance'>('wechat')
|
||||
|
||||
|
||||
/** 订单创建状态 */
|
||||
const orderCreating = ref(false)
|
||||
const orderCreated = ref(false)
|
||||
|
||||
/** 轮询状态 */
|
||||
const isPolling = ref(false)
|
||||
|
||||
/** 支付成功状态 */
|
||||
const paymentSuccess = ref(false)
|
||||
|
||||
// -------------------- 计算属性 --------------------
|
||||
|
||||
/** 获取二维码链接 */
|
||||
const qrCodeUrl = computed(() => {
|
||||
if (!orderInfo.value?.data) return ''
|
||||
return paymentMethod.value === 'wechat'
|
||||
? orderInfo.value.data.wx
|
||||
: orderInfo.value.data.ali
|
||||
})
|
||||
|
||||
// -------------------- 方法定义 --------------------
|
||||
|
||||
/**
|
||||
* 查询隐私保护价格
|
||||
*/
|
||||
const queryPrivacyPrice = async (type: number = 1, year: number = 1): Promise<void> => {
|
||||
try {
|
||||
priceLoading.value = true
|
||||
priceError.value = false
|
||||
|
||||
const params: PrivacyPriceRequest = {
|
||||
type, // 1:新购 2:续费
|
||||
year,
|
||||
}
|
||||
|
||||
const { fetch, data } = privacyPrice(params)
|
||||
await fetch()
|
||||
|
||||
// 假设 API 返回的数据结构中包含价格信息
|
||||
const responseData = data.value?.data
|
||||
if (responseData && typeof responseData.price === 'number') {
|
||||
protectionForm.price = responseData.price
|
||||
} else {
|
||||
// 如果没有返回价格或格式不正确,设置错误状态
|
||||
priceError.value = true
|
||||
protectionForm.price = 0
|
||||
}
|
||||
} catch (error) {
|
||||
priceError.value = true
|
||||
protectionForm.price = 0
|
||||
handleError(error)
|
||||
} finally {
|
||||
priceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建隐私保护订单
|
||||
*/
|
||||
const createPrivacyOrder = async (): Promise<void> => {
|
||||
try {
|
||||
orderCreating.value = true
|
||||
orderCreated.value = false
|
||||
console.log(currentProps.value, '--')
|
||||
const params: PrivacyRequest = {
|
||||
type: currentProps.value?.privacy === 1? 2:1, // 1:新购 2:续费
|
||||
domain: protectionForm.domain,
|
||||
year: protectionForm.protectionTime,
|
||||
email: protectionForm.contactEmail || undefined,
|
||||
}
|
||||
|
||||
const { fetch, data } = privacyOrder(params)
|
||||
await fetch()
|
||||
// 检查API返回状态
|
||||
if (data.value?.status === false) {
|
||||
// 如果状态为false,抛出包含具体错误信息的错误
|
||||
const errorMsg = data.value?.msg || '订单创建失败'
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
orderInfo.value = data.value
|
||||
orderCreated.value = true
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
throw error
|
||||
} finally {
|
||||
orderCreating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换支付方式
|
||||
*/
|
||||
const switchPaymentMethod = (method: 'wechat' | 'alipay' | 'balance') => {
|
||||
paymentMethod.value = method
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单状态
|
||||
*/
|
||||
const resetForm = () => {
|
||||
currentStep.value = 1
|
||||
orderInfo.value = null
|
||||
priceError.value = false
|
||||
priceLoading.value = false
|
||||
orderCreating.value = false
|
||||
orderCreated.value = false
|
||||
paymentMethod.value = 'wechat'
|
||||
paymentSuccess.value = false
|
||||
stopPaymentPolling()
|
||||
Object.assign(protectionForm, {
|
||||
domain: '',
|
||||
protectionTime: 1,
|
||||
contactEmail: '',
|
||||
price: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据保护时长计算价格
|
||||
* @param years 保护年数
|
||||
*/
|
||||
const calculatePrice = (years: number): number => {
|
||||
// 这里可以根据实际的定价策略来计算
|
||||
// 暂时使用固定价格 30元/年
|
||||
return years * 30
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新保护时长并重新计算价格
|
||||
* @param years 保护年数
|
||||
*/
|
||||
const updateProtectionTime = (years: number) => {
|
||||
protectionForm.protectionTime = years
|
||||
protectionForm.price = calculatePrice(years)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询隐私保护订单支付状态
|
||||
*/
|
||||
const queryPrivacyPaymentStatus = async (orderNo: string): Promise<boolean> => {
|
||||
try {
|
||||
const { fetch, data } = queryPaymentStatus({ order_no: orderNo })
|
||||
await fetch()
|
||||
return Number(data.value?.data?.status ?? 0) === 1
|
||||
} catch (error) {
|
||||
console.error('查询支付状态失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动支付轮询
|
||||
*/
|
||||
const startPaymentPolling = (orderNo: string) => {
|
||||
if (!orderNo || isPolling.value) return
|
||||
|
||||
stopPaymentPolling()
|
||||
isPolling.value = true
|
||||
|
||||
let count = 0
|
||||
const maxCount = 60 // 最多轮询60次(3分钟)
|
||||
|
||||
paymentPollTimer = setInterval(async () => {
|
||||
count++
|
||||
try {
|
||||
const isPaid = await queryPrivacyPaymentStatus(orderNo)
|
||||
if (isPaid) {
|
||||
paymentSuccess.value = true
|
||||
stopPaymentPolling()
|
||||
return
|
||||
}
|
||||
|
||||
if (count >= maxCount) {
|
||||
stopPaymentPolling()
|
||||
}
|
||||
} catch (error) {
|
||||
if (count >= maxCount) {
|
||||
stopPaymentPolling()
|
||||
}
|
||||
}
|
||||
}, 3000) // 每3秒查询一次
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止支付轮询
|
||||
*/
|
||||
const stopPaymentPolling = () => {
|
||||
if (paymentPollTimer) {
|
||||
clearInterval(paymentPollTimer)
|
||||
paymentPollTimer = null
|
||||
}
|
||||
isPolling.value = false
|
||||
}
|
||||
|
||||
// 返回状态和方法
|
||||
return {
|
||||
// 状态
|
||||
protectionForm,
|
||||
currentStep,
|
||||
orderLoading,
|
||||
orderInfo,
|
||||
priceLoading,
|
||||
priceError,
|
||||
paymentMethod,
|
||||
orderCreating,
|
||||
orderCreated,
|
||||
isPolling,
|
||||
paymentSuccess,
|
||||
currentProps,
|
||||
|
||||
// 计算属性
|
||||
qrCodeUrl,
|
||||
|
||||
// 方法
|
||||
queryPrivacyPrice,
|
||||
createPrivacyOrder,
|
||||
switchPaymentMethod,
|
||||
resetForm,
|
||||
calculatePrice,
|
||||
updateProtectionTime,
|
||||
startPaymentPolling,
|
||||
stopPaymentPolling,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 导出Store实例
|
||||
*/
|
||||
export const usePrivacyProtectionState = () => {
|
||||
const store = usePrivacyProtectionStore()
|
||||
return {
|
||||
...store,
|
||||
...storeToRefs(store),
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
*/
|
||||
|
||||
import { defineComponent, ref, computed } from 'vue'
|
||||
import { NCard, NGrid, NGridItem, NText, NSkeleton, NAlert, NIcon, NFlex } from 'naive-ui'
|
||||
import { CheckCircle, AlertTriangle, XCircle } from 'lucide-vue-next'
|
||||
import { NCard, NGrid, NGridItem, NText, NSkeleton, NAlert, NIcon, NFlex, NButton, NDivider } from 'naive-ui'
|
||||
import { CheckCircle, AlertTriangle, XCircle, RefreshCw } from 'lucide-vue-next'
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
import { useController } from '../useController'
|
||||
|
||||
|
|
@ -53,7 +53,13 @@ export default defineComponent({
|
|||
setup(props) {
|
||||
// 显示警告提示
|
||||
const showAlert = ref(true)
|
||||
const { domainInfo, realNameInfo, loading } = useController(props.domainId)
|
||||
const { domainInfo, realNameInfo, loading, realNameInfoUpdating, openTemplateChangeModal, refreshDomainInfo } = useController(props.domainId)
|
||||
|
||||
// 计算加载状态
|
||||
const isRealNameUpdating = computed(() => {
|
||||
return realNameInfoUpdating.value !== null &&
|
||||
typeof realNameInfoUpdating.value === 'object';
|
||||
})
|
||||
|
||||
// 计算实名状态配置
|
||||
const realNameStatus = computed(() => {
|
||||
|
|
@ -133,6 +139,39 @@ export default defineComponent({
|
|||
{renderInfoItem('联系人', realNameInfo.value?.contact_person)}
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
<NDivider />
|
||||
<div class="flex items-center">
|
||||
<NButton
|
||||
type="primary"
|
||||
loading={isRealNameUpdating.value}
|
||||
disabled={isRealNameUpdating.value}
|
||||
onClick={openTemplateChangeModal}
|
||||
>
|
||||
更换实名模板
|
||||
</NButton>
|
||||
|
||||
{isRealNameUpdating.value && (
|
||||
<>
|
||||
<div class="text-[#f0a020] text-sm ml-4">处理时间约为1-30分钟,请耐心等待</div>
|
||||
<NButton
|
||||
text
|
||||
size="small"
|
||||
class="ml-3"
|
||||
loading={loading.value}
|
||||
onClick={refreshDomainInfo}
|
||||
v-slots={{
|
||||
icon: () => (
|
||||
<NIcon size={16}>
|
||||
<RefreshCw />
|
||||
</NIcon>
|
||||
),
|
||||
}}
|
||||
>
|
||||
刷新状态
|
||||
</NButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
{/* 认证状态说明 */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,268 @@
|
|||
/**
|
||||
* 实名模板更换弹窗组件
|
||||
* 负责展示当前模板信息和提供新模板选择功能
|
||||
*/
|
||||
|
||||
import { defineComponent, ref, computed, type PropType } from 'vue';
|
||||
import { NAlert, NCard, NSelect, NButton, NFlex, NGrid, NGridItem, NSpin, NTag, NIcon } from 'naive-ui'
|
||||
import { useDomainDetailState } from '../useStore';
|
||||
import { useMessage } from '@baota/naive-ui/hooks';
|
||||
import type { DomainInfo, RealNameInfo } from '@/types/domain';
|
||||
import type { ContactTemplateItem } from '@/types/real-name';
|
||||
import { updateDomainRealName } from '@/api/domain'
|
||||
import { maskUtils } from '@/views/real-name/useStore'
|
||||
import { useModal } from '@baota/naive-ui/hooks'
|
||||
import DomainRegistrationForm from '@/views/real-name/components/DomainRegistrationForm/index'
|
||||
import { RefreshFilled } from '@vicons/material'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RealNameTemplateChangeDialog',
|
||||
props: {
|
||||
domainId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
domainInfo: {
|
||||
type: Object as PropType<DomainInfo | null>,
|
||||
default: null
|
||||
},
|
||||
currentTemplate: {
|
||||
type: Object as PropType<RealNameInfo | null>,
|
||||
default: null
|
||||
},
|
||||
refresh: {
|
||||
type: Function as PropType<() => Promise<void>>,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const { realNameTemplates, realNameTemplatesLoading, openTemplateChangeDialog, fetchRealNameTemplateList } =
|
||||
useDomainDetailState()
|
||||
|
||||
const message = useMessage()
|
||||
const selectedTemplateId = ref<string>('')
|
||||
const submitting = ref(false)
|
||||
|
||||
// 模板选项(排除当前模板)
|
||||
const templateOptions = computed(() => {
|
||||
return realNameTemplates.value
|
||||
.filter((template) => template.registrant_id !== props.currentTemplate?.registrant_id)
|
||||
.map((template) => ({
|
||||
label: `${template.template_name || template.owner_name}(${template.type === 1 ? '个人实名认证' : '企业实名认证'})`,
|
||||
value: template.registrant_id,
|
||||
}))
|
||||
})
|
||||
|
||||
// 获取选中的模板详情
|
||||
const selectedTemplateDetail = computed(() => {
|
||||
if (!selectedTemplateId.value) return null
|
||||
return realNameTemplates.value.find((template) => template.registrant_id === selectedTemplateId.value)
|
||||
})
|
||||
|
||||
// 选中实名模板
|
||||
const handleSelectRealName = (val: string) => {
|
||||
if (val === '-1') openCreateRealNameModal()
|
||||
else selectedTemplateId.value = val
|
||||
}
|
||||
/** 创建实名模板窗口(步骤一入口中的快捷按钮) */
|
||||
const openCreateRealNameModal = () => {
|
||||
const modal = useModal({
|
||||
title: '创建实名模板',
|
||||
area: '1000px',
|
||||
component: DomainRegistrationForm,
|
||||
componentProps: {
|
||||
mode: 'add',
|
||||
refresh: async () => {
|
||||
await fetchRealNameTemplateList()
|
||||
},
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
return modal
|
||||
}
|
||||
|
||||
// 提交更换
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedTemplateId.value) {
|
||||
message.warning('请选择新的实名模板')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
submitting.value = true
|
||||
const { fetch, message, data } = updateDomainRealName({
|
||||
domain_id: props.domainId,
|
||||
new_registrant_id: selectedTemplateId.value,
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
if (data.value.status) {
|
||||
await props.refresh()
|
||||
openTemplateChangeDialog.value?.close()
|
||||
}
|
||||
} catch (error) {
|
||||
// 错误已在 store 中处理
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
openTemplateChangeDialog.value?.close()
|
||||
}
|
||||
|
||||
// 渲染模板详细信息的通用函数
|
||||
const renderTemplateDetail = (template: RealNameInfo | ContactTemplateItem | null, title: string) => {
|
||||
if (!template) return null
|
||||
|
||||
// 统一数据格式处理
|
||||
const templateData = {
|
||||
id: (template as ContactTemplateItem).id || (template as RealNameInfo).registrant_id || '-',
|
||||
name:
|
||||
(template as ContactTemplateItem).template_name ||
|
||||
(template as ContactTemplateItem).owner_name ||
|
||||
(template as RealNameInfo).owner_name ||
|
||||
'-',
|
||||
type: template.type === 1 ? '个人实名认证' : '企业实名认证',
|
||||
idNumber: (template as ContactTemplateItem).id_number || (template as RealNameInfo).id_number || '-',
|
||||
owner: (template as ContactTemplateItem).owner_name || (template as RealNameInfo).owner_name || '-',
|
||||
email: template.email || '-',
|
||||
phone: (template as ContactTemplateItem).phone || (template as RealNameInfo).contact_person || '-',
|
||||
status:
|
||||
(template as ContactTemplateItem).status === 2
|
||||
? '已审核'
|
||||
: (template as RealNameInfo).status === 2
|
||||
? '已通过'
|
||||
: '审核中',
|
||||
}
|
||||
|
||||
return (
|
||||
<NCard class="mb-4" size="small">
|
||||
<div class="font-bold mb-2">{title}</div>
|
||||
<NGrid cols="2" xGap="16" yGap="12">
|
||||
<NGridItem>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-600 min-w-20">模板ID:</span>
|
||||
<span class="ml-2">{templateData.id}</span>
|
||||
</div>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-600 min-w-20">模板名称:</span>
|
||||
<span class="ml-2">{templateData.name}</span>
|
||||
</div>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-600 min-w-20">认证类型:</span>
|
||||
<span class="ml-2">{templateData.type}</span>
|
||||
</div>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-600 min-w-20">证件号码:</span>
|
||||
<span class="ml-2">{maskUtils.maskCertificateNumber(templateData.idNumber)}</span>
|
||||
</div>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-600 min-w-20">模板所有者:</span>
|
||||
<span class="ml-2">{templateData.owner}</span>
|
||||
</div>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-600 min-w-20">邮箱:</span>
|
||||
<span class="ml-2">{templateData.email}</span>
|
||||
</div>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-600 min-w-20">手机号码:</span>
|
||||
<span class="ml-2">{templateData.phone}</span>
|
||||
</div>
|
||||
</NGridItem>
|
||||
<NGridItem>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-600 min-w-20">审核状态:</span>
|
||||
<NTag type="success" size="small" class="ml-2">
|
||||
{templateData.status}
|
||||
</NTag>
|
||||
</div>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</NCard>
|
||||
)
|
||||
}
|
||||
|
||||
return () => (
|
||||
<>
|
||||
{/* 蓝色提示条 */}
|
||||
<NAlert type="info" class="mb-6" showIcon>
|
||||
模板更换处理时间约1-30分钟,请耐心等待
|
||||
</NAlert>
|
||||
|
||||
{/* 标题说明 */}
|
||||
<div class="mb-4">
|
||||
<div class="font-bold">当前模板与选择新模板</div>
|
||||
<div class="text-sm text-gray-600">以下是当前使用的实名模板,请选择要更换的新模板。</div>
|
||||
</div>
|
||||
|
||||
{/* 当前使用模板 */}
|
||||
{renderTemplateDetail(props.currentTemplate, '当前使用模板')}
|
||||
|
||||
{/* 备选信息模板选择器 */}
|
||||
<div class="mb-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div>
|
||||
切换实名信息模板<span class="text-gray-400">(切换时不会影响域名使用)</span>
|
||||
</div>
|
||||
<NButton ghost text size="tiny" onClick={() => fetchRealNameTemplateList()}>
|
||||
{{
|
||||
default: () => (
|
||||
<>
|
||||
<NIcon class="flex cursor-pointer ml-1" size={16}>
|
||||
<RefreshFilled />
|
||||
</NIcon>
|
||||
<span>刷新实名列表</span>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
</NButton>
|
||||
</div>
|
||||
<NSpin show={realNameTemplatesLoading.value}>
|
||||
<NSelect
|
||||
value={selectedTemplateId.value}
|
||||
onUpdateValue={(val) => handleSelectRealName(val)}
|
||||
options={[...templateOptions.value, { label: '创建实名模板…', value: '-1' }]}
|
||||
placeholder="请选择需要替换的实名模板"
|
||||
disabled={templateOptions.value.length === 0}
|
||||
clearable
|
||||
/>
|
||||
</NSpin>
|
||||
</div>
|
||||
|
||||
{/* 新选择的模板详细信息 */}
|
||||
{selectedTemplateDetail.value && renderTemplateDetail(selectedTemplateDetail.value, '新选择的模板详细信息')}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<NFlex justify="end" size="medium" class="mt-6">
|
||||
<NButton size="large" onClick={handleCancel}>
|
||||
取消
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={submitting.value}
|
||||
disabled={!selectedTemplateId.value}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
提交更换请求
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</>
|
||||
)
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
/**
|
||||
* 域名安全组件
|
||||
* 职责:展示域名锁定状态和安全服务配置
|
||||
*/
|
||||
|
||||
import { defineComponent, ref, PropType, computed } from 'vue'
|
||||
import { NCard, NGrid, NGridItem, NSwitch, NButton, NText, NIcon, NTag } from 'naive-ui'
|
||||
import { ShieldCheckmarkOutline, SettingsOutline } from '@vicons/ionicons5'
|
||||
import { useApp } from '@/components/layout/useStore'
|
||||
import { setDomainSecurity } from '@/api/domain'
|
||||
import { useMessage, useModal } from '@baota/naive-ui/hooks'
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { useDomainDetailState } from '../useStore'
|
||||
import DnssecManagement from './DnssecMgt'
|
||||
import type { DomainInfo, PrivacyInfo } from '@/types/domain'
|
||||
|
||||
interface SecurityProps {
|
||||
domainId: number
|
||||
domainInfo?: DomainInfo | null
|
||||
privacyInfo?: PrivacyInfo | null
|
||||
loading?: boolean
|
||||
onRefresh?: () => void
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DomainSecurity',
|
||||
props: {
|
||||
domainId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
domainInfo: {
|
||||
type: Object as PropType<DomainInfo | null>,
|
||||
default: null,
|
||||
},
|
||||
privacyInfo: {
|
||||
type: Object as PropType<PrivacyInfo | null>,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onRefresh: {
|
||||
type: Function as PropType<() => void>,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup(props: SecurityProps) {
|
||||
const { isMobile } = useApp()
|
||||
const { handleError } = useError()
|
||||
const { openPrivacyDialog } = useDomainDetailState()
|
||||
|
||||
// 域名锁定状态
|
||||
const transferLock = ref(props.domainInfo?.transfer_lock === 1)
|
||||
const updateLock = ref(props.domainInfo?.update_lock === 1)
|
||||
|
||||
// DNSSEC状态
|
||||
const dnssecEnabled = ref(false)
|
||||
|
||||
// 加载状态
|
||||
const transferLockLoading = ref(false)
|
||||
const updateLockLoading = ref(false)
|
||||
|
||||
// 隐私保护状态
|
||||
const isPrivacy = computed(() => {
|
||||
return !!props.domainInfo?.privacy
|
||||
})
|
||||
|
||||
// 切换禁止转移锁
|
||||
const handleTransferLockChange = async (value: boolean) => {
|
||||
try {
|
||||
transferLockLoading.value = true
|
||||
const {
|
||||
fetch,
|
||||
message: apiMessage,
|
||||
data,
|
||||
} = setDomainSecurity({
|
||||
domain_id: props.domainId,
|
||||
type: 'transfer',
|
||||
status: value ? 1 : 0,
|
||||
})
|
||||
apiMessage.value = true
|
||||
await fetch()
|
||||
|
||||
if (data.value?.status) {
|
||||
transferLock.value = value
|
||||
const message = useMessage()
|
||||
message.success(value ? '禁止转移锁已开启' : '禁止转移锁已关闭')
|
||||
props.onRefresh?.()
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
transferLockLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换禁止更新锁
|
||||
const handleUpdateLockChange = async (value: boolean) => {
|
||||
try {
|
||||
updateLockLoading.value = true
|
||||
const {
|
||||
fetch,
|
||||
message: apiMessage,
|
||||
data,
|
||||
} = setDomainSecurity({
|
||||
domain_id: props.domainId,
|
||||
type: 'update',
|
||||
status: value ? 1 : 0,
|
||||
})
|
||||
apiMessage.value = true
|
||||
await fetch()
|
||||
|
||||
if (data.value?.status) {
|
||||
updateLock.value = value
|
||||
const message = useMessage()
|
||||
message.success(value ? '禁止更新锁已开启' : '禁止更新锁已关闭')
|
||||
props.onRefresh?.()
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
updateLockLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 管理DNSSEC
|
||||
const handleManageDnssec = () => {
|
||||
useModal({
|
||||
title: 'DNSSEC管理',
|
||||
area: '900px',
|
||||
component: DnssecManagement,
|
||||
componentProps: {
|
||||
domainId: props.domainId,
|
||||
domainName: props.domainInfo?.full_domain || '',
|
||||
visible: true,
|
||||
onClose: () => {},
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 打开域名隐私保护弹窗
|
||||
const openCnDomainPrivacyModal = () => {
|
||||
if (props.domainInfo) {
|
||||
openPrivacyDialog.value = useModal({
|
||||
title: '.CN/.中国专属域名隐私保护',
|
||||
area: '600px',
|
||||
component: () => import('./PrivacyProtection'),
|
||||
componentProps: {
|
||||
domain: props.domainInfo,
|
||||
privacy: props.privacyInfo,
|
||||
refresh: props.onRefresh,
|
||||
onClose: () => {
|
||||
openPrivacyDialog.value?.close()
|
||||
},
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染信息项
|
||||
const renderInfoItem = (label: string, value: any, valueType: 'text' | 'switch' = 'text', config?: any) => {
|
||||
if (props.loading) {
|
||||
return (
|
||||
<div class="mb-2 flex items-center justify-between h-10">
|
||||
<div class="text-gray-500 font-bold w-25 mr-5 ml-1">{label}</div>
|
||||
<div class="w-8 h-4 bg-gray-200 rounded animate-pulse"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (valueType === 'switch') {
|
||||
return (
|
||||
<div class="mb-2 flex items-center justify-between h-10 hover:bg-gray-100">
|
||||
<div class="text-gray-500 font-bold w-25 mr-5 ml-1">{label}</div>
|
||||
<NSwitch
|
||||
value={value}
|
||||
onUpdateValue={config?.onChange}
|
||||
loading={config?.loading}
|
||||
disabled={props.loading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="mb-2 flex items-center justify-between h-10 hover:bg-gray-100">
|
||||
<div class="text-gray-500 font-bold w-25 mr-5 ml-1">{label}</div>
|
||||
<div>{value || '-'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return () => (
|
||||
<div class="py-2">
|
||||
<NGrid cols="1 m:2" xGap="16" yGap="16" responsive="screen">
|
||||
{/* 域名锁定 */}
|
||||
<NGridItem>
|
||||
<NCard title="域名锁定" header-style="font-size:16px;font-weight:500" class="h-full">
|
||||
{renderInfoItem('禁止转移锁', transferLock.value, 'switch', {
|
||||
onChange: handleTransferLockChange,
|
||||
loading: transferLockLoading.value,
|
||||
})}
|
||||
<div class="pl-2 mb-4 text-sm text-gray-600">防止域名被恶意转移到其他注册商</div>
|
||||
|
||||
{renderInfoItem('禁止更新锁', updateLock.value, 'switch', {
|
||||
onChange: handleUpdateLockChange,
|
||||
loading: updateLockLoading.value,
|
||||
})}
|
||||
<div class="pl-2 text-sm text-gray-600">防止域名信息被恶意修改</div>
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
|
||||
{/* 安全服务 */}
|
||||
<NGridItem>
|
||||
<NCard title="安全服务" header-style="font-size:16px;font-weight:500" class="h-full">
|
||||
<div class="mb-2 flex items-center justify-between h-10 hover:bg-gray-100">
|
||||
<div class="text-gray-500 font-bold w-25 mr-5 ml-1">DNSSEC</div>
|
||||
<NButton type="primary" size="small" onClick={handleManageDnssec} disabled={props.loading}>
|
||||
管理DNSSEC
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="pl-2 mb-4 text-sm text-gray-600">DNS安全扩展,防止DNS劫持和缓存投毒</div>
|
||||
|
||||
{/* 隐私保护 - 仅对.cn和.中国域名显示 */}
|
||||
{(props.domainInfo?.suffix === 'cn' || props.domainInfo?.suffix === '中国') && (
|
||||
<>
|
||||
<div class="mb-2 flex items-center justify-between h-10 hover:bg-gray-100">
|
||||
<div class="text-gray-500 font-bold w-40 mr-5 ml-1 whitespace-nowrap">
|
||||
.CN/.中国专属域名隐私保护
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<NTag type={isPrivacy.value ? 'success' : 'warning'} bordered={false}>
|
||||
{isPrivacy.value
|
||||
? `已开启(到期:${formatDate(props.privacyInfo?.end_time, 'yyyy-MM-dd')})`
|
||||
: '未开启'}
|
||||
</NTag>
|
||||
<NButton type="primary" size="small" onClick={openCnDomainPrivacyModal} disabled={props.loading}>
|
||||
{isPrivacy.value ? '延续隐私保护' : '开启保护'}
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-2 mb-4 text-sm text-gray-600">隐藏域名注册信息,保护个人隐私</div>
|
||||
</>
|
||||
)}
|
||||
</NCard>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -21,12 +21,12 @@ export default defineComponent({
|
|||
const router = useRouter()
|
||||
const BaseInfo = defineAsyncComponent(() => import('./components/BaseInfo'))
|
||||
const RealName = defineAsyncComponent(() => import('./components/RealName'))
|
||||
const DnsAnalysis = defineAsyncComponent(() => import('./components/DnsAnalysis/index'))
|
||||
const Security = defineAsyncComponent(() => import('./components/security'))
|
||||
|
||||
// 获取域名ID(从路由参数中获取)
|
||||
const domainId = route.params.id as string
|
||||
// 获取控制器
|
||||
const { loading, domainInfo, activeTab, refreshDomainInfo, switchTab } = useController(domainId)
|
||||
const { loading, domainInfo,privacyInfo, activeTab, refreshDomainInfo, switchTab } = useController(domainId)
|
||||
|
||||
return () => (
|
||||
<div class="domain-detail-container">
|
||||
|
|
@ -57,13 +57,24 @@ export default defineComponent({
|
|||
class="mb-4"
|
||||
>
|
||||
<NTabPane name="base" tab="基本信息">
|
||||
<BaseInfo domainInfo={domainInfo.value} loading={loading.value} onRefresh={refreshDomainInfo} />
|
||||
<BaseInfo
|
||||
domainInfo={domainInfo.value}
|
||||
privacyInfo={privacyInfo.value}
|
||||
loading={loading.value}
|
||||
onRefresh={refreshDomainInfo}
|
||||
/>
|
||||
</NTabPane>
|
||||
<NTabPane name="realName" tab="实名认证">
|
||||
<RealName domainId={Number(domainId)} />
|
||||
</NTabPane>
|
||||
<NTabPane name="analysis" tab="域名解析">
|
||||
<DnsAnalysis domainId={Number(domainId)} />
|
||||
<NTabPane name="security" tab="域名安全">
|
||||
<Security
|
||||
domainId={Number(domainId)}
|
||||
domainInfo={domainInfo.value}
|
||||
privacyInfo={privacyInfo.value}
|
||||
loading={loading.value}
|
||||
onRefresh={refreshDomainInfo}
|
||||
/>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NCard>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type {
|
|||
/**
|
||||
* 域名详情标签页类型
|
||||
*/
|
||||
export type DomainDetailTabKey = "base" | "realName" | "analysis" | "logs";
|
||||
export type DomainDetailTabKey = 'base' | 'realName' | 'security' | 'analysis' | 'logs'
|
||||
|
||||
/**
|
||||
* DNS记录表单数据
|
||||
|
|
|
|||
|
|
@ -5,57 +5,99 @@
|
|||
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
// import { useMessage } from "naive-ui";
|
||||
// import { useDialog } from "@baota/naive-ui/hooks";
|
||||
import { useModal } from "@baota/naive-ui/hooks";
|
||||
import { useDomainDetailState } from "./useStore";
|
||||
import { domainUtils } from "./config";
|
||||
import type { DomainDetailTabKey } from "./types.d";
|
||||
// 动态导入组件以避免循环依赖问题
|
||||
// import RealNameTemplateChangeDialog from "./components/RealNameTemplateChangeDialog";
|
||||
|
||||
/**
|
||||
* 域名详情页面控制器
|
||||
* @param domainId 域名ID
|
||||
*/
|
||||
export function useController(domainId: string | number) {
|
||||
// 获取状态管理
|
||||
const { loading, domainInfo, fetchDomainInfo, realNameInfo } =
|
||||
useDomainDetailState();
|
||||
const route = useRoute();
|
||||
// 获取状态管理
|
||||
const {
|
||||
loading,
|
||||
domainInfo,
|
||||
privacyInfo,
|
||||
fetchDomainInfo,
|
||||
realNameInfo,
|
||||
realNameInfoUpdating,
|
||||
openTemplateChangeDialog,
|
||||
openPrivacyDialog,
|
||||
fetchRealNameTemplateList,
|
||||
} = useDomainDetailState()
|
||||
const route = useRoute()
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref<DomainDetailTabKey>("base");
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref<DomainDetailTabKey>('base')
|
||||
|
||||
/**
|
||||
* 刷新域名详情
|
||||
*/
|
||||
const refreshDomainInfo = () => {
|
||||
fetchDomainInfo(domainId);
|
||||
};
|
||||
/**
|
||||
* 刷新域名详情
|
||||
* 用于更新实名状态等信息
|
||||
*/
|
||||
const refreshDomainInfo = async () => {
|
||||
try {
|
||||
await fetchDomainInfo(domainId)
|
||||
} catch (error) {
|
||||
// 错误已在store中处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换标签页
|
||||
* @param tab 标签页键值
|
||||
*/
|
||||
const switchTab = (tab: DomainDetailTabKey) => {
|
||||
activeTab.value = tab;
|
||||
};
|
||||
/**
|
||||
* 切换标签页
|
||||
* @param tab 标签页键值
|
||||
*/
|
||||
const switchTab = (tab: DomainDetailTabKey) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
// 组件挂载时获取域名详情
|
||||
onMounted(() => {
|
||||
if (route.query.tabs)
|
||||
activeTab.value = route.query.tabs as DomainDetailTabKey;
|
||||
fetchDomainInfo(domainId);
|
||||
});
|
||||
/**
|
||||
* 打开实名模板更换弹窗
|
||||
*/
|
||||
const openTemplateChangeModal = async () => {
|
||||
// 先加载实名模板列表
|
||||
await fetchRealNameTemplateList()
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
domainInfo,
|
||||
realNameInfo,
|
||||
activeTab,
|
||||
switchTab,
|
||||
// 方法
|
||||
refreshDomainInfo,
|
||||
// 工具
|
||||
domainUtils,
|
||||
};
|
||||
// 遵循 real-name 模式,将 useModal 结果赋值给 store 中的 ref
|
||||
openTemplateChangeDialog.value = useModal({
|
||||
title: '更换实名模板',
|
||||
area: '650px',
|
||||
component: () => import('./components/RealNameTemplateChangeDialog'),
|
||||
componentProps: {
|
||||
domainId: Number(domainId),
|
||||
domainInfo: domainInfo.value,
|
||||
currentTemplate: realNameInfo.value,
|
||||
refresh: async () => {
|
||||
// 刷新域名信息
|
||||
await fetchDomainInfo(domainId)
|
||||
},
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时获取域名详情
|
||||
onMounted(() => {
|
||||
if (route.query.tabs) activeTab.value = route.query.tabs as DomainDetailTabKey
|
||||
fetchDomainInfo(domainId)
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
domainInfo,
|
||||
privacyInfo,
|
||||
realNameInfo,
|
||||
realNameInfoUpdating,
|
||||
activeTab,
|
||||
switchTab,
|
||||
// 方法
|
||||
refreshDomainInfo,
|
||||
openTemplateChangeModal,
|
||||
// 工具
|
||||
domainUtils,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
*/
|
||||
|
||||
import { fetchDomainDetail } from "@/api/domain";
|
||||
import { fetchContactUserDetail } from "@/api/real-name";
|
||||
import { useError } from "@baota/hooks/error";
|
||||
|
||||
import type { DomainInfo, RealNameInfo } from "@/types/domain";
|
||||
import type { DomainInfo, RealNameInfo, PrivacyInfo } from '@/types/domain'
|
||||
import type { ContactTemplateItem } from "@/types/real-name";
|
||||
|
||||
const { handleError } = useError();
|
||||
|
||||
|
|
@ -14,49 +16,93 @@ const { handleError } = useError();
|
|||
* 域名详情页面状态Store
|
||||
*/
|
||||
export const useDomainDetailStore = defineStore("domain-detail-store", () => {
|
||||
// -------------------- 状态定义 --------------------
|
||||
// -------------------- 状态定义 --------------------
|
||||
|
||||
/** 页面加载状态 */
|
||||
const loading = ref(false);
|
||||
/** 页面加载状态 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 域名详情信息 */
|
||||
const domainInfo = ref<DomainInfo | null>(null);
|
||||
/** 域名详情信息 */
|
||||
const domainInfo = ref<DomainInfo | null>(null)
|
||||
|
||||
/** 实名认证信息 */
|
||||
const realNameInfo = ref<RealNameInfo | null>(null);
|
||||
/** 实名认证信息 */
|
||||
const realNameInfo = ref<RealNameInfo | null>(null)
|
||||
|
||||
// -------------------- 方法定义 --------------------
|
||||
/** 实名信息更新状态 */
|
||||
const realNameInfoUpdating = ref<RealNameInfo | null>(null)
|
||||
|
||||
/**
|
||||
* 获取域名详情
|
||||
* @param domainId 域名ID
|
||||
*/
|
||||
const fetchDomainInfo = async (domainId: number | string) => {
|
||||
try {
|
||||
const { fetch, data } = fetchDomainDetail({
|
||||
domain_id: Number(domainId),
|
||||
});
|
||||
await fetch();
|
||||
const { status, data: rdata } = data.value;
|
||||
if (status) {
|
||||
domainInfo.value = rdata.domain_info;
|
||||
realNameInfo.value = rdata.real_name_info;
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
/** 实名模板更换弹窗控制 */
|
||||
const openTemplateChangeDialog = ref()
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
domainInfo,
|
||||
realNameInfo,
|
||||
/** 实名模板相关状态 */
|
||||
const realNameTemplates = ref<ContactTemplateItem[]>([])
|
||||
const realNameTemplatesLoading = ref(false)
|
||||
|
||||
// 方法
|
||||
fetchDomainInfo,
|
||||
};
|
||||
/** 隐私保护弹窗控制 */
|
||||
const openPrivacyDialog = ref()
|
||||
|
||||
/** 隐私保护信息 */
|
||||
const privacyInfo = ref<PrivacyInfo | null>(null)
|
||||
|
||||
// -------------------- 方法定义 --------------------
|
||||
|
||||
/**
|
||||
* 获取域名详情
|
||||
* @param domainId 域名ID
|
||||
*/
|
||||
const fetchDomainInfo = async (domainId: number | string) => {
|
||||
try {
|
||||
const { fetch, data } = fetchDomainDetail({
|
||||
domain_id: Number(domainId),
|
||||
})
|
||||
await fetch()
|
||||
const { status, data: rdata } = data.value
|
||||
if (status) {
|
||||
domainInfo.value = rdata.domain_info
|
||||
realNameInfo.value = rdata.real_name_info
|
||||
realNameInfoUpdating.value = rdata.real_name_update_info
|
||||
privacyInfo.value = rdata.privacy_info
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实名模板列表
|
||||
*/
|
||||
const fetchRealNameTemplateList = async () => {
|
||||
try {
|
||||
realNameTemplatesLoading.value = true
|
||||
const { fetch, data } = fetchContactUserDetail({ p: 1, rows: 50, status: 2 })
|
||||
await fetch()
|
||||
const payload = data.value as any
|
||||
const list = (payload?.msg?.data || payload?.data?.data || []) as ContactTemplateItem[]
|
||||
realNameTemplates.value = Array.isArray(list) ? list : []
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
realNameTemplates.value = []
|
||||
} finally {
|
||||
realNameTemplatesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
domainInfo,
|
||||
privacyInfo,
|
||||
realNameInfo,
|
||||
realNameInfoUpdating,
|
||||
openTemplateChangeDialog,
|
||||
realNameTemplates,
|
||||
realNameTemplatesLoading,
|
||||
openPrivacyDialog,
|
||||
|
||||
// 方法
|
||||
fetchDomainInfo,
|
||||
fetchRealNameTemplateList,
|
||||
}
|
||||
});
|
||||
|
||||
export const useDomainDetailState = () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,416 @@
|
|||
import { defineComponent, ref, type PropType, reactive, computed, watch } from 'vue'
|
||||
import {
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NSelect,
|
||||
NButton,
|
||||
NInputNumber,
|
||||
NSpace,
|
||||
NCascader,
|
||||
useMessage,
|
||||
type FormInst,
|
||||
type FormRules,
|
||||
type SelectOption,
|
||||
NFlex,
|
||||
NIcon,
|
||||
} from 'naive-ui'
|
||||
import { DnsRecordForm } from '../../types'
|
||||
import { useModalHooks } from '@baota/naive-ui/hooks'
|
||||
import { useDnsAnalysisStore } from './useStore'
|
||||
import { useApp } from '@/components/layout/useStore'
|
||||
|
||||
|
||||
// 获取Store
|
||||
const {
|
||||
createRecord,
|
||||
updateRecord,
|
||||
} = useDnsAnalysisStore();
|
||||
/**
|
||||
* 域名解析表单组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'DnsAnalysisForm',
|
||||
props: {
|
||||
/** 是否显示表单 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 表单模式:add-新增, edit-编辑 */
|
||||
mode: {
|
||||
type: String as PropType<'add' | 'edit'>,
|
||||
default: 'add',
|
||||
},
|
||||
/** 初始表单数据 */
|
||||
initialData: {
|
||||
type: Object as () => Partial<DnsRecordForm & { domain_id: number }>,
|
||||
default: () => ({}),
|
||||
},
|
||||
recordTypesOptions: {
|
||||
type: Array as PropType<SelectOption[]>,
|
||||
default: () => [],
|
||||
},
|
||||
viewsOptions: {
|
||||
type: Array as PropType<SelectOption[]>,
|
||||
default: () => [],
|
||||
},
|
||||
/** 刷新数据函数 */
|
||||
refresh: {
|
||||
type: Function as PropType<() => Promise<void>>,
|
||||
default: () => Promise.resolve(),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// const { handleFormSubmit } = useController(props)
|
||||
// 表单引用和消息实例
|
||||
const message = useMessage()
|
||||
const { close } = useModalHooks()
|
||||
const { isMobile } = useApp()
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
const handleCloseModal = close() // 构建关闭方式
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<DnsRecordForm & { domain_id: number }>({
|
||||
// 基础信息
|
||||
domain_id: 0,
|
||||
record_id: '',
|
||||
record: '',
|
||||
type: '',
|
||||
value: '',
|
||||
mx: 0,
|
||||
ttl: 0,
|
||||
remark: '',
|
||||
viewId: 0,
|
||||
...props.initialData,
|
||||
})
|
||||
|
||||
// 当前选中的字段,用于切换下方帮助信息
|
||||
const activeField = ref<string>('record')
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
record: { required: true, message: '请输入主机记录' },
|
||||
value: { required: true, message: '请输入记录值' },
|
||||
mx: { required: true, message: '请输入MX值' },
|
||||
ttl: { required: true, message: '请输入TTL值' },
|
||||
remark: { required: true, message: '请输入备注' },
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
const submitData = { ...formData }
|
||||
|
||||
if (props.mode === 'add') {
|
||||
await createRecord(submitData)
|
||||
handleCloseModal()
|
||||
props.refresh()
|
||||
} else {
|
||||
await updateRecord({
|
||||
record_id: submitData.record_id as string,
|
||||
...submitData,
|
||||
})
|
||||
handleCloseModal()
|
||||
props.refresh()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
message.error('请检查表单填写是否正确')
|
||||
}
|
||||
}
|
||||
|
||||
// -------- 快填面板(移动端)逻辑 --------
|
||||
// const isMobileDevice = computed(() => typeof window !== 'undefined' && window.innerWidth <= 740)
|
||||
// // 由可用帮助项决定步骤(不包含备注)
|
||||
// const baseStepFields: Array<keyof DnsRecordForm> = ['record', 'type', 'value', 'ttl']
|
||||
// const steps = computed(() => {
|
||||
// // 仅保留在帮助集中定义的字段
|
||||
// const allowed = baseStepFields.filter((f) => !!getFieldHelpInfo(String(f), formData.type || 'A'))
|
||||
// // MX 类型时插入 mx
|
||||
// if ((formData.type || 'A') === 'MX') {
|
||||
// allowed.push('mx')
|
||||
// }
|
||||
// return allowed
|
||||
// })
|
||||
// const currentStepIndex = ref<number>(0)
|
||||
// const currentStepField = computed(() => steps.value[currentStepIndex.value])
|
||||
// // 面板显隐
|
||||
// const showQuickFill = ref(true)
|
||||
// // 当前步骤选中的建议索引
|
||||
// const selectedSuggestionIndex = ref<number | null>(null)
|
||||
// // 记录值步骤的提示文本
|
||||
// const valueStepHint = ref<string>('')
|
||||
|
||||
// // 进入"记录值"步骤时自动生成提示
|
||||
// watch(currentStepField, (field) => {
|
||||
// if (field === 'value') {
|
||||
// valueStepHint.value = getValueStepHint(formData.type || 'A')
|
||||
// } else {
|
||||
// valueStepHint.value = ''
|
||||
// }
|
||||
// })
|
||||
|
||||
// const goPrev = () => {
|
||||
// currentStepIndex.value = Math.max(0, currentStepIndex.value - 1)
|
||||
// activeField.value = (currentStepField.value as string) || 'record'
|
||||
// selectedSuggestionIndex.value = null
|
||||
// valueStepHint.value = ''
|
||||
// }
|
||||
// const goNext = () => {
|
||||
// currentStepIndex.value = Math.min(steps.value.length - 1, currentStepIndex.value + 1)
|
||||
// activeField.value = (currentStepField.value as string) || 'record'
|
||||
// selectedSuggestionIndex.value = null
|
||||
// valueStepHint.value = ''
|
||||
// }
|
||||
|
||||
// const ensureValueThenNext = () => {
|
||||
// const field = currentStepField.value
|
||||
// if (!field) return
|
||||
// const val = (formData as any)[field]
|
||||
// if (field === 'mx' || field === 'ttl') {
|
||||
// if (val === null || val === undefined) return
|
||||
// } else if (!val) {
|
||||
// return
|
||||
// }
|
||||
// goNext()
|
||||
// }
|
||||
|
||||
// // 生成"记录值"步骤的类型提示
|
||||
// const getValueStepHint = (type: string): string => {
|
||||
// switch (String(type || 'A').toUpperCase()) {
|
||||
// case 'MX':
|
||||
// return '您选择的是 MX 记录,请在此填写邮件服务器的域名或 IP 地址,一般由邮件注册商提供,域名结尾"."表示根域,系统默认自动添加'
|
||||
// case 'A':
|
||||
// return '当前为 A 记录,请填写域名指向一个 IPv4 地址,例如 8.8.8.8'
|
||||
// case 'AAAA':
|
||||
// return '当前为 AAAA 记录,请填写域名指向一个 IPv6 地址,如 ff06:0:0:0:0:0:0:c3'
|
||||
// case 'CNAME':
|
||||
// return '当前为 CNAME 记录,请域名指向的另一个域名地址,不可填写 IP 地址'
|
||||
// case 'TXT':
|
||||
// return '当前为 TXT 记录,请填写文本内容,常用于验证/配置'
|
||||
// case 'NS':
|
||||
// return '当前为 NS 记录,请填写权威 DNS 服务器域名'
|
||||
// default:
|
||||
// return '请根据记录类型填写对应格式的记录值'
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 建议项生成
|
||||
// type Suggestion = { label: string; value: any; desc?: string }
|
||||
// const getTypeSuggestions = (): Suggestion[] => {
|
||||
// const opts = (props.recordTypesOptions || []) as any[]
|
||||
// return opts.map((o) => ({ label: String(o.label ?? o.value), value: o.value, desc: o.desc }))
|
||||
// }
|
||||
// const getValueSuggestions = (): Suggestion[] => {
|
||||
// const help = getFieldHelpInfo('value', formData.type || 'A')
|
||||
// return (help.examples || []).map((e: any) => ({ label: e.value, value: e.value, desc: e.desc }))
|
||||
// }
|
||||
// const getRecordSuggestions = (): Suggestion[] => {
|
||||
// const help = getFieldHelpInfo('record', formData.type || 'A')
|
||||
// return (help.examples || []).map((e: any) => ({ label: e.value, value: e.value, desc: e.desc }))
|
||||
// }
|
||||
// const getTtlSuggestions = (): Suggestion[] => [300, 600, 1200].map((v) => ({ label: String(v), value: v }))
|
||||
// const getMxSuggestions = (): Suggestion[] => [5, 10, 20].map((v) => ({ label: String(v), value: v }))
|
||||
// const getRemarkSuggestions = (): Suggestion[] => []
|
||||
|
||||
// const suggestions = computed<Suggestion[]>(() => {
|
||||
// switch (currentStepField.value) {
|
||||
// case 'record':
|
||||
// return getRecordSuggestions()
|
||||
// case 'type':
|
||||
// return getTypeSuggestions()
|
||||
// case 'value':
|
||||
// return getValueSuggestions()
|
||||
// case 'ttl':
|
||||
// return getTtlSuggestions()
|
||||
// case 'mx':
|
||||
// return getMxSuggestions()
|
||||
// case 'remark':
|
||||
// return getRemarkSuggestions()
|
||||
// default:
|
||||
// return []
|
||||
// }
|
||||
// })
|
||||
|
||||
// const applySuggestion = (s: Suggestion, idx?: number) => {
|
||||
// const field = currentStepField.value
|
||||
// if (!field) return
|
||||
// // 切换选中态(瞬时视觉反馈)
|
||||
// if (typeof idx === 'number') {
|
||||
// selectedSuggestionIndex.value = idx
|
||||
// }
|
||||
// // 在"记录值"步骤,不直接填充,仅展示类型提示
|
||||
// if (field === 'value') {
|
||||
// valueStepHint.value = getValueStepHint(formData.type || 'A')
|
||||
// return
|
||||
// }
|
||||
// ;(formData as any)[field] = s.value
|
||||
// // 立即进入下一步
|
||||
// goNext()
|
||||
// }
|
||||
|
||||
// const stepTitleMap: Record<string, string> = {
|
||||
// record: '主机记录',
|
||||
// type: '记录类型',
|
||||
// value: '记录值',
|
||||
// viewId: '线路类型',
|
||||
// ttl: 'TTL',
|
||||
// mx: 'MX/权重',
|
||||
// remark: '备注',
|
||||
// }
|
||||
|
||||
return () => (
|
||||
<div class="flex flex-col gap-3">
|
||||
{/* 上层:表单 */}
|
||||
<NForm
|
||||
ref={formRef}
|
||||
model={formData}
|
||||
rules={rules}
|
||||
labelPlacement={isMobile.value ? 'top' : 'left'}
|
||||
labelWidth="120px"
|
||||
requireMarkPlacement="right-hanging"
|
||||
>
|
||||
{/* 两列布局表单 */}
|
||||
<NFormItem label="主机记录">
|
||||
<NInput
|
||||
value={formData.record}
|
||||
onFocus={() => (activeField.value = 'record')}
|
||||
onUpdateValue={(v: string) => (formData.record = v)}
|
||||
placeholder="@ / www / *"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="记录类型">
|
||||
<NSelect
|
||||
value={formData.type}
|
||||
options={props.recordTypesOptions as any}
|
||||
onFocus={() => (activeField.value = 'type')}
|
||||
onUpdateValue={(v: any) => (formData.type = v)}
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="线路类型">
|
||||
<NCascader
|
||||
value={formData.viewId}
|
||||
options={props.viewsOptions as any}
|
||||
onFocus={() => (activeField.value = 'viewId')}
|
||||
onUpdateValue={(v: any) => (formData.viewId = v)}
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="记录值">
|
||||
<NInput
|
||||
value={formData.value}
|
||||
onFocus={() => (activeField.value = 'value')}
|
||||
onUpdateValue={(v: string) => (formData.value = v)}
|
||||
placeholder="目标IP或域名"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NSpace>
|
||||
<NFormItem label="TTL">
|
||||
<NInputNumber
|
||||
value={formData.ttl}
|
||||
min={1}
|
||||
onFocus={() => (activeField.value = 'ttl')}
|
||||
onUpdateValue={(v: any) => (formData.ttl = Number(v) || 1)}
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="MX(可选)">
|
||||
<NInputNumber
|
||||
value={formData.mx}
|
||||
min={0}
|
||||
max={100}
|
||||
onFocus={() => (activeField.value = 'mx')}
|
||||
onUpdateValue={(v: any) => (formData.mx = Number(v) || 0)}
|
||||
/>
|
||||
</NFormItem>
|
||||
</NSpace>
|
||||
<NFormItem label="备注(可选)">
|
||||
<NInput value={formData.remark} onUpdateValue={(v: string) => (formData.remark = v)} />
|
||||
</NFormItem>
|
||||
|
||||
{/* 表单操作按钮 */}
|
||||
<NFormItem>
|
||||
<NFlex class="mt-4 w-full" justify="end">
|
||||
<NButton onClick={handleCloseModal}>取消</NButton>
|
||||
<NButton type="primary" onClick={handleSubmit}>
|
||||
保存
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
{/* 移动端快填面板 */}
|
||||
{/*isMobileDevice.value && showQuickFill.value && (
|
||||
<div class="fixed left-0 right-0 bottom-0 z-50 bg-white border-t border-gray-200 p-3 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-[12px] text-gray-500">
|
||||
步骤 {currentStepIndex.value + 1}/{steps.value.length} (快填)
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<NButton size="small" tertiary onClick={goPrev} disabled={currentStepIndex.value === 0}>
|
||||
上一项
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
tertiary
|
||||
onClick={goNext}
|
||||
disabled={currentStepIndex.value >= steps.value.length - 1}
|
||||
>
|
||||
下一项
|
||||
</NButton>
|
||||
<NButton size="small" quaternary onClick={() => (showQuickFill.value = false)} aria-label="关闭快填">
|
||||
<NIcon size={16}>
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[13px] font-medium text-slate-800 mb-2">
|
||||
{stepTitleMap[currentStepField.value as string] || '当前字段'}
|
||||
</div>
|
||||
<div class="text-[12px] text-gray-600 mb-2 line-clamp-2">
|
||||
{(getFieldHelpInfo(String(currentStepField.value || ''), formData.type || 'A') as any)?.desc ||
|
||||
(getFieldHelpInfo(String(currentStepField.value || ''), formData.type || 'A') as any)?.description}
|
||||
</div>
|
||||
{currentStepField.value === 'value' && valueStepHint.value ? (
|
||||
<div class="text-[12px] text-gray-600 mt-2">
|
||||
<span class="px-3 py-2 bg-red-50 border-l-4 border-red-500 rounded text-[11px] text-slate-800 block">
|
||||
<span class="font-medium">提示:</span>
|
||||
{valueStepHint.value}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
suggestions.value.length > 0 && (
|
||||
<div class="max-h-[160px] overflow-y-auto flex flex-col gap-1 mb-2 pr-1">
|
||||
{suggestions.value.map((s, i) => (
|
||||
<div key={i}>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
onClick={() => applySuggestion(s, i)}
|
||||
class={
|
||||
(selectedSuggestionIndex.value === i ? 'bg-blue-50 border border-blue-200 ' : '') +
|
||||
'!text-[12px] !px-2 !py-1 bg-gray-50 hover:bg-gray-100 w-full justify-start'
|
||||
}
|
||||
>
|
||||
<span>{s.label}</span>
|
||||
{s.desc && <span class="ml-1 text-[11px] text-gray-500">- {s.desc}</span>}
|
||||
</NButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<NFlex justify="end" class="mt-2">
|
||||
<NButton size="small" onClick={ensureValueThenNext} type="primary">
|
||||
{currentStepIndex.value >= steps.value.length - 1 ? '保存' : '填入并下一步'}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</div>
|
||||
)*/}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,336 @@
|
|||
/**
|
||||
* @fileoverview DNS分析模块 - 控制器层
|
||||
* @description 连接视图层和模型层,处理业务逻辑和UI状态
|
||||
*/
|
||||
|
||||
import { ref, reactive, computed, watch, onMounted } from "vue";
|
||||
import { useDnsAnalysisStore } from "./useStore";
|
||||
import type { DnsRecordItem } from "@/types/domain";
|
||||
import type {
|
||||
// GetDnsRecordListRequest,
|
||||
UpdateDnsRecordRequest,
|
||||
DeleteDnsRecordRequest,
|
||||
CreateDnsRecordRequest,
|
||||
ToggleDnsRecordRequest,
|
||||
} from "@/types/dns";
|
||||
// import type { SelectOption } from "naive-ui";
|
||||
import { DnsRecordForm } from "../../types";
|
||||
import { useModal, useMessage, useForm, useFormHooks } from '@baota/naive-ui/hooks'
|
||||
import DnsAnalysisForm from './form'
|
||||
import { NButton, NSpace } from 'naive-ui'
|
||||
import { useApp } from '@/components/layout/useStore'
|
||||
|
||||
/**
|
||||
* DNS分析控制器参数接口
|
||||
*/
|
||||
interface DnsAnalysisControllerProps {
|
||||
/** 域名ID */
|
||||
domainId: number;
|
||||
/** 域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
domainType?: number;
|
||||
}
|
||||
// 获取移动端状态
|
||||
const { isMobile } = useApp()
|
||||
|
||||
/**
|
||||
* DNS分析控制器
|
||||
* @param domainId 域名ID
|
||||
* @param domainType 域名类型:1=宝塔内部域名,2=外部域名
|
||||
* @returns 暴露的状态和方法
|
||||
*/
|
||||
export function useDnsAnalysisController(domainId: number, domainType: number = 1) {
|
||||
// 获取Store
|
||||
const {
|
||||
isLoading,
|
||||
dnsRecords,
|
||||
hasRecords,
|
||||
pagination,
|
||||
recordCount,
|
||||
viewsOptions,
|
||||
recordListParams,
|
||||
searchKeyOptions,
|
||||
recordTypesOptions,
|
||||
fetchRecords,
|
||||
fetchViews,
|
||||
fetchRecordTypes,
|
||||
createRecord,
|
||||
updateRecord,
|
||||
deleteRecord,
|
||||
pauseRecord,
|
||||
startRecord,
|
||||
setCurrentDomainId,
|
||||
setCurrentDomainType,
|
||||
} = useDnsAnalysisStore();
|
||||
|
||||
// 本地UI状态
|
||||
const editingRecord = ref<DnsRecordItem | null>();
|
||||
const isEditing = ref<string>("");
|
||||
const isAdding = ref<boolean>(false);
|
||||
|
||||
// dns解析弹窗
|
||||
const dnsAnalysisDialog = ref<boolean>(false);
|
||||
|
||||
// 新记录表单
|
||||
const newRecord = reactive<DnsRecordForm>({
|
||||
record_id: "",
|
||||
record: "",
|
||||
type: "A",
|
||||
value: "",
|
||||
mx: 1,
|
||||
ttl: 600,
|
||||
remark: "",
|
||||
viewId: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* 取消添加
|
||||
*/
|
||||
const cancelAdding = (): void => {
|
||||
isAdding.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建DNS记录
|
||||
*/
|
||||
const handleCreateRecord = async (): Promise<boolean> => {
|
||||
const params: CreateDnsRecordRequest = {
|
||||
domain_id: domainId,
|
||||
record: newRecord.record,
|
||||
type: newRecord.type,
|
||||
value: newRecord.value,
|
||||
ttl: newRecord.ttl,
|
||||
mx: newRecord.mx,
|
||||
remark: newRecord.remark,
|
||||
viewId: newRecord.viewId,
|
||||
};
|
||||
try {
|
||||
await createRecord(params);
|
||||
isAdding.value = false;
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新DNS记录
|
||||
*/
|
||||
const handleUpdateRecord = async (): Promise<boolean> => {
|
||||
const params: UpdateDnsRecordRequest = {
|
||||
domain_id: domainId,
|
||||
record_id: newRecord.record_id || 0,
|
||||
record: newRecord.record,
|
||||
type: newRecord.type,
|
||||
value: newRecord.value,
|
||||
ttl: newRecord.ttl,
|
||||
mx: newRecord.mx,
|
||||
remark: newRecord.remark,
|
||||
viewId: newRecord.viewId,
|
||||
};
|
||||
try {
|
||||
await updateRecord(params);
|
||||
isEditing.value = "";
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除DNS记录
|
||||
* @param recordId 记录ID
|
||||
*/
|
||||
const handlDeleteRecord = async (recordId: number | string) => {
|
||||
const params: DeleteDnsRecordRequest = {
|
||||
record_id: recordId,
|
||||
domain_id: domainId,
|
||||
};
|
||||
await deleteRecord(params);
|
||||
};
|
||||
|
||||
/**
|
||||
* 暂停DNS解析记录
|
||||
* @param recordId 记录ID
|
||||
*/
|
||||
const handlPauseRecord = async (recordId: number | string) => {
|
||||
const params: ToggleDnsRecordRequest = {
|
||||
record_id: recordId,
|
||||
domain_id: domainId,
|
||||
};
|
||||
await pauseRecord(params);
|
||||
};
|
||||
|
||||
/**
|
||||
* 启用DNS解析记录
|
||||
* @param recordId 记录ID
|
||||
*/
|
||||
const handlStartRecord = async (recordId: number | string) => {
|
||||
const params: ToggleDnsRecordRequest = {
|
||||
record_id: recordId,
|
||||
domain_id: domainId,
|
||||
};
|
||||
await startRecord(params);
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换记录状态(启用/暂停)
|
||||
* @param record 记录对象
|
||||
*/
|
||||
const toggleRecordStatus = async (record: DnsRecordItem) => {
|
||||
// 根据当前状态决定是启用还是暂停
|
||||
// state: 0-启用, 1-暂停
|
||||
if (record.state === 0) {
|
||||
await handlPauseRecord(record.record_id);
|
||||
} else {
|
||||
await handlStartRecord(record.record_id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 打开dns解析弹窗(移动端),支持添加/编辑
|
||||
*/
|
||||
function openDnsAnalysisDialog(mode: 'add' | 'edit' = 'add', record?: DnsRecordItem) {
|
||||
if (!isMobile.value) return
|
||||
// 组装初始数据
|
||||
const initialData: Partial<DnsRecordForm & { domain_id: number }> = mode === 'edit' && record
|
||||
? {
|
||||
record_id: String(record.record_id || 0),
|
||||
record: record.record,
|
||||
type: record.type as any,
|
||||
value: record.value,
|
||||
ttl: Number((record as any).TTL ?? 600),
|
||||
mx: Number((record as any).MX ?? 0),
|
||||
remark: record.remark,
|
||||
viewId: Number((record as any).viewID ?? 0),
|
||||
}
|
||||
: {
|
||||
record_id: '',
|
||||
record: '',
|
||||
type: 'A',
|
||||
value: '',
|
||||
ttl: 600,
|
||||
mx: 1,
|
||||
remark: '',
|
||||
viewId: 0,
|
||||
}
|
||||
initialData['domain_id'] = domainId
|
||||
useModal({
|
||||
title: mode === 'add' ? '新增DNS记录' : '编辑DNS记录',
|
||||
area: ['100%','100%'],
|
||||
component: DnsAnalysisForm,
|
||||
componentProps: {
|
||||
mode,
|
||||
initialData,
|
||||
recordTypesOptions: recordTypesOptions.value,
|
||||
viewsOptions: viewsOptions.value,
|
||||
refresh: fetchRecords,
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
};
|
||||
|
||||
// -------------------- 搜索表单(useForm) --------------------
|
||||
const { useFormInput, useFormSelect } = useFormHooks()
|
||||
const formConfig = () => [
|
||||
useFormSelect(
|
||||
'',
|
||||
'searchKey',
|
||||
searchKeyOptions.value,
|
||||
{
|
||||
placeholder: '请选择查询字段',
|
||||
class: 'w-30',
|
||||
disabled: isAdding.value || isEditing.value !== '',
|
||||
},
|
||||
{ showLabel: false, showFeedback: false },
|
||||
),
|
||||
useFormInput(
|
||||
'',
|
||||
'searchValue',
|
||||
{
|
||||
placeholder: '请输入查询的字段内容',
|
||||
clearable: true,
|
||||
disabled: isAdding.value || isEditing.value !== '',
|
||||
},
|
||||
{ showLabel: false, showFeedback: false },
|
||||
),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => (
|
||||
<NSpace>
|
||||
<NButton type="primary" onClick={() => formFetchSearch()}>
|
||||
查询
|
||||
</NButton>
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
async function handleFormSearch(values: { searchKey?: string | number; searchValue?: string }) {
|
||||
recordListParams.value.searchKey = values?.searchKey as any
|
||||
recordListParams.value.searchValue = values?.searchValue || ''
|
||||
recordListParams.value.p = 1
|
||||
await fetchRecords()
|
||||
}
|
||||
|
||||
const { component: FilterForm, fetch: formFetchSearch } = useForm({
|
||||
config: formConfig(),
|
||||
defaultValue: {
|
||||
searchKey: recordListParams.value.searchKey,
|
||||
searchValue: recordListParams.value.searchValue,
|
||||
},
|
||||
request: handleFormSearch,
|
||||
})
|
||||
|
||||
|
||||
// 组件挂载时获取记录
|
||||
onMounted(async () => {
|
||||
if (domainId) {
|
||||
setCurrentDomainId(domainId);
|
||||
setCurrentDomainType(domainType);
|
||||
await fetchRecords(); // 先获取记录
|
||||
await fetchRecordTypes(); // 再获取记录类型
|
||||
await fetchViews(); // 最后获取线路
|
||||
}
|
||||
});
|
||||
|
||||
// 返回暴露的状态和方法
|
||||
return {
|
||||
// 从Store获取的状态
|
||||
isLoading,
|
||||
dnsRecords,
|
||||
hasRecords,
|
||||
pagination,
|
||||
recordListParams,
|
||||
searchKeyOptions,
|
||||
recordCount,
|
||||
viewsOptions,
|
||||
recordTypesOptions,
|
||||
isMobile,
|
||||
|
||||
// 本地UI状态
|
||||
newRecord,
|
||||
editingRecord,
|
||||
isEditing,
|
||||
isAdding,
|
||||
|
||||
// 方法
|
||||
fetchRecords,
|
||||
fetchViews,
|
||||
fetchRecordTypes,
|
||||
handleCreateRecord,
|
||||
handleUpdateRecord,
|
||||
handlDeleteRecord,
|
||||
handlPauseRecord,
|
||||
handlStartRecord,
|
||||
toggleRecordStatus,
|
||||
// startAdding,
|
||||
cancelAdding,
|
||||
|
||||
openDnsAnalysisDialog,
|
||||
|
||||
// 表单
|
||||
FilterForm,
|
||||
formFetchSearch,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
/**
|
||||
* @fileoverview DNS分析模块 - 状态管理层
|
||||
* @description 使用Pinia管理DNS记录相关状态,提供数据获取和操作方法
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
getDnsRecordList,
|
||||
createDnsRecord,
|
||||
updateDnsRecord,
|
||||
deleteDnsRecord,
|
||||
getViews,
|
||||
getRecordTypeList,
|
||||
pauseDnsRecord,
|
||||
startDnsRecord,
|
||||
} from '@/api/dns'
|
||||
import type {
|
||||
GetDnsRecordListRequest,
|
||||
UpdateDnsRecordRequest,
|
||||
DeleteDnsRecordRequest,
|
||||
CreateDnsRecordRequest,
|
||||
ToggleDnsRecordRequest,
|
||||
DnsView,
|
||||
DnsRecordType,
|
||||
} from '@/types/dns'
|
||||
import type { DnsRecordItem } from '@/types/domain'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { CascaderOption, PaginationProps, SelectOption } from 'naive-ui'
|
||||
|
||||
// 错误处理
|
||||
const { handleError } = useError()
|
||||
|
||||
/**
|
||||
* DNS分析模块状态管理
|
||||
*/
|
||||
export const useDnsAnalysisStare = defineStore('dnsAnalysis', () => {
|
||||
// 状态定义
|
||||
const dnsRecords = ref<DnsRecordItem[]>([]) /** 解析记录列表 */
|
||||
const currentDomainType = ref<number>(1) /** 当前域名类型:1=宝塔内部域名,2=外部域名 */
|
||||
const recordListParams = ref<GetDnsRecordListRequest>({
|
||||
domain_id: 0,
|
||||
domain_type: 1,
|
||||
searchKey: 'record',
|
||||
searchValue: '',
|
||||
row: 10,
|
||||
p: 1,
|
||||
})
|
||||
const searchKeyOptions = ref<SelectOption[]>([
|
||||
{
|
||||
label: '主机记录',
|
||||
value: 'record',
|
||||
},
|
||||
{
|
||||
label: '解析值',
|
||||
value: 'value',
|
||||
},
|
||||
{
|
||||
label: '记录类型',
|
||||
value: 'type',
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
value: 'remark',
|
||||
},
|
||||
])
|
||||
const isLoading = ref<boolean>(false) /** 加载状态 */
|
||||
const currentDomainId = ref<number>(0) // 当前域名ID
|
||||
// Cascader组件选项类型
|
||||
|
||||
const viewsOptions = ref<CascaderOption[]>([])
|
||||
const recordTypesOptions = ref<SelectOption[]>([])
|
||||
|
||||
// 分页配置
|
||||
const pagination = ref<PaginationProps>({
|
||||
page: 1,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
prefix: ({ itemCount }) => `共 ${itemCount} 条`,
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const hasRecords = computed(() => dnsRecords.value.length > 0)
|
||||
const recordCount = computed(() => dnsRecords.value.length)
|
||||
|
||||
/**
|
||||
* 设置当前域名ID
|
||||
* @param id 域名ID
|
||||
*/
|
||||
const setCurrentDomainId = (id: number): void => {
|
||||
currentDomainId.value = id
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前域名类型
|
||||
* @param type 域名类型:1=宝塔内部域名,2=外部域名
|
||||
*/
|
||||
const setCurrentDomainType = (type: number): void => {
|
||||
currentDomainType.value = type
|
||||
recordListParams.value.domain_type = type
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取DNS记录列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
const fetchRecords = async (params?: GetDnsRecordListRequest) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { fetch, data } = getDnsRecordList({
|
||||
...recordListParams.value,
|
||||
...(params || {}),
|
||||
...{
|
||||
domain_id: currentDomainId.value,
|
||||
domain_type: currentDomainType.value,
|
||||
},
|
||||
})
|
||||
await fetch()
|
||||
const {
|
||||
data: { data: recordList, count },
|
||||
} = data.value
|
||||
dnsRecords.value = recordList || []
|
||||
pagination.value.itemCount = count
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
dnsRecords.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取线路列表
|
||||
* @description 转换为Cascader组件所需的数据格式
|
||||
*/
|
||||
const fetchViews = async (): Promise<void> => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { fetch, data } = getViews()
|
||||
await fetch()
|
||||
const { data: viewList } = data.value
|
||||
|
||||
// 转换为Cascader组件所需的数据格式
|
||||
const transformToCascaderOptions = (viewsOptions: DnsView[]): CascaderOption[] => {
|
||||
return viewsOptions.map((view) => ({
|
||||
label: view.name,
|
||||
value: view.viewId,
|
||||
disabled: !view.free,
|
||||
children: view.children && view.children.length > 0 ? transformToCascaderOptions(view.children) : undefined,
|
||||
}))
|
||||
}
|
||||
viewsOptions.value = transformToCascaderOptions(viewList) || []
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
viewsOptions.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解析记录类型列表
|
||||
*/
|
||||
const fetchRecordTypes = async (): Promise<void> => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { fetch, data } = getRecordTypeList()
|
||||
await fetch()
|
||||
const { data: typeList } = data.value
|
||||
recordTypesOptions.value =
|
||||
typeList.map((item: DnsRecordType) => ({
|
||||
label: item.type,
|
||||
value: item.type,
|
||||
desc: item.desc,
|
||||
})) || []
|
||||
console.log(recordTypesOptions.value)
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
recordTypesOptions.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建DNS记录
|
||||
* @param params 创建参数
|
||||
* @returns 是否创建成功
|
||||
*/
|
||||
const createRecord = async (params: CreateDnsRecordRequest) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { fetch, data, message } = createDnsRecord({
|
||||
...params,
|
||||
domain_type: currentDomainType.value,
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
return data.value
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新DNS记录
|
||||
* @param params 更新参数
|
||||
* @returns 是否更新成功
|
||||
*/
|
||||
const updateRecord = async (params: UpdateDnsRecordRequest) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { fetch, data, message } = updateDnsRecord({
|
||||
...params,
|
||||
domain_type: currentDomainType.value,
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
return data.value
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除DNS记录
|
||||
* @param params 删除参数
|
||||
* @returns 是否删除成功
|
||||
*/
|
||||
const deleteRecord = async (params: DeleteDnsRecordRequest) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { fetch, data, message } = deleteDnsRecord({
|
||||
...params,
|
||||
domain_type: currentDomainType.value,
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
return data.value
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停DNS解析记录
|
||||
* @param params 暂停参数
|
||||
* @returns 是否暂停成功
|
||||
*/
|
||||
const pauseRecord = async (params: ToggleDnsRecordRequest) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { fetch, data, message } = pauseDnsRecord({
|
||||
...params,
|
||||
domain_type: currentDomainType.value,
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
return data.value
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用DNS解析记录
|
||||
* @param params 启用参数
|
||||
* @returns 是否启用成功
|
||||
*/
|
||||
const startRecord = async (params: ToggleDnsRecordRequest) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { fetch, data, message } = startDnsRecord({
|
||||
...params,
|
||||
domain_type: currentDomainType.value,
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
return data.value
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回暴露的状态和方法
|
||||
return {
|
||||
// 状态
|
||||
dnsRecords,
|
||||
isLoading,
|
||||
pagination,
|
||||
recordListParams,
|
||||
searchKeyOptions,
|
||||
viewsOptions,
|
||||
recordTypesOptions,
|
||||
currentDomainId,
|
||||
|
||||
// 计算属性
|
||||
hasRecords,
|
||||
recordCount,
|
||||
|
||||
// 操作方法
|
||||
fetchRecords,
|
||||
fetchViews,
|
||||
fetchRecordTypes,
|
||||
createRecord,
|
||||
updateRecord,
|
||||
deleteRecord,
|
||||
pauseRecord,
|
||||
startRecord,
|
||||
setCurrentDomainId,
|
||||
setCurrentDomainType,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取DNS分析Store实例及其状态
|
||||
* @returns Store实例和响应式状态
|
||||
*/
|
||||
export const useDnsAnalysisStore = () => {
|
||||
const store = useDnsAnalysisStare()
|
||||
return {
|
||||
...store,
|
||||
...storeToRefs(store),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* 域名解析详情页面
|
||||
* 职责:展示指定域名的DNS解析记录列表和管理功能
|
||||
*/
|
||||
|
||||
import { defineComponent, onMounted, ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { NCard, NButton, NIcon, NFlex, NText, NSpin } from 'naive-ui'
|
||||
import { ArrowBackOutline, ReloadOutline } from '@vicons/ionicons5'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { useDomainResolveState } from '../useStore'
|
||||
|
||||
// 引入DNS解析组件
|
||||
import DnsAnalysisComponent from './DnsAnalysis/index'
|
||||
|
||||
/**
|
||||
* 域名解析详情页面组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'DomainResolveDetail',
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const { handleError } = useError()
|
||||
const { selectedDomainInfo, clearSelectedDomainInfo } = useDomainResolveState()
|
||||
|
||||
// 获取域名ID参数
|
||||
const domainId = Number(route.params.id)
|
||||
|
||||
// 跨组件初始化流程
|
||||
// 1. 判断 route.query.domain_type 是否存在,有的话就按照传的来获取值,没用就默认1
|
||||
const domainType = computed(() => {
|
||||
if (route.query.domain_type) {
|
||||
return Number(route.query.domain_type)
|
||||
}
|
||||
// 如果是从内部跳转,使用Store中的信息
|
||||
if (selectedDomainInfo.value) {
|
||||
return selectedDomainInfo.value.domain_type
|
||||
}
|
||||
return 1 // 默认值
|
||||
})
|
||||
|
||||
// 2. 判断 route.query.domain_name 是否存在,有的话就获取值,没用的话默认空
|
||||
const domainName = computed(() => {
|
||||
if (route.query.domain_name) {
|
||||
return route.query.domain_name as string
|
||||
}
|
||||
// 如果是从内部跳转,使用Store中的信息
|
||||
if (selectedDomainInfo.value) {
|
||||
return selectedDomainInfo.value.name
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
// 返回到域名解析列表页面
|
||||
const handleBack = () => {
|
||||
// 清除Store中的选中域名信息
|
||||
clearSelectedDomainInfo()
|
||||
router.push('/domain-resolve')
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class="domain-resolve-detail-container">
|
||||
{/* 返回导航区 */}
|
||||
<div class="mb-4 flex items-center">
|
||||
<NButton
|
||||
circle
|
||||
size="medium"
|
||||
onClick={handleBack}
|
||||
class="hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<NIcon size="16">
|
||||
<ArrowBackOutline />
|
||||
</NIcon>
|
||||
</NButton>
|
||||
<NFlex align="center" class="ml-4">
|
||||
<NText class="text-lg font-semibold text-gray-900">域名解析</NText>
|
||||
{domainName.value && (
|
||||
<>
|
||||
<NText class="text-gray-400">-</NText>
|
||||
<NText class="text-sm text-gray-500">{domainName.value}</NText>
|
||||
</>
|
||||
)}
|
||||
</NFlex>
|
||||
</div>
|
||||
|
||||
{/* DNS解析记录内容区 */}
|
||||
<NCard class="card-shadow" bordered={false}>
|
||||
{domainId > 0 && (
|
||||
<DnsAnalysisComponent domainId={domainId} domainType={domainType.value} />
|
||||
)}
|
||||
</NCard>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* 域名解析页面
|
||||
* 职责:渲染域名解析界面,展示筛选、列表与分页
|
||||
*/
|
||||
|
||||
import { defineComponent, onMounted, ref } from 'vue'
|
||||
import { NCard, NFlex, NButton, NDivider, NIcon } from 'naive-ui'
|
||||
import { CloseOutline, SearchOutline } from '@vicons/ionicons5'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useController } from './useController'
|
||||
|
||||
/**
|
||||
* 域名解析页面组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'DomainResolve',
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const { loading, isMobile, ResolveTable, ResolveTablePage, ResolveCardList, tableData, FilterForm, formFetchSearch, handleShowAddDomainModal, domainName } =
|
||||
useController()
|
||||
|
||||
// 移动端搜索表单显示状态
|
||||
const showSearchForm = ref(false)
|
||||
|
||||
// 判断是否为子路由(详情页)
|
||||
const isDetailPage = () => route.matched.some(record => record.name === 'DomainResolveDetail')
|
||||
|
||||
// 切换搜索表单显示状态
|
||||
const toggleSearchForm = () => {
|
||||
showSearchForm.value = !showSearchForm.value
|
||||
}
|
||||
|
||||
// 渲染筛选搜索区域
|
||||
const renderFilterSection = () => (
|
||||
<NCard class="card-shadow" bordered={false}>
|
||||
{isMobile.value ? (
|
||||
<NFlex vertical class="w-full" size="medium">
|
||||
<NFlex justify="space-between" align="center">
|
||||
<NButton type="primary" onClick={handleShowAddDomainModal}>
|
||||
添加域名
|
||||
</NButton>
|
||||
<NButton onClick={toggleSearchForm} class="search-toggle-btn">
|
||||
{showSearchForm.value ? (
|
||||
<>
|
||||
<NIcon size="18">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
<span>关闭</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NIcon size="18">
|
||||
<SearchOutline />
|
||||
</NIcon>
|
||||
<span>搜索</span>
|
||||
</>
|
||||
)}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
{showSearchForm.value && (
|
||||
<>
|
||||
<NDivider class="!my-2" dashed />
|
||||
<div class="mobile-search-form">
|
||||
<FilterForm inline={false} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NFlex>
|
||||
) : (
|
||||
<NFlex justify="space-between" align="center" class="w-full">
|
||||
<NFlex class="w-full !flex-row !flex-wrap" size="medium">
|
||||
<NButton type="primary" onClick={handleShowAddDomainModal}>
|
||||
添加域名
|
||||
</NButton>
|
||||
<FilterForm class="flex-1 justify-end" inline={true} />
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
)}
|
||||
</NCard>
|
||||
)
|
||||
|
||||
// 渲染解析记录列表
|
||||
const renderResolveList = () => (
|
||||
<>
|
||||
{isMobile.value ? (
|
||||
<NFlex vertical>
|
||||
<ResolveCardList data={tableData.value?.list || []} loading={loading.value} class="mb-4" />
|
||||
<NFlex justify="center">
|
||||
<ResolveTablePage />
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
) : (
|
||||
<NCard class="card-shadow" bordered={false}>
|
||||
<ResolveTable loading={loading.value} class="mb-4" />
|
||||
<NFlex justify="end">
|
||||
<ResolveTablePage />
|
||||
</NFlex>
|
||||
</NCard>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
// 只在列表页面时执行数据加载
|
||||
if (!isDetailPage()) {
|
||||
formFetchSearch()
|
||||
}
|
||||
})
|
||||
|
||||
// 主渲染
|
||||
return () => (
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
{isDetailPage() ? (
|
||||
// 显示子路由内容(详情页)
|
||||
<router-view />
|
||||
) : (
|
||||
// 显示列表页内容
|
||||
<>
|
||||
{renderFilterSection()}
|
||||
{renderResolveList()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
134
frontend/apps/domain-management-backend/src/views/domain-resolve/types.d.ts
vendored
Normal file
134
frontend/apps/domain-management-backend/src/views/domain-resolve/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* 域名解析页面类型定义
|
||||
* 包含解析记录列表页面的状态接口、表单数据等类型
|
||||
*/
|
||||
|
||||
/**
|
||||
* 标签类型(NTag组件使用)
|
||||
*/
|
||||
export type TagType = "default" | "success" | "warning" | "error" | "info";
|
||||
|
||||
/**
|
||||
* NS状态枚举
|
||||
*/
|
||||
export enum NsStatus {
|
||||
/** 未设置 */
|
||||
NotSet = 0,
|
||||
/** 已生效 */
|
||||
Active = 1,
|
||||
/** 未生效 */
|
||||
Pending = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* 域名类型枚举
|
||||
*/
|
||||
export enum DomainType {
|
||||
/** 平台注册域名 */
|
||||
Platform = 1,
|
||||
/** 外部添加域名 */
|
||||
External = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* 域名来源枚举
|
||||
*/
|
||||
export enum DomainSource {
|
||||
/** 平台注册 */
|
||||
Platform = "platform",
|
||||
/** 外部添加 */
|
||||
External = "external",
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析记录项接口
|
||||
*/
|
||||
export interface ResolveItem {
|
||||
/** 创建时间 */
|
||||
created_at: string;
|
||||
/** 51DNS中的域名ID,0表示未在51DNS中创建 */
|
||||
dns_id: number;
|
||||
/** 域名类型:1=平台注册域名,2=外部添加域名 */
|
||||
domain_type: number;
|
||||
/** 完整域名 */
|
||||
full_domain: string;
|
||||
/** 最后DNS状态检测时间 */
|
||||
last_check_time: string | null;
|
||||
/** 本地数据库中的域名ID */
|
||||
local_id: number;
|
||||
/** NS状态:0=未设置,1=已生效,2=未生效 */
|
||||
ns_status: number;
|
||||
/** DNS解析记录数量 */
|
||||
record_count: number;
|
||||
/** 备注信息 */
|
||||
remark: string;
|
||||
/** 域名来源:platform=平台注册,external=外部添加 */
|
||||
source: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析记录列表请求参数
|
||||
*/
|
||||
export interface ResolveListRequest {
|
||||
/** 页码 */
|
||||
p?: number;
|
||||
/** 每页数量 */
|
||||
rows?: number;
|
||||
/** 搜索关键词 */
|
||||
keyword?: string;
|
||||
/** 状态筛选 */
|
||||
status?: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析记录状态选项接口
|
||||
*/
|
||||
export interface StatusOption {
|
||||
/** 选项文本 */
|
||||
label: string;
|
||||
/** 选项值 */
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析记录状态配置接口
|
||||
*/
|
||||
export interface ResolveStatusConfig {
|
||||
/** 标签类型 */
|
||||
type: TagType;
|
||||
/** 显示文本 */
|
||||
text: string;
|
||||
/** 显示颜色 */
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 域名添加表单数据接口
|
||||
*/
|
||||
export interface DomainAddForm {
|
||||
/** 域名 */
|
||||
domain: string;
|
||||
/** 备注 */
|
||||
remark: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DNS记录表单数据
|
||||
*/
|
||||
export interface DnsRecordForm {
|
||||
record_id?: string | number;
|
||||
/** 主机记录 */
|
||||
record: string;
|
||||
/** 记录类型 */
|
||||
type: string;
|
||||
/** 记录值 */
|
||||
value: string;
|
||||
/** MX值 */
|
||||
mx: number;
|
||||
/** TTL值 */
|
||||
ttl: number;
|
||||
/** 备注 */
|
||||
remark: string;
|
||||
/** 线路ID */
|
||||
viewId: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,819 @@
|
|||
/**
|
||||
* 域名解析页面控制器
|
||||
* 负责处理业务逻辑、事件响应和生命周期管理
|
||||
*/
|
||||
|
||||
import { ref, reactive, onMounted, watch, computed, defineComponent } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
NButton,
|
||||
NTag,
|
||||
NSpace,
|
||||
NCard,
|
||||
NFlex,
|
||||
NAlert,
|
||||
NText,
|
||||
NIcon,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
useDialog,
|
||||
} from 'naive-ui'
|
||||
import { CheckmarkOutline } from '@vicons/ionicons5'
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
import { useDomainResolveState } from './useStore'
|
||||
import {
|
||||
useTable,
|
||||
useForm,
|
||||
useFormHooks,
|
||||
useModal,
|
||||
useModalHooks,
|
||||
useMessage,
|
||||
useLoadingMask,
|
||||
} from '@baota/naive-ui/hooks'
|
||||
import { useApp } from '@/components/layout/useStore'
|
||||
import { addExternalDomain, removeDomain } from '@/api/resolve'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
|
||||
const message = useMessage()
|
||||
const { handleError } = useError()
|
||||
|
||||
import type { ResolveListRequest, ResolveItem, DomainAddForm } from './types.d'
|
||||
import { TableColumns } from 'naive-ui/es/data-table/src/interface'
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
|
||||
// 定义标签类型
|
||||
type TagType = 'default' | 'success' | 'warning' | 'error' | 'info'
|
||||
|
||||
/**
|
||||
* DNS设置提示模态框组件
|
||||
*/
|
||||
const createDnsSetupModal = (fetchResolve: () => Promise<void>) =>
|
||||
defineComponent({
|
||||
name: 'DnsSetupModal',
|
||||
props: {
|
||||
domain: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
domainId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
domainType: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
lastCheckTime: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { close } = useModalHooks()
|
||||
const closeModal = close()
|
||||
const message = useMessage()
|
||||
const { checkDomainStatusApi } = useDomainResolveState()
|
||||
const redetectLoading = ref(false)
|
||||
|
||||
// 计算是否在半小时内检测过
|
||||
const isRedetectDisabled = computed(() => {
|
||||
if (!props.lastCheckTime) {
|
||||
return false // 如果没有检测时间,说明没有检测过,可以检测
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析时间字符串,支持多种格式
|
||||
let lastCheckTime: number
|
||||
|
||||
// 尝试直接解析
|
||||
const directParse = new Date(props.lastCheckTime).getTime()
|
||||
if (!isNaN(directParse)) {
|
||||
lastCheckTime = directParse
|
||||
} else {
|
||||
// 如果直接解析失败,尝试手动解析 "2025-09-12 16:49:37" 格式
|
||||
const timeStr = props.lastCheckTime.replace(/-/g, '/') // 将 - 替换为 / 以提高兼容性
|
||||
lastCheckTime = new Date(timeStr).getTime()
|
||||
}
|
||||
|
||||
const currentTime = new Date().getTime()
|
||||
const halfHour = 30 * 60 * 1000 // 30分钟的毫秒数
|
||||
|
||||
// 检查时间是否有效
|
||||
if (isNaN(lastCheckTime)) {
|
||||
console.warn('Invalid lastCheckTime format:', props.lastCheckTime)
|
||||
return false
|
||||
}
|
||||
|
||||
const timeDiff = currentTime - lastCheckTime
|
||||
console.log('Time calculation:', {
|
||||
lastCheckTime: props.lastCheckTime,
|
||||
lastCheckTimeMs: lastCheckTime,
|
||||
lastCheckTimeDate: new Date(lastCheckTime).toLocaleString(),
|
||||
currentTimeMs: currentTime,
|
||||
currentTimeDate: new Date(currentTime).toLocaleString(),
|
||||
timeDiffMs: timeDiff,
|
||||
timeDiffMinutes: Math.round(timeDiff / (60 * 1000)),
|
||||
halfHourMs: halfHour,
|
||||
halfHourMinutes: 30,
|
||||
isDisabled: timeDiff < halfHour,
|
||||
})
|
||||
|
||||
return timeDiff < halfHour
|
||||
} catch (error) {
|
||||
console.error('Error parsing lastCheckTime:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// 计算剩余时间(分钟)
|
||||
const remainingMinutes = computed(() => {
|
||||
if (!props.lastCheckTime || !isRedetectDisabled.value) {
|
||||
return 0
|
||||
}
|
||||
|
||||
try {
|
||||
const lastCheckTime = new Date(props.lastCheckTime).getTime()
|
||||
const currentTime = new Date().getTime()
|
||||
const halfHour = 30 * 60 * 1000
|
||||
const timeDiff = currentTime - lastCheckTime
|
||||
const remaining = halfHour - timeDiff
|
||||
|
||||
const minutes = Math.ceil(remaining / (60 * 1000)) // 转换为分钟并向上取整
|
||||
console.log('Remaining time calculation:', {
|
||||
remainingMs: remaining,
|
||||
remainingMinutes: minutes,
|
||||
})
|
||||
|
||||
return Math.max(0, minutes) // 确保不返回负数
|
||||
} catch (error) {
|
||||
console.error('Error calculating remaining time:', error)
|
||||
return 0
|
||||
}
|
||||
})
|
||||
const handleRedetect = async () => {
|
||||
try {
|
||||
redetectLoading.value = true
|
||||
const result = await checkDomainStatusApi(props.domainId, props.domainType)
|
||||
if (!result?.status) {
|
||||
return message.error(result?.msg || '检测失败,请稍后重试')
|
||||
}
|
||||
await fetchResolve()
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
message.error(error as string)
|
||||
} finally {
|
||||
redetectLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class="max-w-2xl">
|
||||
<div class="space-y-6">
|
||||
<NAlert type="warning" showIcon>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">域名待生效</span>
|
||||
</div>
|
||||
<div class="mt-2 text-sm">
|
||||
域名 <span class="font-bold">{props.domain}</span> 当前状态为待生效,请按以下步骤设置DNS服务器:
|
||||
</div>
|
||||
</NAlert>
|
||||
|
||||
<NAlert type="info" showIcon>
|
||||
<div class="font-medium mb-3">DNS服务器设置指引</div>
|
||||
<div class="text-sm space-y-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-blue-600 font-medium min-w-[20px]">1.</span>
|
||||
<span>登录您的域名注册商管理面板</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-blue-600 font-medium min-w-[20px]">2.</span>
|
||||
<span>找到DNS设置或域名服务器设置</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-blue-600 font-medium min-w-[20px]">3.</span>
|
||||
<div class="flex-1">
|
||||
<div class="mb-2">将域名服务器修改为以下地址:</div>
|
||||
<div class="bg-gray-50 p-3 rounded-md font-mono text-sm space-y-1">
|
||||
<div class="text-gray-600">ns1.baotadns.com</div>
|
||||
<div class="text-gray-600">ns2.baotadns.com</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-blue-600 font-medium min-w-[20px]">4.</span>
|
||||
<span>保存设置并等待生效(通常需要24-48小时)</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-blue-600 font-medium min-w-[20px]">5.</span>
|
||||
<span>点击检测按钮验证设置是否生效</span>
|
||||
</div>
|
||||
</div>
|
||||
</NAlert>
|
||||
|
||||
<NAlert type="success" showIcon>
|
||||
<div class="font-medium mb-2">设置完成后</div>
|
||||
<div class="flex items-start gap-2 text-sm">
|
||||
<NIcon class="text-green-500 mt-0.5" size="16">
|
||||
<CheckmarkOutline />
|
||||
</NIcon>
|
||||
<span>DNS服务器设置完成后,您可以点击下方的"重新检测"按钮来检测域名状态是否已更新。</span>
|
||||
</div>
|
||||
</NAlert>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-4 pt-4 border-t border-gray-100">
|
||||
<NButton onClick={closeModal} disabled={redetectLoading.value}>
|
||||
关闭
|
||||
</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
loading={redetectLoading.value}
|
||||
disabled={redetectLoading.value || isRedetectDisabled.value}
|
||||
onClick={handleRedetect}
|
||||
>
|
||||
{redetectLoading.value
|
||||
? '检测中...'
|
||||
: isRedetectDisabled.value
|
||||
? `重新检测(${remainingMinutes.value}分钟后可用)`
|
||||
: '重新检测'}
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* 域名解析页面控制器
|
||||
*/
|
||||
export function useController() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 获取从URL传递的域名名称
|
||||
const domainName = ref<string>((route.query.domain_name as string) || '')
|
||||
|
||||
const {
|
||||
fetchResolveListData,
|
||||
filterFormData,
|
||||
statusOptions,
|
||||
NS_STATUS_CONFIG,
|
||||
NS_STATUS_MAP,
|
||||
DOMAIN_TYPE_CONFIG,
|
||||
DOMAIN_TYPE_MAP,
|
||||
setSelectedDomainInfo,
|
||||
} = useDomainResolveState()
|
||||
|
||||
const { useFormInput, useFormSelect } = useFormHooks()
|
||||
const dialog = useDialog()
|
||||
// 获取移动端状态
|
||||
const { isMobile } = useApp()
|
||||
|
||||
// -------------------- 事件处理方法 --------------------
|
||||
|
||||
/**
|
||||
* 添加域名表单配置
|
||||
*/
|
||||
const addDomainFormConfig = [
|
||||
useFormInput(
|
||||
'域名',
|
||||
'domain',
|
||||
{
|
||||
placeholder: '请输入域名,如:example.com',
|
||||
clearable: true,
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
showFeedback: true,
|
||||
labelWidth: '80px',
|
||||
rule: {
|
||||
required: true,
|
||||
message: '请输入域名',
|
||||
trigger: ['blur', 'input'],
|
||||
},
|
||||
},
|
||||
),
|
||||
useFormInput(
|
||||
'备注',
|
||||
'remark',
|
||||
{
|
||||
placeholder: '请输入备注信息(可选)',
|
||||
clearable: true,
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
showFeedback: false,
|
||||
labelWidth: '80px',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
/**
|
||||
* 添加域名
|
||||
*/
|
||||
|
||||
async function handleAddDomainSubmit(data: DomainAddForm): Promise<void> {
|
||||
const { open: openLoad, close: closeLoad } = useLoadingMask({ text: '正在添加域名,请稍后...' })
|
||||
openLoad()
|
||||
try {
|
||||
const {
|
||||
fetch,
|
||||
message,
|
||||
data: addDomainData,
|
||||
} = addExternalDomain({
|
||||
full_domain: data.domain.trim(),
|
||||
remark: data.remark?.trim(),
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
if (!addDomainData.value?.status) {
|
||||
useMessage().error(addDomainData.value?.msg || '添加域名失败')
|
||||
return
|
||||
}
|
||||
await fetchResolve()
|
||||
closeAddDomainModal()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
closeLoad()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理解析按钮点击
|
||||
*/
|
||||
const handleResolve = (row: ResolveItem) => {
|
||||
setSelectedDomainInfo({
|
||||
id: row.local_id,
|
||||
name: row.full_domain,
|
||||
domain_type: row.domain_type,
|
||||
})
|
||||
router.push({
|
||||
path: `/domain-resolve/detail/${row.local_id}`,
|
||||
query: {
|
||||
domain_name: row.full_domain,
|
||||
domain_type: row.domain_type,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理删除按钮点击
|
||||
*/
|
||||
const handleDelete = (row: ResolveItem) => {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: `确定要删除域名"${row.full_domain}"吗?删除后无法恢复。`,
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
const { open: openLoad, close: closeLoad } = useLoadingMask({ text: '正在删除域名,请稍后...' })
|
||||
openLoad()
|
||||
try {
|
||||
const { fetch, message } = removeDomain({
|
||||
domain_id: row.local_id,
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
await fetchResolve()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
closeLoad()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态点击(仅未生效状态)
|
||||
*/
|
||||
const handleStatusClick = (row: ResolveItem) => {
|
||||
if (row.ns_status === 2) {
|
||||
// 未生效状态
|
||||
handleShowDnsModal(row.full_domain?.trim(), row.local_id, row.domain_type, row.last_check_time || undefined)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示DNS设置模态框
|
||||
*/
|
||||
const handleShowDnsModal = (domain: string, domainId: number, domainType: number, lastCheckTime?: string) => {
|
||||
const DnsSetupModalComponent = createDnsSetupModal(fetchResolve)
|
||||
useModal({
|
||||
title: 'DNS设置',
|
||||
area: '600px',
|
||||
component: DnsSetupModalComponent,
|
||||
componentProps: {
|
||||
domain,
|
||||
domainId,
|
||||
domainType,
|
||||
lastCheckTime,
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加域名模态框
|
||||
*/
|
||||
const handleShowAddDomainModal = () => {
|
||||
addDomainModalRef.value = useModal({
|
||||
title: '添加域名',
|
||||
area: '500px',
|
||||
component: AddDomainFormWrapper,
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单配置
|
||||
*/
|
||||
const formConfig = () => [
|
||||
// useFormSelect(
|
||||
// '',
|
||||
// 'status',
|
||||
// statusOptions.value,
|
||||
// {
|
||||
// placeholder: '全部状态',
|
||||
// class: 'w-28',
|
||||
// },
|
||||
// { showLabel: false, showFeedback: false },
|
||||
// ),
|
||||
useFormInput(
|
||||
'',
|
||||
'keyword',
|
||||
{
|
||||
placeholder: '请输入域名',
|
||||
clearable: true,
|
||||
class: 'w-64',
|
||||
},
|
||||
{ showLabel: false, showFeedback: false },
|
||||
),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => (
|
||||
<NSpace>
|
||||
<NButton type="primary" onClick={() => formFetchSearch()}>
|
||||
搜索
|
||||
</NButton>
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 表格列配置
|
||||
*/
|
||||
const createColumns = [
|
||||
{
|
||||
title: '域名',
|
||||
key: 'full_domain',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row: ResolveItem) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="i-mdi-earth text-emerald-500" />
|
||||
<div class="font-medium">{row.full_domain?.trim()}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'NS状态',
|
||||
key: 'ns_status',
|
||||
width: 100,
|
||||
render: (row: ResolveItem) => {
|
||||
const isPending = row.ns_status === 2
|
||||
return isPending ? (
|
||||
<NButton
|
||||
text
|
||||
type="warning"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
handleShowDnsModal(
|
||||
row.full_domain?.trim(),
|
||||
row.local_id,
|
||||
row.domain_type,
|
||||
row.last_check_time || undefined,
|
||||
)
|
||||
}
|
||||
class="!p-0 !h-auto"
|
||||
>
|
||||
<NTag type="warning" bordered={false} size="small" class="cursor-pointer">
|
||||
{getNsStatusText(row.ns_status)}
|
||||
</NTag>
|
||||
</NButton>
|
||||
) : (
|
||||
<NTag type={getNsStatusType(row.ns_status)} bordered={false} size="small">
|
||||
{getNsStatusText(row.ns_status)}
|
||||
</NTag>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '购买来源',
|
||||
key: 'domain_type',
|
||||
width: 120,
|
||||
render: (row: ResolveItem) => {
|
||||
const typeKey = DOMAIN_TYPE_MAP[row.domain_type] || 'platform'
|
||||
const config = DOMAIN_TYPE_CONFIG[typeKey]
|
||||
return <span class="text-gray-700">{config.text}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '解析记录数',
|
||||
key: 'record_count',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (row: ResolveItem) => <span class="font-medium">{row.record_count}</span>,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'remark',
|
||||
width: 150,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row: ResolveItem) => <span class="text-gray-600">{row.remark || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created_at',
|
||||
width: 150,
|
||||
render: (row: ResolveItem) => {
|
||||
const date = new Date(row.created_at)
|
||||
return formatDate(date.getTime(), 'yyyy-MM-dd HH:mm')
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
render: (row: ResolveItem) => (
|
||||
<NSpace justify="end">
|
||||
<NButton size="small" type="primary" ghost onClick={() => handleResolve(row)}>
|
||||
解析
|
||||
</NButton>
|
||||
{row.domain_type === 2 && (
|
||||
<NButton size="small" type="error" ghost onClick={() => handleDelete(row)}>
|
||||
删除
|
||||
</NButton>
|
||||
)}
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
] as TableColumns<ResolveItem>
|
||||
|
||||
/**
|
||||
* 移动端卡片组件
|
||||
*/
|
||||
const ResolveCardList = defineComponent({
|
||||
name: 'ResolveCardList',
|
||||
props: {
|
||||
data: {
|
||||
type: Array as PropType<ResolveItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<NFlex vertical size="medium">
|
||||
{props.data.map((item: ResolveItem) => (
|
||||
<NCard key={item.local_id} class="card-shadow" bordered={false}>
|
||||
<NFlex vertical size="small">
|
||||
<NFlex align="center" justify="space-between">
|
||||
<NFlex align="center" size="small">
|
||||
<div>
|
||||
<div class="font-medium text-base">{item.full_domain?.trim()}</div>
|
||||
<div class="text-sm text-gray-500">{item.remark || '-'}</div>
|
||||
</div>
|
||||
</NFlex>
|
||||
<NFlex align="center" size="small">
|
||||
{item.ns_status === 2 ? (
|
||||
<NButton
|
||||
text
|
||||
type="warning"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
handleShowDnsModal(
|
||||
item.full_domain?.trim(),
|
||||
item.local_id,
|
||||
item.domain_type,
|
||||
item.last_check_time || undefined,
|
||||
)
|
||||
}
|
||||
class="!p-0 !h-auto"
|
||||
>
|
||||
<NTag type="warning" bordered={false} size="small" class="cursor-pointer">
|
||||
{getNsStatusText(item.ns_status)}
|
||||
</NTag>
|
||||
</NButton>
|
||||
) : (
|
||||
<NTag type={getNsStatusType(item.ns_status)} bordered={false} size="small">
|
||||
{getNsStatusText(item.ns_status)}
|
||||
</NTag>
|
||||
)}
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
|
||||
<NFlex justify="space-between" class="text-sm">
|
||||
<span class="text-gray-500">解析记录数:</span>
|
||||
<span class="font-medium text-blue-600">{item.record_count}</span>
|
||||
</NFlex>
|
||||
|
||||
<NFlex justify="space-between" class="text-sm text-gray-600">
|
||||
<div>
|
||||
<span class="text-gray-500">创建:</span>
|
||||
{formatDate(new Date(item.created_at).getTime(), 'yyyy-MM-dd')}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">类型:</span>
|
||||
{DOMAIN_TYPE_CONFIG[DOMAIN_TYPE_MAP[item.domain_type] || 'platform']?.text}
|
||||
</div>
|
||||
</NFlex>
|
||||
|
||||
<NFlex justify="end" size="small">
|
||||
<NButton size="small" type="primary" ghost onClick={() => handleResolve(item)}>
|
||||
解析
|
||||
</NButton>
|
||||
{item.domain_type === 2 && (
|
||||
<NButton size="small" type="error" ghost onClick={() => handleDelete(item)}>
|
||||
删除
|
||||
</NButton>
|
||||
)}
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
))}
|
||||
</NFlex>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// 表格实例
|
||||
const {
|
||||
TableComponent: ResolveTable,
|
||||
PageComponent: ResolveTablePage,
|
||||
loading,
|
||||
param,
|
||||
fetch: fetchResolve,
|
||||
data: tableData,
|
||||
} = useTable<ResolveItem, ResolveListRequest>({
|
||||
config: createColumns,
|
||||
request: fetchResolveListData,
|
||||
defaultValue: filterFormData,
|
||||
alias: {
|
||||
page: 'p',
|
||||
pageSize: 'rows',
|
||||
},
|
||||
watchValue: ['p', 'rows', 'status'],
|
||||
})
|
||||
|
||||
const { component: FilterForm } = useForm<ResolveListRequest>({
|
||||
config: formConfig(),
|
||||
defaultValue: filterFormData,
|
||||
})
|
||||
|
||||
const { component: AddDomainForm, fetch: submitAddDomain } = useForm<DomainAddForm>({
|
||||
config: addDomainFormConfig,
|
||||
defaultValue: { domain: '', remark: '' },
|
||||
request: handleAddDomainSubmit,
|
||||
})
|
||||
|
||||
const addDomainModalRef = ref<any>(null)
|
||||
|
||||
/**
|
||||
* 关闭添加域名模态框
|
||||
*/
|
||||
const closeAddDomainModal = () => {
|
||||
if (addDomainModalRef.value) {
|
||||
addDomainModalRef.value.close()
|
||||
addDomainModalRef.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加域名表单包装组件(包含DNS提示和提交按钮)
|
||||
*/
|
||||
const AddDomainFormWrapper = defineComponent({
|
||||
name: 'AddDomainFormWrapper',
|
||||
setup() {
|
||||
return () => (
|
||||
<div class="max-w-2xl">
|
||||
<AddDomainForm />
|
||||
|
||||
<NAlert type="info" showIcon class="mt-4">
|
||||
<div class="font-medium mb-2">DNS服务器设置</div>
|
||||
<div class="text-sm mb-3">添加域名后,您需要将DNS服务器修改为:</div>
|
||||
|
||||
<div class="bg-gray-50 p-3 rounded-md font-mono text-sm space-y-1">
|
||||
<div class="text-gray-600">ns1.baotadns.com</div>
|
||||
<div class="text-gray-600">ns2.baotadns.com</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-xs text-gray-500">
|
||||
请在您的域名注册商处将DNS服务器修改为上述地址,修改后生效时间通常为24-48小时。
|
||||
</div>
|
||||
</NAlert>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-4 pt-4 border-t border-gray-100">
|
||||
<NButton onClick={closeAddDomainModal}>取消</NButton>
|
||||
<NButton type="primary" onClick={() => submitAddDomain()}>
|
||||
确认添加
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const formFetchSearch = async () => {
|
||||
await fetchResolve()
|
||||
}
|
||||
|
||||
// 监听路由从其他页面返回时刷新列表
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath, oldPath) => {
|
||||
// 当路由从详情页返回到列表页时,刷新数据
|
||||
if (newPath === '/domain-resolve' && oldPath?.includes('/domain-resolve/detail/')) {
|
||||
fetchResolve()
|
||||
}
|
||||
},
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
// -------------------- 映射工具方法 --------------------
|
||||
|
||||
/**
|
||||
* 获取NS状态文本
|
||||
*/
|
||||
const getNsStatusText = (status: number | undefined): string => {
|
||||
const statusKey = String(status ?? 0) as unknown as keyof typeof NS_STATUS_MAP
|
||||
const key = NS_STATUS_MAP[statusKey] || 'notSet'
|
||||
return NS_STATUS_CONFIG[key]?.text || '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取NS状态类型
|
||||
*/
|
||||
const getNsStatusType = (status: number | undefined): TagType => {
|
||||
const statusKey = String(status ?? 0) as unknown as keyof typeof NS_STATUS_MAP
|
||||
const key = NS_STATUS_MAP[statusKey] || 'notSet'
|
||||
return NS_STATUS_CONFIG[key]?.type || 'default'
|
||||
}
|
||||
|
||||
// -------------------- 事件处理 --------------------
|
||||
|
||||
/**
|
||||
* 重置查询条件
|
||||
*/
|
||||
function formDataReset() {
|
||||
filterFormData.value = { keyword: '', status: '' }
|
||||
param.value.keyword = ''
|
||||
param.value.status = ''
|
||||
param.value.p = 1
|
||||
fetchResolve()
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
isMobile,
|
||||
tableData,
|
||||
domainName,
|
||||
|
||||
// 表格
|
||||
ResolveTable,
|
||||
ResolveTablePage,
|
||||
|
||||
// 移动端卡片
|
||||
ResolveCardList,
|
||||
|
||||
// 表单
|
||||
FilterForm,
|
||||
formFetchSearch,
|
||||
formDataReset,
|
||||
|
||||
// 事件处理
|
||||
handleResolve,
|
||||
handleDelete,
|
||||
handleStatusClick,
|
||||
|
||||
// 模态框
|
||||
handleShowAddDomainModal,
|
||||
handleShowDnsModal,
|
||||
|
||||
// 工具方法
|
||||
getNsStatusText,
|
||||
getNsStatusType,
|
||||
|
||||
// 数据刷新
|
||||
fetchResolve,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* 域名解析页面状态管理
|
||||
* 负责管理解析记录数据、筛选条件、分页等状态
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { getResolveList, checkDomainStatus } from '@/api/resolve'
|
||||
|
||||
import type { TableResponse } from '@baota/naive-ui/types/table'
|
||||
import type { ResolveItem, ResolveListRequest, StatusOption } from './types.d'
|
||||
|
||||
/** NS状态映射配置(后端数值 -> 前端联合类型) */
|
||||
const NS_STATUS_MAP: Record<number, "notSet" | "active" | "pending"> = {
|
||||
0: "notSet", // 未设置
|
||||
1: "active", // 已生效
|
||||
2: "pending", // 未生效
|
||||
} as const
|
||||
|
||||
/** NS状态显示配置 */
|
||||
const NS_STATUS_CONFIG = {
|
||||
notSet: {
|
||||
type: "default" as const,
|
||||
text: "未设置",
|
||||
color: "#909399", // 灰色
|
||||
},
|
||||
active: {
|
||||
type: "success" as const,
|
||||
text: "已生效",
|
||||
color: "#18a058", // 绿色
|
||||
},
|
||||
pending: {
|
||||
type: "warning" as const,
|
||||
text: "未生效",
|
||||
color: "#f0a020", // 橙色
|
||||
},
|
||||
} as const
|
||||
|
||||
/** 域名类型映射配置 */
|
||||
const DOMAIN_TYPE_MAP: Record<number, "platform" | "external"> = {
|
||||
1: "platform",
|
||||
2: "external",
|
||||
} as const
|
||||
|
||||
/** 域名类型显示配置 */
|
||||
const DOMAIN_TYPE_CONFIG = {
|
||||
platform: {
|
||||
type: 'info' as const,
|
||||
text: '堡塔',
|
||||
},
|
||||
external: {
|
||||
type: 'warning' as const,
|
||||
text: '外部',
|
||||
},
|
||||
} as const
|
||||
|
||||
const message = useMessage()
|
||||
const { handleError } = useError()
|
||||
|
||||
/**
|
||||
* 域名解析页面状态Store
|
||||
*/
|
||||
export const useDomainResolveStore = defineStore('domain-resolve-store', () => {
|
||||
// -------------------- 状态定义 --------------------
|
||||
|
||||
/** 页面加载状态 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** NS状态选项 */
|
||||
const statusOptions = ref<StatusOption[]>([
|
||||
{ label: '全部状态', value: '' },
|
||||
{ label: '已生效', value: 1 },
|
||||
{ label: '未生效', value: 2 },
|
||||
{ label: '未设置', value: 0 },
|
||||
])
|
||||
|
||||
/** 筛选表单数据 */
|
||||
const filterFormData = ref<ResolveListRequest>({
|
||||
p: 1,
|
||||
rows: 10,
|
||||
keyword: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
/** 选中的域名信息(用于内部跳转) */
|
||||
const selectedDomainInfo = ref<{
|
||||
id: number
|
||||
name: string
|
||||
domain_type: number
|
||||
} | null>(null)
|
||||
|
||||
// -------------------- 方法定义 --------------------
|
||||
|
||||
/**
|
||||
* 获取解析记录列表数据
|
||||
* @param params 查询参数
|
||||
*/
|
||||
const fetchResolveListData = async <T = ResolveItem>(
|
||||
params: ResolveListRequest = {}
|
||||
): Promise<TableResponse<T>> => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 处理参数:只有status有值时才传递该参数
|
||||
const requestParams = { ...params }
|
||||
if (requestParams.status === '' || requestParams.status === undefined) {
|
||||
delete requestParams.status
|
||||
}
|
||||
|
||||
const { fetch, data } = getResolveList(requestParams)
|
||||
await fetch()
|
||||
|
||||
if (data.value?.status) {
|
||||
const responseData = data.value?.data
|
||||
return {
|
||||
list: responseData.data as T[],
|
||||
total: responseData.total,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
message.error('加载解析记录列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
return { list: [] as T[], total: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选中的域名信息
|
||||
* @param info 域名信息
|
||||
*/
|
||||
const setSelectedDomainInfo = (info: { id: number; name: string; domain_type: number }) => {
|
||||
selectedDomainInfo.value = info
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除选中的域名信息
|
||||
*/
|
||||
const clearSelectedDomainInfo = () => {
|
||||
selectedDomainInfo.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测域名状态
|
||||
* @param domainId 域名ID
|
||||
* @param domainType 域名类型
|
||||
*/
|
||||
const checkDomainStatusApi = async (domainId: number, domainType?: number) => {
|
||||
try {
|
||||
const { fetch, data, message } = checkDomainStatus({
|
||||
domain_id: domainId,
|
||||
domain_type: domainType
|
||||
})
|
||||
message.value = true
|
||||
await fetch()
|
||||
if (data.value?.status) {
|
||||
return data.value
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 返回状态和方法
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
statusOptions,
|
||||
filterFormData,
|
||||
selectedDomainInfo,
|
||||
|
||||
// 方法
|
||||
fetchResolveListData,
|
||||
setSelectedDomainInfo,
|
||||
clearSelectedDomainInfo,
|
||||
checkDomainStatusApi,
|
||||
|
||||
// 常量
|
||||
NS_STATUS_CONFIG,
|
||||
NS_STATUS_MAP,
|
||||
DOMAIN_TYPE_CONFIG,
|
||||
DOMAIN_TYPE_MAP,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 导出Store实例
|
||||
*/
|
||||
export const useDomainResolveState = () => {
|
||||
const store = useDomainResolveStore()
|
||||
const storeRefs = storeToRefs(store)
|
||||
return {
|
||||
// 响应式状态
|
||||
...storeRefs,
|
||||
// 方法
|
||||
fetchResolveListData: store.fetchResolveListData,
|
||||
setSelectedDomainInfo: store.setSelectedDomainInfo,
|
||||
clearSelectedDomainInfo: store.clearSelectedDomainInfo,
|
||||
checkDomainStatusApi: store.checkDomainStatusApi,
|
||||
// 常量
|
||||
NS_STATUS_CONFIG: store.NS_STATUS_CONFIG,
|
||||
NS_STATUS_MAP: store.NS_STATUS_MAP,
|
||||
DOMAIN_TYPE_CONFIG: store.DOMAIN_TYPE_CONFIG,
|
||||
DOMAIN_TYPE_MAP: store.DOMAIN_TYPE_MAP,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* 域名安全页面
|
||||
* 职责:渲染域名安全界面,展示API密钥管理功能
|
||||
*/
|
||||
|
||||
import { defineComponent, onMounted, ref } from 'vue'
|
||||
import { NCard, NFlex, NButton, NDivider, NIcon } from 'naive-ui'
|
||||
import { CloseOutline, SearchOutline } from '@vicons/ionicons5'
|
||||
import { useController } from './useController'
|
||||
|
||||
/**
|
||||
* 域名安全页面组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'DomainSecurity',
|
||||
setup() {
|
||||
const {
|
||||
loading,
|
||||
isMobile,
|
||||
ApiKeyTable,
|
||||
ApiKeyTablePage,
|
||||
ApiKeyCardList,
|
||||
tableData,
|
||||
FilterForm,
|
||||
formFetchSearch,
|
||||
handleCreate,
|
||||
} = useController()
|
||||
|
||||
// 移动端搜索表单显示状态
|
||||
const showSearchForm = ref(false)
|
||||
|
||||
// 切换搜索表单显示状态
|
||||
const toggleSearchForm = () => {
|
||||
showSearchForm.value = !showSearchForm.value
|
||||
}
|
||||
|
||||
// 渲染筛选搜索区域
|
||||
const renderFilterSection = () => (
|
||||
<NCard class="card-shadow" bordered={false}>
|
||||
{isMobile.value ? (
|
||||
<NFlex vertical class="w-full" size="medium">
|
||||
<NFlex justify="space-between" align="center">
|
||||
<NButton type="primary" onClick={handleCreate}>
|
||||
创建API密钥
|
||||
</NButton>
|
||||
<NButton onClick={toggleSearchForm} class="search-toggle-btn">
|
||||
{showSearchForm.value ? (
|
||||
<>
|
||||
<NIcon size="18">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
<span>关闭</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NIcon size="18">
|
||||
<SearchOutline />
|
||||
</NIcon>
|
||||
<span>搜索</span>
|
||||
</>
|
||||
)}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
{showSearchForm.value && (
|
||||
<>
|
||||
<NDivider class="!my-2" dashed />
|
||||
<div class="mobile-search-form">
|
||||
<FilterForm inline={false} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NFlex>
|
||||
) : (
|
||||
<NFlex justify="space-between" align="center" class="w-full">
|
||||
<NFlex class="w-full !flex-row !flex-wrap" size="medium">
|
||||
<NButton type="primary" onClick={handleCreate}>
|
||||
创建API密钥
|
||||
</NButton>
|
||||
<FilterForm class="flex-1 justify-end" inline={true} />
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
)}
|
||||
</NCard>
|
||||
)
|
||||
|
||||
// 渲染API密钥列表
|
||||
const renderApiKeyList = () => (
|
||||
<>
|
||||
{isMobile.value ? (
|
||||
<NFlex vertical>
|
||||
<ApiKeyCardList data={tableData.value?.list || []} loading={loading.value} class="mb-4" />
|
||||
<NFlex justify="center">
|
||||
<ApiKeyTablePage />
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
) : (
|
||||
<NCard class="card-shadow" bordered={false}>
|
||||
<ApiKeyTable loading={loading.value} class="mb-4" />
|
||||
<NFlex justify="end">
|
||||
<ApiKeyTablePage />
|
||||
</NFlex>
|
||||
</NCard>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
onMounted(() => formFetchSearch())
|
||||
|
||||
// 主渲染
|
||||
return () => (
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
{renderFilterSection()}
|
||||
{renderApiKeyList()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
62
frontend/apps/domain-management-backend/src/views/domain-security/types.d.ts
vendored
Normal file
62
frontend/apps/domain-management-backend/src/views/domain-security/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* 域名安全页面类型定义
|
||||
*/
|
||||
|
||||
/** API密钥项 */
|
||||
export interface ApiKeyItem {
|
||||
/** 密钥ID */
|
||||
id: number
|
||||
/** 密钥名称 */
|
||||
name: string
|
||||
/** Access Key */
|
||||
access_key: string
|
||||
/** Secret Key */
|
||||
secret_key: string
|
||||
/** Account ID */
|
||||
account_id: string
|
||||
/** 状态:0-禁用,1-启用 */
|
||||
status: 0 | 1
|
||||
/** 状态文本 */
|
||||
status_text?: string
|
||||
/** 最后调用时间 - 字符串格式 */
|
||||
last_used_at?: string | null
|
||||
/** 最后调用IP */
|
||||
last_used_ip?: string
|
||||
/** IP白名单 */
|
||||
ip_whitelist?: string[]
|
||||
/** 创建时间(字符串格式) */
|
||||
created_at: string
|
||||
/** 更新时间(字符串格式) */
|
||||
updated_at: string
|
||||
/** UID */
|
||||
uid?: number
|
||||
}
|
||||
|
||||
/** API密钥列表请求参数 */
|
||||
export interface ApiKeyListRequest {
|
||||
/** 页码,从 1 开始 */
|
||||
p?: number
|
||||
/** 每页条数 */
|
||||
rows?: number
|
||||
/** 关键字搜索 */
|
||||
keyword?: string
|
||||
/** 状态过滤:不传-全部,0-禁用,1-启用 */
|
||||
status?: number | string
|
||||
}
|
||||
|
||||
/** 状态选项 */
|
||||
export interface StatusOption {
|
||||
label: string
|
||||
value: number | string
|
||||
}
|
||||
|
||||
/** API密钥创建/更新请求 */
|
||||
export interface ApiKeyFormData {
|
||||
/** 密钥名称 */
|
||||
name: string
|
||||
/** IP白名单(可选) */
|
||||
ip_whitelist?: string
|
||||
/** 状态 */
|
||||
status: 0 | 1
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,636 @@
|
|||
/**
|
||||
* 域名安全页面控制器
|
||||
* 负责处理业务逻辑、事件响应和生命周期管理
|
||||
*/
|
||||
|
||||
import { defineComponent, ref, computed, type PropType } from 'vue'
|
||||
import { NButton, NSpace, NCard, NFlex, NSwitch, useDialog, NAlert, NText, NIcon } from 'naive-ui'
|
||||
import { CopyOutline, CheckmarkOutline } from '@vicons/ionicons5'
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
import { useDomainSecurityState } from './useStore'
|
||||
import { useTable, useForm, useFormHooks, useModal, useLoadingMask } from '@baota/naive-ui/hooks'
|
||||
import { useApp } from '@/components/layout/useStore'
|
||||
|
||||
import type { ApiKeyListRequest, ApiKeyItem, ApiKeyFormData } from './types.d'
|
||||
import { TableColumns } from 'naive-ui/es/data-table/src/interface'
|
||||
|
||||
/**
|
||||
* 域名安全页面控制器
|
||||
*/
|
||||
export function useController() {
|
||||
// 获取状态管理
|
||||
const {
|
||||
fetchApiKeyListData,
|
||||
filterFormData,
|
||||
statusOptions,
|
||||
formData,
|
||||
isEditing,
|
||||
editingId,
|
||||
resetFormData,
|
||||
setEditingData,
|
||||
createApiKey,
|
||||
updateApiKey,
|
||||
toggleApiKeyStatus,
|
||||
regenerateApiKey,
|
||||
deleteApiKey,
|
||||
} = useDomainSecurityState()
|
||||
|
||||
const { useFormInput, useFormSelect, useFormSwitch, useFormHelp } = useFormHooks()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 获取移动端状态
|
||||
const { isMobile } = useApp()
|
||||
|
||||
/**
|
||||
* 筛选表单配置
|
||||
*/
|
||||
const filterFormConfig = () => [
|
||||
useFormSelect(
|
||||
'',
|
||||
'status',
|
||||
statusOptions.value,
|
||||
{
|
||||
class: 'w-28',
|
||||
},
|
||||
{ showLabel: false, showFeedback: false }
|
||||
),
|
||||
useFormInput(
|
||||
'',
|
||||
'keyword',
|
||||
{
|
||||
placeholder: '请输入名称',
|
||||
clearable: true,
|
||||
class: 'w-64',
|
||||
},
|
||||
{ showLabel: false, showFeedback: false }
|
||||
),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => (
|
||||
<NSpace>
|
||||
<NButton type="primary" onClick={() => formFetchSearch()}>
|
||||
搜索
|
||||
</NButton>
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 创建/编辑表单配置
|
||||
*/
|
||||
const modalFormConfig = computed(() => {
|
||||
const config: any[] = [
|
||||
useFormInput(
|
||||
'密钥名称',
|
||||
'name',
|
||||
{
|
||||
placeholder: '请输入密钥名称',
|
||||
clearable: true,
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
showFeedback: true,
|
||||
labelWidth: '90px',
|
||||
rule: {
|
||||
required: true,
|
||||
message: '请输入密钥名称',
|
||||
trigger: ['blur', 'input'],
|
||||
},
|
||||
},
|
||||
),
|
||||
useFormInput(
|
||||
'IP白名单',
|
||||
'ip_whitelist',
|
||||
{
|
||||
placeholder: '请输入IP地址,每行一个IP',
|
||||
clearable: true,
|
||||
type: 'textarea',
|
||||
autosize: { minRows: 3, maxRows: 6 },
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
labelWidth: '90px',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
// 只在编辑时显示状态字段
|
||||
if (isEditing.value) {
|
||||
config.push(
|
||||
useFormSwitch(
|
||||
'状态',
|
||||
'status',
|
||||
{
|
||||
checkedValue: 1,
|
||||
uncheckedValue: 0,
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
showFeedback: false,
|
||||
labelWidth: '90px',
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// 添加按钮区域
|
||||
config.push(
|
||||
useFormHelp(
|
||||
[
|
||||
{
|
||||
content: '为了安全,建议设置IP白名单。留空表示不限制IP访问。每行一个IP地址。',
|
||||
},
|
||||
],
|
||||
{
|
||||
listStyle: 'none',
|
||||
class: 'text-[13px] text-gray-500 !pl-[65px] !leading-[1.6] mt-2 mb-4 ml-0',
|
||||
} as any,
|
||||
),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => (
|
||||
<NFlex justify="end" size="medium" class="mt-4">
|
||||
<NButton onClick={() => closeModal()}>取消</NButton>
|
||||
<NButton
|
||||
type="primary"
|
||||
disabled={formLoading.value}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await submitForm()
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isEditing.value ? '更新' : '创建'}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
/**
|
||||
* 表格列配置
|
||||
*/
|
||||
const createColumns = [
|
||||
{
|
||||
title: '名称',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row: ApiKeyItem) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="font-medium">{row.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (row: ApiKeyItem) => (
|
||||
<NSwitch
|
||||
value={row.status === 1}
|
||||
checkedValue={true}
|
||||
uncheckedValue={false}
|
||||
onUpdateValue={async (value: boolean) => {
|
||||
const newStatus = value ? 1 : 0
|
||||
try {
|
||||
await toggleApiKeyStatus(row.id, newStatus)
|
||||
await fetchApiKeys()
|
||||
} catch (error) {
|
||||
console.error('切换状态失败:', error)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '最后调用',
|
||||
key: 'last_used_at',
|
||||
width: 180,
|
||||
render: (row: ApiKeyItem) => {
|
||||
if (row.last_used_at && row.last_used_at !== null) {
|
||||
const date = new Date(row.last_used_at)
|
||||
if (!isNaN(date.getTime())) {
|
||||
return formatDate(date.getTime(), 'yyyy-MM-dd HH:mm:ss')
|
||||
}
|
||||
}
|
||||
return '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'IP白名单',
|
||||
key: 'ip_whitelist',
|
||||
width: 200,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row: ApiKeyItem) => (row.ip_whitelist?.length ? row.ip_whitelist.join(', ') : '-'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 220,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
render: (row: ApiKeyItem) => (
|
||||
<NSpace justify="end" size="small">
|
||||
<NButton size="small" type="primary" ghost onClick={() => handleEdit(row)}>
|
||||
编辑
|
||||
</NButton>
|
||||
<NButton size="small" type="primary" ghost onClick={() => handleRegenerate(row)}>
|
||||
重新生成
|
||||
</NButton>
|
||||
<NButton size="small" type="error" ghost onClick={() => handleDelete(row.id)}>
|
||||
删除
|
||||
</NButton>
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
] as TableColumns<ApiKeyItem>
|
||||
|
||||
/**
|
||||
* 移动端卡片组件
|
||||
*/
|
||||
const ApiKeyCardList = defineComponent({
|
||||
name: 'ApiKeyCardList',
|
||||
props: {
|
||||
data: {
|
||||
type: Array as PropType<ApiKeyItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<NFlex vertical size="medium">
|
||||
{props.data.map((item: ApiKeyItem) => (
|
||||
<NCard key={item.id} class="card-shadow" bordered={false}>
|
||||
<NFlex vertical size="small">
|
||||
<NFlex align="center" justify="space-between">
|
||||
<div class="font-medium text-base">{item.name}</div>
|
||||
<NSwitch
|
||||
value={item.status === 1}
|
||||
checkedValue={true}
|
||||
uncheckedValue={false}
|
||||
onUpdateValue={async (value: boolean) => {
|
||||
const newStatus = value ? 1 : 0
|
||||
try {
|
||||
await toggleApiKeyStatus(item.id, newStatus)
|
||||
await fetchApiKeys()
|
||||
} catch (error) {
|
||||
console.error('切换状态失败:', error)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</NFlex>
|
||||
|
||||
{item.ip_whitelist && item.ip_whitelist.length > 0 && (
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-500">IP白名单:</span>
|
||||
<span class="text-gray-700">
|
||||
{item.ip_whitelist.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="text-gray-500">最后调用:</span>
|
||||
{item.last_used_at && item.last_used_at !== null
|
||||
? (() => {
|
||||
const date = new Date(item.last_used_at)
|
||||
return !isNaN(date.getTime()) ? formatDate(date.getTime(), 'yyyy-MM-dd HH:mm:ss') : '-'
|
||||
})()
|
||||
: '-'
|
||||
}
|
||||
</div>
|
||||
|
||||
<NFlex justify="end" size="small">
|
||||
<NButton size="small" type="primary" ghost onClick={() => handleEdit(item)}>
|
||||
编辑
|
||||
</NButton>
|
||||
<NButton size="small" type="primary" ghost onClick={() => handleRegenerate(item)}>
|
||||
重新生成
|
||||
</NButton>
|
||||
<NButton size="small" type="error" ghost onClick={() => handleDelete(item.id)}>
|
||||
删除
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
))}
|
||||
</NFlex>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// 表格实例
|
||||
const {
|
||||
TableComponent: ApiKeyTable,
|
||||
PageComponent: ApiKeyTablePage,
|
||||
loading,
|
||||
fetch: fetchApiKeys,
|
||||
data: tableData,
|
||||
} = useTable<ApiKeyItem, ApiKeyListRequest>({
|
||||
config: createColumns,
|
||||
request: fetchApiKeyListData,
|
||||
defaultValue: filterFormData,
|
||||
alias: {
|
||||
page: 'p',
|
||||
pageSize: 'rows',
|
||||
},
|
||||
watchValue: ['p', 'rows', 'status'],
|
||||
})
|
||||
|
||||
// 筛选表单实例
|
||||
const { component: FilterForm, fetch: formFetchSearch } = useForm<ApiKeyListRequest>({
|
||||
config: filterFormConfig(),
|
||||
defaultValue: filterFormData,
|
||||
request: handleFormSearch,
|
||||
})
|
||||
|
||||
// 创建/编辑表单实例
|
||||
const { component: ModalForm, fetch: submitForm, loading: formLoading } = useForm<ApiKeyFormData>({
|
||||
config: modalFormConfig,
|
||||
defaultValue: formData,
|
||||
request: handleFormSubmit,
|
||||
})
|
||||
|
||||
// 模态框引用
|
||||
const currentModal = ref<any>(null)
|
||||
|
||||
/**
|
||||
* API密钥信息显示组件
|
||||
*/
|
||||
const ApiKeyInfoModal = defineComponent({
|
||||
name: 'ApiKeyInfoModal',
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<{
|
||||
access_key: string
|
||||
account_id: string
|
||||
secret_key: string
|
||||
}>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const copiedFields = ref<Set<string>>(new Set())
|
||||
|
||||
const copyToClipboard = async (text: string, field: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copiedFields.value.add(field)
|
||||
setTimeout(() => {
|
||||
copiedFields.value.delete(field)
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const copyAllInfo = async () => {
|
||||
const allInfo = `Access Key: ${props.data.access_key}\nAccount ID: ${props.data.account_id}\nSecret Key: ${props.data.secret_key}`
|
||||
try {
|
||||
await navigator.clipboard.writeText(allInfo)
|
||||
copiedFields.value.add('all')
|
||||
setTimeout(() => {
|
||||
copiedFields.value.delete('all')
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const keyItems = [
|
||||
{ label: 'Access Key', key: 'access_key', value: props.data.access_key },
|
||||
{ label: 'Account ID', key: 'account_id', value: props.data.account_id },
|
||||
{ label: 'Secret Key', key: 'secret_key', value: props.data.secret_key },
|
||||
]
|
||||
|
||||
const renderCopyButton = (key: string, value: string) => (
|
||||
<NButton
|
||||
size="small"
|
||||
type={copiedFields.value.has(key) ? 'success' : 'default'}
|
||||
onClick={() => copyToClipboard(value, key)}
|
||||
>
|
||||
<NIcon class="mr-1">
|
||||
{copiedFields.value.has(key) ? <CheckmarkOutline /> : <CopyOutline />}
|
||||
</NIcon>
|
||||
{copiedFields.value.has(key) ? '已复制' : '复制'}
|
||||
</NButton>
|
||||
)
|
||||
|
||||
return () => (
|
||||
<div class="p-4">
|
||||
<NAlert type="warning" class="mb-4" showIcon>
|
||||
API密钥创建成功!请立即保存以下信息,关闭后将无法再次查看。
|
||||
</NAlert>
|
||||
|
||||
<div class="space-y-4">
|
||||
{keyItems.map((item) => (
|
||||
<div key={item.key} class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-gray-700 mb-1">{item.label}:</div>
|
||||
<NText code class="text-sm select-all">
|
||||
{item.value}
|
||||
</NText>
|
||||
</div>
|
||||
{renderCopyButton(item.key, item.value)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<NFlex justify="center" class="mt-6">
|
||||
<NButton type="primary" size="large" onClick={copyAllInfo}>
|
||||
<NIcon class="mr-1">{copiedFields.value.has('all') ? <CheckmarkOutline /> : <CopyOutline />}</NIcon>
|
||||
{copiedFields.value.has('all') ? '全部已复制' : '复制全部信息'}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 关闭模态框
|
||||
*/
|
||||
const closeModal = () => {
|
||||
if (currentModal.value) {
|
||||
currentModal.value.close()
|
||||
currentModal.value = null
|
||||
}
|
||||
resetFormData()
|
||||
}
|
||||
|
||||
// -------------------- 事件处理 --------------------
|
||||
|
||||
/**
|
||||
* 表单搜索触发
|
||||
*/
|
||||
async function handleFormSearch(formData: ApiKeyListRequest) {
|
||||
await fetchApiKeys()
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单提交处理
|
||||
*/
|
||||
async function handleFormSubmit(data: ApiKeyFormData): Promise<void> {
|
||||
const { open: openLoad, close: closeLoad } = useLoadingMask({ text: `正在${isEditing.value ? '更新' : '创建'}API密钥,请稍后...` })
|
||||
openLoad()
|
||||
try {
|
||||
if (isEditing.value && editingId.value) {
|
||||
await updateApiKey(editingId.value, data)
|
||||
await fetchApiKeys()
|
||||
closeModal()
|
||||
} else {
|
||||
const result = await createApiKey(data)
|
||||
if (result.success) {
|
||||
await fetchApiKeys()
|
||||
closeModal()
|
||||
if (result.keyInfo) {
|
||||
useModal({
|
||||
title: 'API密钥创建成功',
|
||||
area: '600px',
|
||||
component: <ApiKeyInfoModal data={result.keyInfo} />,
|
||||
footer: false,
|
||||
maskClosable: false,
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error)
|
||||
} finally {
|
||||
closeLoad()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开创建密钥弹窗
|
||||
*/
|
||||
function handleCreate() {
|
||||
resetFormData()
|
||||
currentModal.value = useModal({
|
||||
title: '创建API密钥',
|
||||
area: '500px',
|
||||
component: <ModalForm />,
|
||||
footer: false,
|
||||
onClose: () => {
|
||||
resetFormData()
|
||||
currentModal.value = null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑密钥
|
||||
*/
|
||||
function handleEdit(item: ApiKeyItem) {
|
||||
setEditingData(item)
|
||||
currentModal.value = useModal({
|
||||
title: '编辑API密钥',
|
||||
area: '500px',
|
||||
component: <ModalForm />,
|
||||
footer: false,
|
||||
onClose: () => {
|
||||
resetFormData()
|
||||
currentModal.value = null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 重新生成密钥
|
||||
*/
|
||||
function handleRegenerate(item: ApiKeyItem) {
|
||||
dialog.warning({
|
||||
title: '确认重新生成',
|
||||
content: `确定要重新生成"${item.name}"的API密钥吗?重新生成后,原密钥将失效,请及时更新您的应用配置。`,
|
||||
positiveText: '重新生成',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
const { open: openLoad, close: closeLoad } = useLoadingMask({ text: '正在重新生成API密钥,请稍后...' })
|
||||
openLoad()
|
||||
try {
|
||||
const result = await regenerateApiKey(item.id)
|
||||
if (result.success) {
|
||||
await fetchApiKeys()
|
||||
if (result.keyInfo) {
|
||||
useModal({
|
||||
title: 'API密钥重新生成成功',
|
||||
area: '600px',
|
||||
component: <ApiKeyInfoModal data={result.keyInfo} />,
|
||||
footer: false,
|
||||
maskClosable: false,
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重新生成API密钥失败:', error)
|
||||
} finally {
|
||||
closeLoad()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除密钥
|
||||
*/
|
||||
function handleDelete(id: number) {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个API密钥吗?删除后无法恢复。',
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
const { open: openLoad, close: closeLoad } = useLoadingMask({ text: '正在删除API密钥,请稍后...' })
|
||||
openLoad()
|
||||
try {
|
||||
await deleteApiKey(id)
|
||||
await fetchApiKeys()
|
||||
} catch (error) {
|
||||
console.error('删除API密钥失败:', error)
|
||||
} finally {
|
||||
closeLoad()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
isMobile,
|
||||
|
||||
// 表格
|
||||
ApiKeyTable,
|
||||
ApiKeyTablePage,
|
||||
tableData,
|
||||
|
||||
// 移动端卡片
|
||||
ApiKeyCardList,
|
||||
|
||||
// 表单
|
||||
FilterForm,
|
||||
formFetchSearch,
|
||||
|
||||
// 事件处理
|
||||
handleCreate,
|
||||
handleEdit,
|
||||
handleRegenerate,
|
||||
handleDelete,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
/**
|
||||
* 域名安全页面状态管理
|
||||
* 负责管理API密钥数据、筛选条件、分页等状态
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { createApi, getApiKeyList, updateApiKey as updateApiKeyApi, regenerateApiKey as regenerateApiKeyApi, deleteApiKey as deleteApiKeyApi } from '@/api/api'
|
||||
|
||||
import type { TableResponse } from '@baota/naive-ui/types/table'
|
||||
import type { ApiKeyItem, ApiKeyListRequest, ApiKeyFormData, StatusOption } from './types.d'
|
||||
|
||||
const message = useMessage()
|
||||
const { handleError } = useError()
|
||||
|
||||
/**
|
||||
* 域名安全页面状态Store
|
||||
*/
|
||||
export const useDomainSecurityStore = defineStore('domain-security-store', () => {
|
||||
// -------------------- 状态定义 --------------------
|
||||
|
||||
/** 页面加载状态 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 状态选项 */
|
||||
const statusOptions = ref<StatusOption[]>([
|
||||
{ label: '全部状态', value: '' },
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
])
|
||||
|
||||
/** 筛选表单数据 */
|
||||
const filterFormData = ref<ApiKeyListRequest>({
|
||||
p: 1,
|
||||
rows: 10,
|
||||
keyword: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
/** 表单数据(创建/编辑) */
|
||||
const formData = ref<ApiKeyFormData>({
|
||||
name: '',
|
||||
ip_whitelist: '',
|
||||
status: 1,
|
||||
})
|
||||
|
||||
/** 编辑状态 */
|
||||
const isEditing = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
|
||||
// -------------------- 方法定义 --------------------
|
||||
|
||||
/**
|
||||
* 获取API密钥列表数据
|
||||
*/
|
||||
const fetchApiKeyListData = async <T = ApiKeyItem>(
|
||||
params: ApiKeyListRequest = {}
|
||||
): Promise<TableResponse<T>> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const requestParams = { ...params }
|
||||
if (requestParams.status === '' || requestParams.status === undefined) {
|
||||
delete requestParams.status
|
||||
}
|
||||
const { fetch, data } = getApiKeyList(requestParams)
|
||||
await fetch()
|
||||
if (data.value?.status) {
|
||||
const responseData = data.value?.data
|
||||
return {
|
||||
list: responseData.data as T[],
|
||||
total: responseData.total,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
return { list: [] as T[], total: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单数据
|
||||
*/
|
||||
const resetFormData = () => {
|
||||
formData.value = {
|
||||
name: '',
|
||||
ip_whitelist: '',
|
||||
status: 1,
|
||||
}
|
||||
isEditing.value = false
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编辑数据
|
||||
*/
|
||||
const setEditingData = (item: ApiKeyItem) => {
|
||||
isEditing.value = true
|
||||
editingId.value = item.id
|
||||
formData.value = {
|
||||
name: item.name,
|
||||
ip_whitelist: item.ip_whitelist?.join('\n') || '',
|
||||
status: item.status,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建API密钥
|
||||
*/
|
||||
const createApiKey = async (data: ApiKeyFormData): Promise<{ success: boolean; keyInfo?: { access_key: string; account_id: string; secret_key: string } }> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const ipWhitelist = data.ip_whitelist
|
||||
? data.ip_whitelist.trim().split(/\n/).map(ip => ip.trim()).filter(ip => ip.length > 0)
|
||||
: []
|
||||
|
||||
const createParams = {
|
||||
name: data.name,
|
||||
ip_whitelist: ipWhitelist
|
||||
}
|
||||
|
||||
const { fetch, data: apiResponse, message } = createApi(createParams)
|
||||
message.value = true
|
||||
await fetch()
|
||||
if (apiResponse.value?.status) {
|
||||
const responseData = apiResponse.value?.data
|
||||
return {
|
||||
success: true,
|
||||
keyInfo: responseData ? {
|
||||
access_key: responseData.access_key,
|
||||
account_id: responseData.account_id,
|
||||
secret_key: responseData.secret_key
|
||||
} : undefined
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新API密钥
|
||||
*/
|
||||
const updateApiKey = async (id: number, data: ApiKeyFormData): Promise<void> => {
|
||||
try {
|
||||
const ipWhitelist = data.ip_whitelist
|
||||
? data.ip_whitelist.trim().split(/\n/).map(ip => ip.trim()).filter(ip => ip.length > 0)
|
||||
: []
|
||||
const updateParams = {
|
||||
config_id: id,
|
||||
name: data.name,
|
||||
status: data.status,
|
||||
ip_whitelist: ipWhitelist
|
||||
}
|
||||
const { fetch, message } = updateApiKeyApi(updateParams)
|
||||
message.value = true
|
||||
await fetch()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换API密钥状态
|
||||
*/
|
||||
const toggleApiKeyStatus = async (config_id: number, status: number): Promise<void> => {
|
||||
try {
|
||||
const { fetch, message } = updateApiKeyApi({
|
||||
config_id,
|
||||
status
|
||||
} as any)
|
||||
message.value = true
|
||||
await fetch()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成API密钥
|
||||
*/
|
||||
const regenerateApiKeyAction = async (config_id: number): Promise<{ success: boolean; keyInfo?: { access_key: string; account_id: string; secret_key: string } }> => {
|
||||
try {
|
||||
const { fetch, data, message } = regenerateApiKeyApi({ config_id })
|
||||
message.value = true
|
||||
await fetch()
|
||||
if (data.value?.status) {
|
||||
const responseData = data.value?.data
|
||||
return {
|
||||
success: true,
|
||||
keyInfo: responseData ? {
|
||||
access_key: responseData.access_key,
|
||||
account_id: responseData.account_id,
|
||||
secret_key: responseData.secret_key
|
||||
} : undefined
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除API密钥
|
||||
*/
|
||||
const deleteApiKey = async (config_id: number): Promise<void> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { fetch, message } = deleteApiKeyApi({ config_id })
|
||||
message.value = true
|
||||
await fetch()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回状态和方法
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
statusOptions,
|
||||
filterFormData,
|
||||
formData,
|
||||
isEditing,
|
||||
editingId,
|
||||
|
||||
// 方法
|
||||
fetchApiKeyListData,
|
||||
resetFormData,
|
||||
setEditingData,
|
||||
createApiKey,
|
||||
updateApiKey,
|
||||
toggleApiKeyStatus,
|
||||
regenerateApiKey: regenerateApiKeyAction,
|
||||
deleteApiKey,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 导出Store实例
|
||||
*/
|
||||
export const useDomainSecurityState = () => {
|
||||
const store = useDomainSecurityStore()
|
||||
const storeRefs = storeToRefs(store)
|
||||
return {
|
||||
// 响应式状态
|
||||
...storeRefs,
|
||||
// 方法
|
||||
fetchApiKeyListData: store.fetchApiKeyListData,
|
||||
resetFormData: store.resetFormData,
|
||||
setEditingData: store.setEditingData,
|
||||
createApiKey: store.createApiKey,
|
||||
updateApiKey: store.updateApiKey,
|
||||
toggleApiKeyStatus: store.toggleApiKeyStatus,
|
||||
regenerateApiKey: store.regenerateApiKey,
|
||||
deleteApiKey: store.deleteApiKey,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,94 @@
|
|||
import { defineComponent, computed } from 'vue'
|
||||
import { NAlert, NButton, NFlex, NGrid, NGridItem, NQrCode, NRadioButton, NRadioGroup, NSelect, NTag, NSteps, NStep } from 'naive-ui'
|
||||
import { defineComponent, computed, ref } from 'vue'
|
||||
import { NAlert, NButton, NFlex, NGrid, NGridItem, NQrCode, NRadioButton, NRadioGroup, NSelect, NTag, NSteps, NStep, NInputNumber, NSpace } from 'naive-ui'
|
||||
import { useDomainState } from '../useStore'
|
||||
import { useController } from '../useController'
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RenewDialog',
|
||||
props: { YEAR_OPTIONS: { type: Array as unknown as () => number[], default: () => [1,2,3,5,10] } },
|
||||
props: { YEAR_OPTIONS: { type: Array as unknown as () => number[], default: () => [1,2,3,5] } },
|
||||
setup(props) {
|
||||
const state = useDomainState()
|
||||
const { changeRenewYear, switchRenewChannel, closeRenewModal, payRenewByBalance } = useController()
|
||||
|
||||
// 自定义年限状态管理
|
||||
const customYear = ref<number | null>(null)
|
||||
const selectedYearType = ref<string | number>(state.renewSelectedYear.value)
|
||||
const isQueryingPrice = ref(false) // 防止重复查询的标志位
|
||||
|
||||
// 生成下拉框选项(预设年份 + 自定义)
|
||||
const yearSelectOptions = [
|
||||
...props.YEAR_OPTIONS.map(year => ({
|
||||
label: `${year}年`,
|
||||
value: year
|
||||
})),
|
||||
{ label: '自定义', value: 'custom' }
|
||||
]
|
||||
|
||||
// 当前是否为自定义模式
|
||||
const isCustomMode = computed(() => selectedYearType.value === 'custom')
|
||||
|
||||
// 验证自定义年份
|
||||
const isValidCustomYear = computed(() => {
|
||||
return customYear.value !== null &&
|
||||
customYear.value >= 1 &&
|
||||
customYear.value <= 10 &&
|
||||
Number.isInteger(customYear.value)
|
||||
})
|
||||
|
||||
// 处理下拉框选择变化
|
||||
const onYearSelectChange = (value: string | number) => {
|
||||
selectedYearType.value = value
|
||||
|
||||
if (typeof value === 'number') {
|
||||
// 选择预设年份
|
||||
customYear.value = null
|
||||
changeRenewYear(value)
|
||||
} else if (value === 'custom') {
|
||||
// 选择自定义模式
|
||||
if (customYear.value && isValidCustomYear.value) {
|
||||
changeRenewYear(customYear.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查询价格的核心逻辑
|
||||
const queryCustomPrice = async () => {
|
||||
if (customYear.value !== null && isValidCustomYear.value && !isQueryingPrice.value) {
|
||||
isQueryingPrice.value = true
|
||||
try {
|
||||
await changeRenewYear(customYear.value)
|
||||
} finally {
|
||||
// 短暂延迟后重置标志位,防止快速重复调用
|
||||
setTimeout(() => {
|
||||
isQueryingPrice.value = false
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理自定义年限输入
|
||||
const onCustomYearChange = (value: number | null) => {
|
||||
customYear.value = value
|
||||
// 输入过程中不立即查询价格,避免频繁调用接口
|
||||
}
|
||||
|
||||
// 处理自定义输入框失焦
|
||||
const onCustomYearBlur = () => {
|
||||
// 只在失焦时查询一次价格
|
||||
void queryCustomPrice()
|
||||
}
|
||||
|
||||
// 处理回车键确认
|
||||
const onCustomYearKeyup = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
// 先查询价格
|
||||
void queryCustomPrice()
|
||||
// 然后让输入框失去焦点,但防重复调用机制会阻止重复查询
|
||||
;(e.target as HTMLInputElement)?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
const qrLink = computed(() => state.renewPayChannel.value === 'wechat' ? state.renewOrderInfo.value?.wx : state.renewOrderInfo.value?.ali)
|
||||
const canPayByBalance = computed(() => Number(state.renewBalanceAvailable.value || 0) >= Number(state.renewOrderInfo.value?.total_price || 0))
|
||||
|
||||
|
|
@ -44,12 +123,46 @@ export default defineComponent({
|
|||
</div>
|
||||
<div class="text-gray-500">续费年限</div>
|
||||
<div class="col-span-2">
|
||||
<NSpace>
|
||||
{/* 年限选择下拉框 */}
|
||||
<NSelect
|
||||
value={state.renewSelectedYear.value}
|
||||
onUpdateValue={(v) => changeRenewYear(Number(v))}
|
||||
options={props.YEAR_OPTIONS.map((y) => ({ label: `${y}年`, value: y }))}
|
||||
value={selectedYearType.value}
|
||||
onUpdateValue={onYearSelectChange}
|
||||
options={yearSelectOptions}
|
||||
loading={state.renewLoading.value}
|
||||
/>
|
||||
style={{ width: '100px' }}
|
||||
placeholder="选择年限"
|
||||
/>
|
||||
|
||||
{/* 自定义年限输入框 - 同层显示 */}
|
||||
{isCustomMode.value && (
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
<NInputNumber
|
||||
value={customYear.value}
|
||||
onUpdateValue={onCustomYearChange}
|
||||
onBlur={onCustomYearBlur}
|
||||
onFocus={(e: FocusEvent) => (e.target as HTMLInputElement).select()}
|
||||
placeholder="1-10年"
|
||||
min={1}
|
||||
max={10}
|
||||
precision={0}
|
||||
style={{ width: '120px' }}
|
||||
status={customYear.value !== null && !isValidCustomYear.value ? 'error' : undefined}
|
||||
inputProps={{
|
||||
onKeyup: onCustomYearKeyup
|
||||
}}
|
||||
/>
|
||||
<span style={{ marginLeft: '4px', fontSize: '14px', color: '#666' }}>年</span>
|
||||
</div>
|
||||
)}
|
||||
</NSpace>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{isCustomMode.value && customYear.value !== null && !isValidCustomYear.value && (
|
||||
<div style={{ color: '#d03050', fontSize: '12px', marginTop: '8px' }}>
|
||||
续费年限必须为1-10年的整数
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="text-gray-500">续费价格</div>
|
||||
<div class="col-span-2 text-red-500 font-medium">¥{priceToShow.value.toFixed(2)}</div>
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@ export default defineComponent({
|
|||
</>
|
||||
)
|
||||
|
||||
onMounted(() => formFetchSearch)
|
||||
// 主渲染
|
||||
return () => (
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export function useController() {
|
|||
const { useFormInput, useFormSelect } = useFormHooks()
|
||||
// 获取移动端状态
|
||||
const { isMobile } = useApp()
|
||||
|
||||
|
||||
const uMessage = useMessage()
|
||||
|
||||
function computeNewExpire(years: number) {
|
||||
|
|
@ -283,7 +283,7 @@ export function useController() {
|
|||
PageComponent: DomainTablePage,
|
||||
loading,
|
||||
param,
|
||||
fetch:fetchDomain,
|
||||
fetch: fetchDomain,
|
||||
data: tableData,
|
||||
} = useTable<DomainItem, DomainListRequest>({
|
||||
config: createColumns,
|
||||
|
|
@ -293,7 +293,7 @@ export function useController() {
|
|||
page: 'p',
|
||||
pageSize: 'rows',
|
||||
},
|
||||
watchValue: ['p', 'rows', 'keyword', 'status', 'suffix'],
|
||||
watchValue: ['p', 'rows', 'status', 'suffix'],
|
||||
})
|
||||
|
||||
// 表单实例
|
||||
|
|
@ -356,12 +356,18 @@ export function useController() {
|
|||
* 点击解析
|
||||
*/
|
||||
function handleDns(row: DomainItem) {
|
||||
router.push(`/domain/detail/${row.id}?tabs=analysis`)
|
||||
// router.push(`/domain/detail/${row.id}?tabs=analysis`)
|
||||
router.push(`/domain-resolve/detail/${row.id}?domain_name=${row.full_domain}`)
|
||||
}
|
||||
|
||||
// 续费入口
|
||||
async function ensureRenewBalance() {
|
||||
try { await loadAccountBalance(); useDomainState().setRenewBalance(Number(recharge.overview.value?.balance || 0)); } catch { useDomainState().setRenewBalance(0) }
|
||||
try {
|
||||
await loadAccountBalance()
|
||||
useDomainState().setRenewBalance(Number(recharge.overview.value?.balance || 0))
|
||||
} catch {
|
||||
useDomainState().setRenewBalance(0)
|
||||
}
|
||||
}
|
||||
function openRenewModal(row: DomainItem) {
|
||||
const state = useDomainState()
|
||||
|
|
@ -372,7 +378,7 @@ export function useController() {
|
|||
title: '域名续费',
|
||||
area: '520px',
|
||||
component: RenewDialog,
|
||||
componentProps: { YEAR_OPTIONS: [1, 2, 3, 5, 10] },
|
||||
componentProps: { YEAR_OPTIONS: [1, 2, 3, 5] },
|
||||
footer: false,
|
||||
onClose: () => {
|
||||
closeRenewModal()
|
||||
|
|
@ -383,16 +389,16 @@ export function useController() {
|
|||
computeNewExpire(state.renewSelectedYear.value)
|
||||
}
|
||||
|
||||
async function doRenew(domain: string, year: number) {
|
||||
async function doRenew(domain: string, year: number) {
|
||||
const state = useDomainState()
|
||||
state.setRenewLoading(true)
|
||||
try {
|
||||
const payload: RenewRequest = { domain_list: [{ domain, year, domain_service: 0 }] }
|
||||
const { fetch,data } = renewOrder(payload)
|
||||
const { fetch, data } = renewOrder(payload)
|
||||
await fetch()
|
||||
if (!data.value?.status) {
|
||||
uMessage.error(data.value?.msg || '续费失败')
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
state.setRenewOrderInfo((data.value.data as RenewData) || null)
|
||||
state.setRenewStep(2)
|
||||
|
|
@ -519,7 +525,9 @@ export function useController() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleRenew(row: DomainItem) { openRenewModal(row) }
|
||||
function handleRenew(row: DomainItem) {
|
||||
openRenewModal(row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新域名注册状态
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import { defineComponent, onMounted } from 'vue'
|
||||
import { NCard, NFlex, NPageHeader, NSpin } from 'naive-ui'
|
||||
import { useController } from './useController'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OperationLog',
|
||||
props: {},
|
||||
setup() {
|
||||
const {
|
||||
// 表格相关
|
||||
OperateLogTable,
|
||||
OperateLogTablePage,
|
||||
OperateLogCardList,
|
||||
loading,
|
||||
tableData,
|
||||
|
||||
// 表单相关
|
||||
FilterForm,
|
||||
|
||||
// 状态
|
||||
isMobile,
|
||||
} = useController()
|
||||
|
||||
return () => (
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
{/* 筛选表单 */}
|
||||
<NCard bordered={false} class="card-shadow">
|
||||
<NFlex class="w-full !flex-row !flex-wrap" size="medium">
|
||||
<FilterForm class="flex-1 justify-end" inline={true} />
|
||||
</NFlex>
|
||||
</NCard>
|
||||
|
||||
{/* 数据展示区域 */}
|
||||
<NCard bordered={false} class="card-shadow">
|
||||
<NSpin show={loading.value}>
|
||||
{isMobile.value ? (
|
||||
/* 移动端卡片列表 */
|
||||
<NFlex vertical size="medium">
|
||||
<OperateLogCardList data={tableData.value?.list || []} loading={loading.value} />
|
||||
<NFlex justify="center">
|
||||
<OperateLogTablePage />
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
) : (
|
||||
/* 桌面端表格 */
|
||||
<NFlex vertical size="medium">
|
||||
<OperateLogTable />
|
||||
<NFlex justify="end">
|
||||
<OperateLogTablePage />
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
)}
|
||||
</NSpin>
|
||||
</NCard>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
/**
|
||||
* 操作日志页面控制器
|
||||
* 负责处理业务逻辑、事件响应和生命周期管理
|
||||
*/
|
||||
|
||||
import { defineComponent, type PropType } from 'vue'
|
||||
import { NButton, NTag, NSpace, NCard, NFlex, NTooltip } from 'naive-ui'
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
import { useTable, useForm, useFormHooks, useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { getOperateLog } from '@/api/operate-log'
|
||||
import type { OperateLogData, OperateLogRequest } from '@/api/operate-log'
|
||||
import { useApp } from '@/components/layout/useStore'
|
||||
import { TableColumns } from 'naive-ui/es/data-table/src/interface'
|
||||
|
||||
// 定义标签类型
|
||||
type TagType = 'default' | 'success' | 'warning' | 'error' | 'info'
|
||||
|
||||
/**
|
||||
* 获取日志级别对应的标签类型
|
||||
*/
|
||||
function getLogLevelType(level: string): TagType {
|
||||
switch (level?.toLowerCase()) {
|
||||
case 'info':
|
||||
return 'info'
|
||||
case 'warning':
|
||||
return 'warning'
|
||||
case 'error':
|
||||
return 'error'
|
||||
case 'debug':
|
||||
return 'default'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作日志页面控制器
|
||||
*/
|
||||
export function useController() {
|
||||
// 获取移动端状态
|
||||
const { isMobile } = useApp()
|
||||
const { useFormInput } = useFormHooks()
|
||||
const message = useMessage()
|
||||
const { handleError } = useError()
|
||||
|
||||
// 默认查询参数
|
||||
const defaultParams = ref<OperateLogRequest>({
|
||||
keyword: '',
|
||||
p: 1,
|
||||
rows: 10,
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单配置
|
||||
*/
|
||||
const formConfig = () => [
|
||||
useFormInput(
|
||||
'',
|
||||
'keyword',
|
||||
{
|
||||
placeholder: '请输入操作内容',
|
||||
clearable: true,
|
||||
},
|
||||
{ showLabel: false, showFeedback: false },
|
||||
),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => (
|
||||
<NSpace>
|
||||
<NButton type="primary" onClick={() => formFetchSearch()}>
|
||||
搜索
|
||||
</NButton>
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 表格列配置
|
||||
*/
|
||||
const createColumns = [
|
||||
{
|
||||
title: '日志ID',
|
||||
key: 'log_id',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '级别',
|
||||
key: 'level',
|
||||
width: 60,
|
||||
render: (row: OperateLogData) => (
|
||||
<NTag type={getLogLevelType(row.level)} bordered={false} size="small">
|
||||
{row.level?.toUpperCase() || 'INFO'}
|
||||
</NTag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '客户端IP',
|
||||
key: 'remote_addr',
|
||||
width: 140,
|
||||
render: (row: OperateLogData) => <span class="text-gray-600 ">{row.remote_addr || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
key: 'module',
|
||||
width: 120,
|
||||
render: (row: OperateLogData) => <span class="text-gray-600">{row.module || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '请求方法',
|
||||
key: 'request_method',
|
||||
width: 100,
|
||||
render: (row: OperateLogData) => (
|
||||
<NTag type="info" bordered={false} size="small">
|
||||
{row.request_method || '-'}
|
||||
</NTag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '请求URL',
|
||||
key: 'request_url',
|
||||
width: 200,
|
||||
render: (row: OperateLogData) => <span class="text-gray-600 ">{row.request_url || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '操作内容',
|
||||
key: 'message',
|
||||
width: 400,
|
||||
render: (row: OperateLogData) => <div class="text-sm">{row.message || '-'}</div>,
|
||||
},
|
||||
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
render: (row: OperateLogData) => (
|
||||
<span class="text-gray-600 text-sm">{formatDate(row.created_at, 'yyyy-MM-dd HH:mm:ss')}</span>
|
||||
),
|
||||
},
|
||||
] as TableColumns<OperateLogData>
|
||||
|
||||
/**
|
||||
* 移动端卡片组件
|
||||
*/
|
||||
const OperateLogCardList = defineComponent({
|
||||
name: 'OperateLogCardList',
|
||||
props: {
|
||||
data: {
|
||||
type: Array as PropType<OperateLogData[]>,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<NFlex vertical size="medium">
|
||||
{props.data.map((item: OperateLogData) => (
|
||||
<NCard key={item.log_id} class="card-shadow" bordered={false}>
|
||||
<NFlex vertical size="small">
|
||||
{/* 日志级别和模块 */}
|
||||
<NFlex align="center" justify="space-between">
|
||||
<NFlex align="center" size="small">
|
||||
<NTag type={getLogLevelType(item.level)} bordered={false} size="small">
|
||||
{item.level?.toUpperCase() || 'INFO'}
|
||||
</NTag>
|
||||
{item.module && <span class="text-gray-600 text-sm">{item.module}</span>}
|
||||
</NFlex>
|
||||
<span class="text-gray-500">{formatDate(item.created_at, 'MM-dd HH:mm')}</span>
|
||||
</NFlex>
|
||||
|
||||
{/* 操作内容 */}
|
||||
<div class="text-sm text-gray-800">{item.message || '-'}</div>
|
||||
|
||||
{/* 请求信息 */}
|
||||
<NFlex justify="space-between" class="text-gray-500">
|
||||
<div>
|
||||
<span class="text-gray-400">方法:</span>
|
||||
<NTag type="info" bordered={false} size="tiny">
|
||||
{item.request_method || '-'}
|
||||
</NTag>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-400">IP:</span>
|
||||
<span>{item.remote_addr || '-'}</span>
|
||||
</div>
|
||||
</NFlex>
|
||||
|
||||
{/* 请求地址 */}
|
||||
{item.request_url && (
|
||||
<div class="text-gray-500 bg-gray-50 p-2 rounded">{item.request_url}</div>
|
||||
)}
|
||||
</NFlex>
|
||||
</NCard>
|
||||
))}
|
||||
</NFlex>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取操作日志列表
|
||||
* @param params 查询参数
|
||||
*/
|
||||
const fetchOperationListData = async <T = OperateLogData,>(params: OperateLogRequest = {}) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data } = await getOperateLog(params).fetch()
|
||||
return { list: data?.data as T[], total: data?.count || 0 }
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
message.error('加载域名列表失败')
|
||||
return { list: [] as T[], total: 0 }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 表格实例
|
||||
const {
|
||||
TableComponent: OperateLogTable,
|
||||
PageComponent: OperateLogTablePage,
|
||||
loading,
|
||||
param,
|
||||
fetch: fetchOperateLog,
|
||||
data: tableData,
|
||||
} = useTable<OperateLogData, OperateLogRequest>({
|
||||
config: createColumns,
|
||||
request: fetchOperationListData,
|
||||
defaultValue: defaultParams,
|
||||
alias: {
|
||||
page: 'p',
|
||||
pageSize: 'rows',
|
||||
},
|
||||
watchValue: ['p', 'rows'],
|
||||
})
|
||||
|
||||
// 表单实例
|
||||
const { component: FilterForm, fetch: formFetchSearch } = useForm<OperateLogRequest>({
|
||||
config: formConfig(),
|
||||
defaultValue: defaultParams,
|
||||
request: handleFormSearch,
|
||||
})
|
||||
/**
|
||||
* 表单搜索触发
|
||||
*/
|
||||
async function handleFormSearch() {
|
||||
await fetchOperateLog()
|
||||
}
|
||||
|
||||
// 组件挂载时获取路由参数
|
||||
onMounted(async () => {
|
||||
await fetchOperateLog()
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
// 表格相关
|
||||
OperateLogTable,
|
||||
OperateLogTablePage,
|
||||
OperateLogCardList,
|
||||
loading,
|
||||
tableData,
|
||||
fetchOperateLog,
|
||||
|
||||
// 表单相关
|
||||
FilterForm,
|
||||
formFetchSearch,
|
||||
|
||||
// 状态
|
||||
isMobile,
|
||||
}
|
||||
}
|
||||
|
|
@ -218,10 +218,7 @@ export function useController() {
|
|||
{/* 订单号和状态 */}
|
||||
<NFlex align="center" justify="space-between">
|
||||
<NFlex align="center" size="small">
|
||||
<div>
|
||||
<div class="font-medium text-base">{item.son_order_no}</div>
|
||||
<div class="text-xs text-gray-500">{getOrderTypeText(item.order_type)}</div>
|
||||
</div>
|
||||
<div class="text-base font-medium ">{item.full_domain}</div>
|
||||
</NFlex>
|
||||
<NTag type={getOrderStatusType(item.status)} bordered={false} size="small">
|
||||
{getOrderStatusText(item.status)}
|
||||
|
|
@ -230,17 +227,17 @@ export function useController() {
|
|||
|
||||
{/* 域名信息 */}
|
||||
<NFlex align="center" justify="space-between">
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="text-gray-500">域名:</span>
|
||||
{item.full_domain}
|
||||
<div class="text-xs text-gray-500">
|
||||
<div>订单类型:{getOrderTypeText(item.order_type)}</div>
|
||||
<div>订单号:{item.son_order_no}</div>
|
||||
</div>
|
||||
<div class="amount-text">{formatCurrency(parseFloat(item.total_amount))}</div>
|
||||
</NFlex>
|
||||
|
||||
{/* 时间信息 */}
|
||||
<NFlex justify="space-between" class="time-text">
|
||||
<div>
|
||||
<span class="text-gray-500">创建时间:</span>
|
||||
<div class="text-xs text-gray-500">
|
||||
<span>创建时间:</span>
|
||||
{formatRelativeTime(item.created_at)}
|
||||
</div>
|
||||
</NFlex>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ const ORDER_TYPE = {
|
|||
/** 域名续费 */
|
||||
RENEW: 1,
|
||||
/** 域名转入 */
|
||||
TRANSFER: 2,
|
||||
TRANSFER: 2,
|
||||
/** 购买隐私保护 */
|
||||
PURCHASE_PRIVACY: 3,
|
||||
/** 续费隐私保护 */
|
||||
RENEW_PRIVACY: 4,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -84,6 +88,8 @@ const ORDER_TYPE_MAP = {
|
|||
[ORDER_TYPE.REGISTER]: { text: "域名注册", color: "#1890ff" },
|
||||
[ORDER_TYPE.RENEW]: { text: "域名续费", color: "#52c41a" },
|
||||
[ORDER_TYPE.TRANSFER]: { text: "域名转入", color: "#722ed1" },
|
||||
[ORDER_TYPE.PURCHASE_PRIVACY]: { text: "购买隐私保护", color: "#faad14" },
|
||||
[ORDER_TYPE.RENEW_PRIVACY]: { text: "续费隐私保护", color: "#fff2f0" },
|
||||
unknown: { text: "未知类型", color: "#666666" },
|
||||
};
|
||||
|
||||
|
|
@ -106,7 +112,9 @@ const TYPE_OPTIONS = [
|
|||
{ label: "全部类型", value: -1 },
|
||||
{ label: "域名注册", value: ORDER_TYPE.REGISTER },
|
||||
{ label: "域名续费", value: ORDER_TYPE.RENEW },
|
||||
{ label: "域名转入", value: ORDER_TYPE.TRANSFER },
|
||||
{ label: "域名转入", value: ORDER_TYPE.TRANSFER },
|
||||
{ label: "购买隐私保护", value: ORDER_TYPE.PURCHASE_PRIVACY },
|
||||
{ label: "续费隐私保护", value: ORDER_TYPE.RENEW_PRIVACY },
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ export default defineComponent({
|
|||
if (newType !== oldType) {
|
||||
// 个人:身份证,企业:营业执照
|
||||
formData.id_type = newType === 1 ? 1 : 2
|
||||
formData.id_number = ''
|
||||
|
||||
// 清空文件列表
|
||||
formData.id_image_front = []
|
||||
|
|
@ -445,6 +446,7 @@ export default defineComponent({
|
|||
setBase64: (base64: string) => (uploadStates.frontImageBase64 = base64),
|
||||
setPath: (path: string) => (uploadStates.frontImagePath = path),
|
||||
onSuccess: (rdata: any) => {
|
||||
if (formData.type === 2) return
|
||||
const { idnum } = rdata.data || {}
|
||||
formData.id_number = idnum || ''
|
||||
message.success('已自动填充身份证信息')
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ export function useController() {
|
|||
{/* <NButton size="small" onClick={() => handleEditTemplate(row)}>
|
||||
编辑
|
||||
</NButton> */}
|
||||
<NButton size="small" type="error" ghost onClick={() => handleDeleteTemplate(row.registrant_id)}>
|
||||
<NButton size="small" type="error" ghost onClick={() => handleDeleteTemplate(row.id)}>
|
||||
删除
|
||||
</NButton>
|
||||
</NFlex>
|
||||
|
|
@ -329,9 +329,9 @@ export function useController() {
|
|||
|
||||
/**
|
||||
* 处理删除模板
|
||||
* @param registrantId 注册者标识ID
|
||||
* @param id 实名模板id
|
||||
*/
|
||||
async function handleDeleteTemplate(registrantId: string) {
|
||||
async function handleDeleteTemplate(id: number) {
|
||||
useDialog({
|
||||
type: 'warning',
|
||||
title: '确认删除',
|
||||
|
|
@ -341,7 +341,7 @@ export function useController() {
|
|||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await deleteTemplateById(registrantId)
|
||||
await deleteTemplateById(id)
|
||||
fetchTable()
|
||||
} catch (error) {
|
||||
console.error('删除模板失败:', error)
|
||||
|
|
@ -467,7 +467,7 @@ export function useController() {
|
|||
|
||||
{/* 操作按钮 */}
|
||||
<NFlex size="small">
|
||||
<NButton size="small" type="error" ghost onClick={() => handleDeleteTemplate(item.registrant_id)}>
|
||||
<NButton size="small" type="error" ghost onClick={() => handleDeleteTemplate(item.id)}>
|
||||
删除
|
||||
</NButton>
|
||||
</NFlex>
|
||||
|
|
|
|||
|
|
@ -189,11 +189,11 @@ export const useRealNameStore = defineStore('real-name-store', () => {
|
|||
|
||||
/**
|
||||
* 删除实名模板
|
||||
* @param registrantId 注册者标识ID
|
||||
* @param id 实名模板id
|
||||
*/
|
||||
const deleteTemplateById = async (registrant_id: string) => {
|
||||
const deleteTemplateById = async (id: number) => {
|
||||
try {
|
||||
const { data, fetch, message } = deleteUserDetail({ registrant_id })
|
||||
const { data, fetch, message } = deleteUserDetail({ id })
|
||||
message.value = true
|
||||
await fetch()
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { defineComponent, ref, watch, onMounted, computed } from "vue";
|
||||
import { NForm, NFormItem, NRadioGroup, NRadioButton, NSpace, NAlert, NSelect, NQrCode } from 'naive-ui'
|
||||
import { NForm, NFormItem, NSelect, NSpace, NAlert, NInputNumber, NQrCode, NRadioGroup, NRadioButton } from 'naive-ui'
|
||||
import { useRechargeState } from "../useStore";
|
||||
import { useRechargeController } from "../useController";
|
||||
|
||||
|
|
@ -17,11 +17,41 @@ export default defineComponent({
|
|||
const { createPayload, setCreatePayload, qrWxUrl, qrAliUrl } = useRechargeState();
|
||||
const { createRechargeOrder } = useRechargeController();
|
||||
|
||||
const presets = [2000, 3000, 5000];
|
||||
const amountOptions = presets.map((p) => ({ label: `${p}元`, value: p }));
|
||||
// 预设金额配置
|
||||
const presets = [200, 500, 1000, 2000, 5000];
|
||||
const updating = ref(false);
|
||||
const customAmount = ref<number | null>(null);
|
||||
const selectedAmountType = ref<string | number>(2000); // 当前选中的下拉框值
|
||||
|
||||
// 生成下拉框选项
|
||||
const amountOptions = [
|
||||
...presets.map(amount => ({ label: `${amount}元`, value: amount })),
|
||||
{ label: '自定义', value: 'custom' }
|
||||
];
|
||||
|
||||
// 当前是否为自定义模式
|
||||
const isCustomMode = computed(() => selectedAmountType.value === 'custom');
|
||||
|
||||
// 验证自定义金额
|
||||
const isValidCustomAmount = computed(() => {
|
||||
return customAmount.value !== null &&
|
||||
customAmount.value >= 200 &&
|
||||
Number.isInteger(customAmount.value);
|
||||
});
|
||||
|
||||
// 当前是否可以发起请求
|
||||
const canCreateOrder = computed(() => {
|
||||
if (!isCustomMode.value) {
|
||||
return true;
|
||||
}
|
||||
return isValidCustomAmount.value;
|
||||
});
|
||||
|
||||
const requestOrder = async () => {
|
||||
if (!canCreateOrder.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updating.value = true;
|
||||
await createRechargeOrder();
|
||||
|
|
@ -33,16 +63,66 @@ export default defineComponent({
|
|||
const triggerOrder = debounce(requestOrder, 300);
|
||||
|
||||
onMounted(async () => {
|
||||
setCreatePayload({ amount: 2000, channel: 'wechat' });
|
||||
// 默认选中2000元
|
||||
selectedAmountType.value = 2000;
|
||||
setCreatePayload({
|
||||
amount: 2000,
|
||||
channel: 'wechat',
|
||||
amountType: 'preset'
|
||||
});
|
||||
await requestOrder();
|
||||
});
|
||||
|
||||
// 监听金额变化,只在验证通过时触发
|
||||
watch(
|
||||
[() => createPayload.value.amount],
|
||||
() => triggerOrder(),
|
||||
[() => createPayload.value.amount, () => canCreateOrder.value],
|
||||
([newAmount, canCreate]) => {
|
||||
if (canCreate) {
|
||||
triggerOrder();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const onAmountChange = (v: number) => setCreatePayload({ ...createPayload.value, amount: Number(v || 0) });
|
||||
// 处理下拉框选择变化
|
||||
const onSelectChange = (value: string | number) => {
|
||||
selectedAmountType.value = value;
|
||||
|
||||
if (typeof value === 'number') {
|
||||
// 选择预设金额
|
||||
customAmount.value = null;
|
||||
setCreatePayload({
|
||||
...createPayload.value,
|
||||
amount: value,
|
||||
amountType: 'preset'
|
||||
});
|
||||
} else if (value === 'custom') {
|
||||
// 选择自定义模式,但不立即更新payload
|
||||
// 等待用户输入有效金额
|
||||
}
|
||||
};
|
||||
|
||||
// 处理自定义金额输入
|
||||
const onCustomAmountChange = (value: number | null) => {
|
||||
customAmount.value = value;
|
||||
|
||||
// 实时更新payload(如果有效)
|
||||
if (value !== null && isCustomMode.value) {
|
||||
setCreatePayload({
|
||||
...createPayload.value,
|
||||
amount: value,
|
||||
amountType: 'custom'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理自定义输入框失焦
|
||||
const onCustomInputBlur = () => {
|
||||
if (isCustomMode.value && isValidCustomAmount.value) {
|
||||
// 验证通过,触发接口请求
|
||||
triggerOrder();
|
||||
}
|
||||
};
|
||||
|
||||
const onChannelChange = (v: 'wechat' | 'alipay') => setCreatePayload({ ...createPayload.value, channel: v });
|
||||
|
||||
const qrLink = computed(() => (createPayload.value.channel === 'wechat' ? qrWxUrl.value : qrAliUrl.value) || '');
|
||||
|
|
@ -54,28 +134,62 @@ export default defineComponent({
|
|||
</NAlert>
|
||||
<NForm labelPlacement="left" class="p-4">
|
||||
<NFormItem label="充值金额">
|
||||
<NSelect
|
||||
style={{ width: '180px' }}
|
||||
value={createPayload.value.amount}
|
||||
options={amountOptions}
|
||||
onUpdateValue={(v) => onAmountChange(Number(v || 0))}
|
||||
/>
|
||||
<NSpace>
|
||||
{/* 金额选择下拉框 */}
|
||||
<NSelect
|
||||
value={selectedAmountType.value}
|
||||
options={amountOptions}
|
||||
onUpdateValue={onSelectChange}
|
||||
style={{ width: '120px' }}
|
||||
placeholder="选择金额"
|
||||
/>
|
||||
|
||||
{/* 自定义金额输入框 - 仅在选择自定义时显示 */}
|
||||
{isCustomMode.value && (
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
<NInputNumber
|
||||
value={customAmount.value}
|
||||
onUpdateValue={onCustomAmountChange}
|
||||
onBlur={onCustomInputBlur}
|
||||
onFocus={(e: FocusEvent) => (e.target as HTMLInputElement).select()}
|
||||
placeholder="最低200元"
|
||||
min={200}
|
||||
precision={0}
|
||||
style={{ width: '140px' }}
|
||||
status={customAmount.value !== null && !isValidCustomAmount.value ? 'error' : undefined}
|
||||
/>
|
||||
<span style={{ marginLeft: '4px', fontSize: '14px', color: '#666' }}>元</span>
|
||||
</div>
|
||||
)}
|
||||
</NSpace>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{isCustomMode.value && customAmount.value !== null && !isValidCustomAmount.value && (
|
||||
<div style={{ color: '#d03050', fontSize: '12px', marginTop: '8px' }}>
|
||||
充值金额不能少于200元,且必须为整数
|
||||
</div>
|
||||
)}
|
||||
</NFormItem>
|
||||
<NFormItem label="充值方式">
|
||||
<NRadioGroup value={createPayload.value.channel} onUpdateValue={(v) => onChannelChange(v as any)}>
|
||||
<NSpace>
|
||||
<NRadioButton value="wechat">微信支付</NRadioButton>
|
||||
<NRadioButton value="alipay">支付宝</NRadioButton>
|
||||
</NSpace>
|
||||
<NRadioGroup
|
||||
value={createPayload.value.channel}
|
||||
onUpdateValue={(v) => onChannelChange(v as 'wechat' | 'alipay')}
|
||||
>
|
||||
<NRadioButton value="wechat">微信支付</NRadioButton>
|
||||
<NRadioButton value="alipay">支付宝</NRadioButton>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem label="">
|
||||
<div style="width: 256px; height: 256px; display:flex; align-items:center; justify-content:center; border: 1px dashed #ececec;">
|
||||
{qrLink.value ? (
|
||||
<NQrCode value={qrLink.value} size={220} />
|
||||
) : (
|
||||
<span>{updating.value ? '生成中...' : '请选择金额与方式'}</span>
|
||||
)}
|
||||
<div class="flex flex-1 justify-center">
|
||||
<div class="border-1 border-gray-300">
|
||||
{qrLink.value ? (
|
||||
<NQrCode value={qrLink.value} size={180} />
|
||||
) : (
|
||||
<span>
|
||||
{updating.value ? '生成中...' : !canCreateOrder.value ? '请输入有效的充值金额' : '请选择金额与方式'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ export interface RechargeRecord {
|
|||
export interface CreateRechargePayload {
|
||||
amount: number; // 元
|
||||
channel: 'wechat' | 'alipay';
|
||||
amountType?: 'preset' | 'custom'; // 金额类型:预设或自定义
|
||||
customAmount?: number; // 自定义金额
|
||||
}
|
||||
|
||||
export interface RechargeState {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export const useRechargeController = () => {
|
|||
const openRechargeModal = () => {
|
||||
openRechargeDialog.value = useModal({
|
||||
title: '账户充值',
|
||||
area: '530px',
|
||||
area: '520px',
|
||||
component: RechargeDialogContent,
|
||||
componentProps: {},
|
||||
footer: false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
import { defineComponent, ref } from 'vue'
|
||||
import { NCard, NFlex, NButton, NDivider, NIcon, NAlert } from 'naive-ui'
|
||||
import { CloseOutline, SearchOutline } from '@vicons/ionicons5'
|
||||
import { useController } from './useController'
|
||||
import { useApp } from '@/components/layout/useStore'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DomainTransferView',
|
||||
setup() {
|
||||
const { loading, TableComponent, PageComponent, FilterForm, TransferCardList, tableData, openTransferDialog } =
|
||||
useController()
|
||||
const { isMobile } = useApp()
|
||||
|
||||
// 移动端搜索表单显示状态
|
||||
const showSearchForm = ref(false)
|
||||
const toggleSearchForm = () => {
|
||||
showSearchForm.value = !showSearchForm.value
|
||||
}
|
||||
|
||||
const renderFilterSection = () => (
|
||||
<>
|
||||
{isMobile.value ? (
|
||||
<NFlex vertical class="w-full" size="medium">
|
||||
<NFlex justify="space-between" align="center">
|
||||
<NButton type="primary" onClick={() => openTransferDialog()}>
|
||||
域名转入
|
||||
</NButton>
|
||||
<NButton onClick={toggleSearchForm} class="search-toggle-btn">
|
||||
{showSearchForm.value ? (
|
||||
<>
|
||||
<NIcon size="18">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
<span>关闭</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NIcon size="18">
|
||||
<SearchOutline />
|
||||
</NIcon>
|
||||
<span>搜索</span>
|
||||
</>
|
||||
)}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
{showSearchForm.value && (
|
||||
<>
|
||||
<NDivider class="!my-2" dashed />
|
||||
<div class="mobile-search-form">
|
||||
<FilterForm inline={false} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NFlex>
|
||||
) : (
|
||||
<NFlex justify="space-between" align="center" class="w-full">
|
||||
<NFlex class="w-full !flex-row !flex-wrap" size="medium">
|
||||
<div>
|
||||
<NButton type="primary" onClick={() => openTransferDialog()}>
|
||||
域名转入
|
||||
</NButton>
|
||||
<a
|
||||
class="ml-4 text-[#20a53a] decoration-none hover:text-[#20a53a]-800 text-sm cursor-pointer"
|
||||
href="https://docs.bt.cn/domain/user-guide/domain-transfer-in"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
帮助文档
|
||||
</a>
|
||||
</div>
|
||||
<FilterForm class="flex-1 justify-end" inline={true} />
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
const renderList = () => (
|
||||
<>
|
||||
{isMobile.value ? (
|
||||
<NFlex vertical>
|
||||
<>
|
||||
<TransferCardList data={tableData.value?.list || []} loading={loading.value} />
|
||||
<NFlex justify="center">
|
||||
<PageComponent />
|
||||
</NFlex>
|
||||
</>
|
||||
</NFlex>
|
||||
) : (
|
||||
<>
|
||||
<TableComponent max-height={`calc(100vh - 560px)`} class="mb-4" />
|
||||
<NFlex justify="end">
|
||||
<PageComponent />
|
||||
</NFlex>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return () => (
|
||||
<div class="flex flex-col justify-between min-h-[calc(100vh-160px)]">
|
||||
<div class="flex flex-col gap-[16px] p-4">
|
||||
{renderFilterSection()}
|
||||
{renderList()}
|
||||
</div>
|
||||
<NAlert title="重要提示" type="info" class="mt-4">
|
||||
<div class="mt-2">
|
||||
<ul class="list-disc text-sm text-gray-700 leading-relaxed">
|
||||
<li>
|
||||
如域名有效期已达10年(最长期限),则转入时仍需扣除费用但年限不增加。域名续费不足45天时不建议转入,否则可能导致年限减少。
|
||||
</li>
|
||||
<li>根据ICANN 相关规定,域名注册、转入后的60天内将不允许转移注册商。</li>
|
||||
<li>
|
||||
域名转入时,注册局需要重新对转入的域名进行前缀的合法性命名审核,如果合法命名审核无法通过,域名将无法转入。
|
||||
</li>
|
||||
<li>
|
||||
域名转入时必须选择已实名认证的域名信息模板。域名转入一般需要 3~7个工作日,具体取决于注册局审核时间。
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</NAlert>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,446 @@
|
|||
import { ref } from 'vue'
|
||||
import { NFlex, NCard, NTag, NButton, NSpace, NTooltip, NIcon } from 'naive-ui'
|
||||
import { useTable, useForm, useFormHooks } from '@baota/naive-ui/hooks'
|
||||
import { useTransferJoinState } from './useStore'
|
||||
import type { DomainTransferItem, DomainTransferListRequest } from '@/types/transfer'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import type { ContactTemplateItem } from '@/types/real-name'
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
import { createTransferOrder, cancelTransfer } from '@/api/transfer'
|
||||
import { fetchContactUserDetail } from '@/api/real-name'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useRechargeState } from '@/views/recharge/useStore'
|
||||
import { useRechargeController } from '@/views/recharge/useController'
|
||||
import { useModal, useDialog } from '@baota/naive-ui/hooks'
|
||||
import TransferDialog from '../TransferDialog'
|
||||
import TransferDetailsDialog from '../TransferDetailsDialog'
|
||||
import DomainRegistrationForm from '@/views/real-name/components/DomainRegistrationForm/index'
|
||||
import { queryDomainPrice } from '@/api/domain'
|
||||
import { InformationCircleOutline } from '@vicons/ionicons5'
|
||||
|
||||
const STATUS_TYPE: Record<number, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
|
||||
0: 'warning', // 申请已提交
|
||||
1: 'error', // 申请失败
|
||||
2: 'default', // 取消转入
|
||||
3: 'error', // 转入失败
|
||||
4: 'success', // 转入成功
|
||||
}
|
||||
|
||||
const openTransferRef = ref()
|
||||
const openTransferDetailsRef = ref()
|
||||
export function useController() {
|
||||
const { fetchTransferListData, filterFormData } = useTransferJoinState()
|
||||
const store = useTransferJoinState()
|
||||
const { useFormInput } = useFormHooks()
|
||||
const recharge = useRechargeState()
|
||||
const { loadAccountBalance } = useRechargeController()
|
||||
const message = useMessage()
|
||||
/**
|
||||
* 处理取消订单
|
||||
* @param order 订单信息
|
||||
*/
|
||||
async function handleCancelTransfer(record_id: number) {
|
||||
useDialog({
|
||||
type: 'warning',
|
||||
title: '确认取消',
|
||||
area: '40',
|
||||
content: '确定要取消此转入申请吗?此操作不可恢复。',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
const { fetch, message } = cancelTransfer({ record_id })
|
||||
message.value = true
|
||||
await fetch()
|
||||
await gFetch()
|
||||
} catch (error) {
|
||||
console.error('删除模板失败:', error)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
// 打开转入对话框(可选预填)
|
||||
async function openTransferDetails(row: DomainTransferItem) {
|
||||
openTransferDetailsRef.value = useModal({
|
||||
title: '失败详情',
|
||||
area: '600px',
|
||||
component: TransferDetailsDialog,
|
||||
componentProps: {
|
||||
record: row,
|
||||
close: () => {
|
||||
openTransferDetailsRef.value?.close()
|
||||
},
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<DomainTransferItem> = [
|
||||
{ title: '域名', key: 'domain', width: 260 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status_text',
|
||||
width: 140,
|
||||
render: (r: DomainTransferItem) => {
|
||||
const tagElement = (
|
||||
<NTag type={STATUS_TYPE[r.status] || 'default'} bordered={false} size="small">
|
||||
{r.status_text}
|
||||
</NTag>
|
||||
)
|
||||
|
||||
if (r.status === 0) {
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
{tagElement}
|
||||
<NTooltip trigger="click">
|
||||
{{
|
||||
default: () => (
|
||||
<span>
|
||||
域名转入未完成?请参考帮助文档:
|
||||
<a
|
||||
class="text-[#20a53a] hover:text-[#20a53a]-800 text-sm underline cursor-pointer ml-1"
|
||||
href="https://docs.bt.cn/domain/user-guide/domain-transfer-in"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
帮助文档
|
||||
</a>
|
||||
</span>
|
||||
),
|
||||
trigger: () => (
|
||||
<NIcon class="flex justify-center cursor-pointer ml-1">
|
||||
<InformationCircleOutline />
|
||||
</NIcon>
|
||||
),
|
||||
}}
|
||||
</NTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return tagElement
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '转入提交时间',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
render: (r: DomainTransferItem) => formatDate(r?.created_at || 0, 'yyyy-MM-dd'),
|
||||
},
|
||||
{
|
||||
title: '转入流程结束时间',
|
||||
key: 'complete_time',
|
||||
width: 180,
|
||||
render: (r: DomainTransferItem) => formatDate(r?.complete_time || 0, 'yyyy-MM-dd'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
render: (r: DomainTransferItem) => (
|
||||
<NSpace justify="end">
|
||||
{r.status === 0 && (
|
||||
<NButton size="small" ghost onClick={() => handleCancelTransfer(r.id)}>
|
||||
取消
|
||||
</NButton>
|
||||
)}
|
||||
{(r.status === 1 || r.status === 3) && (
|
||||
<NButton size="small" ghost onClick={() => openTransferDetails(r)}>
|
||||
详情
|
||||
</NButton>
|
||||
)}
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 移动端卡片组件
|
||||
*/
|
||||
const TransferCardList = defineComponent({
|
||||
name: 'TransferCardList',
|
||||
props: {
|
||||
data: {
|
||||
type: Array as PropType<DomainTransferItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<NFlex vertical size="medium">
|
||||
{props.data.map((item: DomainTransferItem) => (
|
||||
<NCard key={item.id} class="card-shadow" bordered={false}>
|
||||
<NFlex vertical size="small">
|
||||
{/* 域名信息 */}
|
||||
<NFlex align="center" justify="space-between">
|
||||
<NFlex align="center" size="small">
|
||||
<div>
|
||||
<div class="font-medium text-base">{item.domain}</div>
|
||||
</div>
|
||||
</NFlex>
|
||||
<NFlex align="center" size="small">
|
||||
<NTag type={STATUS_TYPE[item.status] || 'default'} bordered={false} size="small">
|
||||
{item.status_text}
|
||||
</NTag>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
{/* 时间信息 */}
|
||||
<NFlex class="text-sm text-gray-600">
|
||||
<div>
|
||||
<span class="text-gray-500">转入提交时间:</span>
|
||||
{formatDate(item?.created_at || 0, 'yyyy-MM-dd')}
|
||||
</div>
|
||||
</NFlex>
|
||||
<NFlex class="text-sm text-gray-600">
|
||||
<div>
|
||||
<span class="text-gray-500">转入流程结束时间:</span>
|
||||
{formatDate(item?.complete_time || 0, 'yyyy-MM-dd')}
|
||||
</div>
|
||||
</NFlex>
|
||||
{/* 操作按钮 */}
|
||||
<NFlex justify="end" size="small">
|
||||
{item.status === 0 && (
|
||||
<NButton size="small" ghost onClick={() => handleCancelTransfer(item.id)}>
|
||||
取消
|
||||
</NButton>
|
||||
)}
|
||||
{(item.status === 1 || item.status === 3) && (
|
||||
<NButton size="small" ghost onClick={() => openTransferDetails(item)}>
|
||||
详情
|
||||
</NButton>
|
||||
)}
|
||||
</NFlex>
|
||||
</NCard>
|
||||
))}
|
||||
</NFlex>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
TableComponent,
|
||||
PageComponent,
|
||||
data: tableData,
|
||||
loading,
|
||||
fetch: gFetch,
|
||||
param,
|
||||
} = useTable<DomainTransferItem, DomainTransferListRequest>({
|
||||
config: columns,
|
||||
request: fetchTransferListData,
|
||||
defaultValue: filterFormData,
|
||||
alias: { page: 'p', pageSize: 'rows' },
|
||||
watchValue: ['p', 'rows'],
|
||||
})
|
||||
|
||||
// 顶部搜索
|
||||
async function handleSearch() {
|
||||
await gFetch()
|
||||
}
|
||||
|
||||
// 打开转入对话框(可选预填)
|
||||
async function openTransferDialog(preset?: { domain?: string }) {
|
||||
// 先重置,避免覆盖随后加载的实名模板 options
|
||||
store.resetDialog()
|
||||
await loadRealNameOptions()
|
||||
if (preset?.domain) store.rows.value = [{ domain: preset.domain, transfer_code: '' }]
|
||||
openTransferRef.value = useModal({
|
||||
title: '域名转入',
|
||||
area: '720px',
|
||||
component: TransferDialog,
|
||||
componentProps: {
|
||||
refresh: gFetch,
|
||||
close: () => openTransferRef.value?.close?.(),
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
|
||||
function closeTransferDialog() {
|
||||
openTransferRef.value?.close?.()
|
||||
}
|
||||
|
||||
// 查询域名(仅校验非空 → Step2)
|
||||
async function checkDomains() {
|
||||
const rows = store.rows.value.map((r) => ({
|
||||
domain: r.domain.trim().toLowerCase(),
|
||||
transfer_code: r.transfer_code.trim(),
|
||||
}))
|
||||
const hasMissingCode = rows.some((r) => r.domain && !r.transfer_code)
|
||||
if (hasMissingCode) {
|
||||
message.warning('转移码不能为空')
|
||||
return
|
||||
}
|
||||
const valid = rows.filter((r) => r.domain && r.transfer_code)
|
||||
if (valid.length === 0) {
|
||||
message.warning('请填写至少一条域名与转移码')
|
||||
return
|
||||
}
|
||||
const seen = new Set<string>()
|
||||
for (const r of valid) {
|
||||
if (seen.has(r.domain)) {
|
||||
message.warning('域名已存在')
|
||||
return
|
||||
}
|
||||
seen.add(r.domain)
|
||||
}
|
||||
store.setStep(2)
|
||||
await loadTransferPrice(valid.map((v) => v.domain))
|
||||
}
|
||||
|
||||
async function loadTransferPrice(domains: string[]) {
|
||||
try {
|
||||
store.setTransferPriceLoading(true)
|
||||
const { fetch, data } = queryDomainPrice({ domain: domains.join(','), year: 1, type: 'transfer' } as any)
|
||||
await fetch()
|
||||
const results = (data.value as any)?.data?.results || []
|
||||
const list = Array.isArray(results)
|
||||
? results.map((it: any) => ({
|
||||
domain: String(it.domain || ''),
|
||||
price: Number(it.price || 0),
|
||||
error: String(it.error || ''),
|
||||
}))
|
||||
: []
|
||||
console.log(list, '--')
|
||||
store.setTransferPrice(list)
|
||||
} catch {
|
||||
store.setTransferPrice([])
|
||||
} finally {
|
||||
store.setTransferPriceLoading(false)
|
||||
}
|
||||
}
|
||||
/** 选择实名模板 */
|
||||
const handleSelectRealName = (val: number) => {
|
||||
if (val === -1) openCreateRealNameModal()
|
||||
else store.setSelectedRealNameId(val)
|
||||
}
|
||||
/** 创建实名模板窗口(步骤一入口中的快捷按钮) */
|
||||
const openCreateRealNameModal = () => {
|
||||
const modal = useModal({
|
||||
title: '创建实名模板',
|
||||
area: '1000px',
|
||||
component: DomainRegistrationForm,
|
||||
componentProps: {
|
||||
mode: 'add',
|
||||
refresh: async () => {
|
||||
await loadRealNameOptions()
|
||||
},
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
return modal
|
||||
}
|
||||
|
||||
// 优化:加载实名模板并写入 Store,失败兜底
|
||||
async function loadRealNameOptions() {
|
||||
try {
|
||||
const { fetch: dFetch, data: dData } = fetchContactUserDetail({ p: 1, rows: 50, status: 2 })
|
||||
await dFetch()
|
||||
const payload = dData.value as any
|
||||
const list: ContactTemplateItem[] = ((payload && payload.msg && payload.msg.data) ||
|
||||
(payload && payload.data && payload.data.data) ||
|
||||
[]) as ContactTemplateItem[]
|
||||
const options = Array.isArray(list)
|
||||
? list.map((it) => ({ label: it.template_name || it.owner_name || String(it.id), value: it.id }))
|
||||
: []
|
||||
store.setRealNameOptions(options)
|
||||
// 默认选中第一个
|
||||
store.setSelectedRealNameId(options[0]?.value || null)
|
||||
} catch {
|
||||
message.error('加载实名模板失败')
|
||||
store.setRealNameOptions([])
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Step2 -> Step3 创建订单
|
||||
async function createOrder() {
|
||||
if (!store.selectedTemplateId.value) {
|
||||
message.warning('请选择实名模板')
|
||||
return
|
||||
}
|
||||
if (!store.agree.value) {
|
||||
message.warning('请勾选并同意相关协议')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
domain_list: store.rows.value
|
||||
.map((r) => ({ domain: r.domain.trim(), transfer_code: r.transfer_code.trim() }))
|
||||
.filter((r) => r.domain && r.transfer_code),
|
||||
real_name_template_id: store.selectedTemplateId.value as number,
|
||||
}
|
||||
const { fetch, data } = createTransferOrder(payload)
|
||||
await fetch()
|
||||
if (!data.value?.status) {
|
||||
message.error(data.value?.msg || '创建订单失败')
|
||||
return
|
||||
}
|
||||
store.orderInfo.value = data.value.data as any
|
||||
store.setStep(3)
|
||||
await ensureBalance()
|
||||
}
|
||||
|
||||
// 余额获取
|
||||
async function ensureBalance() {
|
||||
try {
|
||||
await loadAccountBalance()
|
||||
store.balanceAvailable.value = Number(recharge.overview.value?.balance || 0)
|
||||
} catch {
|
||||
store.balanceAvailable.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
/** 表单配置(列表页顶部搜索) */
|
||||
const formConfig = () => [
|
||||
useFormInput(
|
||||
'',
|
||||
'keyword',
|
||||
{ placeholder: '搜索域名', clearable: true, class: 'w-64' },
|
||||
{ showLabel: false, showFeedback: false },
|
||||
),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => (
|
||||
<NSpace>
|
||||
<NButton type="primary" onClick={() => handleSearch()}>
|
||||
搜索
|
||||
</NButton>
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const { component: FilterForm, fetch: formFetchSearch } = useForm<DomainTransferListRequest>({
|
||||
config: formConfig(),
|
||||
defaultValue: filterFormData,
|
||||
request: handleSearch,
|
||||
})
|
||||
|
||||
onMounted(gFetch)
|
||||
|
||||
return {
|
||||
TableComponent,
|
||||
PageComponent,
|
||||
loading,
|
||||
tableData,
|
||||
FilterForm,
|
||||
formFetchSearch,
|
||||
TransferCardList,
|
||||
// 页面动作
|
||||
handleSearch,
|
||||
openTransferDialog,
|
||||
// 对话框动作
|
||||
checkDomains,
|
||||
loadTransferPrice,
|
||||
createOrder,
|
||||
handleSelectRealName,
|
||||
loadRealNameOptions,
|
||||
closeTransferDialog,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { fetchDomainTransferList } from '@/api/domain'
|
||||
import type { TableResponse } from '@baota/naive-ui/types/table'
|
||||
import type { DomainTransferItem, DomainTransferListRequest } from '@/types/transfer'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
export const useTransferJoinStore = defineStore('domain-join-store', () => {
|
||||
const loading = ref(false)
|
||||
const filterFormData = ref<DomainTransferListRequest>({ p: 1, rows: 10 })
|
||||
const { handleError } = useError()
|
||||
|
||||
// 对话框状态
|
||||
const transferStep = ref<1 | 2 | 3>(1)
|
||||
const rows = ref<Array<{ domain: string; transfer_code: string }>>([{ domain: '', transfer_code: '' }])
|
||||
const selectedTemplateId = ref<number | null>(null)
|
||||
const agree = ref<boolean>(false)
|
||||
const payChannel = ref<'balance' | 'wechat' | 'alipay'>('wechat')
|
||||
const orderInfo = ref<{ order_no: string; total_price: number; wx: string; ali: string } | null>(null)
|
||||
const balanceAvailable = ref<number>(0)
|
||||
const realNameOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
|
||||
// 单个域名模式的验证状态
|
||||
const rowValidation = ref<Array<{
|
||||
domainError: string
|
||||
transferCodeError: string
|
||||
}>>([])
|
||||
|
||||
// 表单是否有效的计算属性
|
||||
const isFormValid = computed(() => {
|
||||
if (rows.value.length === 0) return false
|
||||
// 检查是否有非空的域名和转移码,且没有验证错误
|
||||
const hasValidRows = rows.value.some(row => row.domain.trim() && row.transfer_code.trim())
|
||||
if (!hasValidRows) return false
|
||||
|
||||
const hasErrors = rowValidation.value.some((validation, index) => {
|
||||
if (index >= rows.value.length) return false
|
||||
const row = rows.value[index]
|
||||
if (!row) return false
|
||||
return (row.domain.trim() && validation.domainError) ||
|
||||
(row.transfer_code.trim() && validation.transferCodeError)
|
||||
})
|
||||
return !hasErrors
|
||||
})
|
||||
|
||||
// 价格查询
|
||||
const transferPriceLoading = ref(false)
|
||||
const transferPriceList = ref<Array<{ domain: string; price: number; error?: string }>>([])
|
||||
const transferPriceTotal = ref<number>(0)
|
||||
|
||||
const setRealNameOptions = (ops: Array<{ label: string; value: number }>) => {
|
||||
realNameOptions.value = ops || []
|
||||
}
|
||||
const setTransferPrice = (list: Array<{ domain: string; price: number; error?: string }>) => {
|
||||
transferPriceList.value = Array.isArray(list) ? list : []
|
||||
transferPriceTotal.value = transferPriceList.value.reduce((s, it) => s + Number(it.price || 0), 0)
|
||||
}
|
||||
const setTransferPriceLoading = (v: boolean) => (transferPriceLoading.value = !!v)
|
||||
|
||||
const setStep = (s: 1 | 2 | 3) => (transferStep.value = s)
|
||||
const setSelectedRealNameId = (id: number | null) => (selectedTemplateId.value = id)
|
||||
const addRow = () => rows.value.push({ domain: '', transfer_code: '' })
|
||||
const removeRow = (idx: number) => { if (rows.value.length > 1) rows.value.splice(idx, 1) }
|
||||
const setRowField = (idx: number, key: 'domain' | 'transfer_code', val: string) => {
|
||||
if (!rows.value[idx]) return
|
||||
rows.value[idx][key] = val
|
||||
}
|
||||
const resetDialog = () => {
|
||||
transferStep.value = 1
|
||||
rows.value = [{ domain: '', transfer_code: '' }]
|
||||
selectedTemplateId.value = null
|
||||
agree.value = false
|
||||
payChannel.value = 'wechat'
|
||||
orderInfo.value = null
|
||||
realNameOptions.value = []
|
||||
transferPriceLoading.value = false
|
||||
transferPriceList.value = []
|
||||
transferPriceTotal.value = 0
|
||||
// 重置验证状态
|
||||
rowValidation.value = []
|
||||
}
|
||||
|
||||
// 验证方法
|
||||
const validateDomain = (domain: string): string => {
|
||||
if (!domain.trim()) return '域名不能为空'
|
||||
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/
|
||||
return domainRegex.test(domain.trim()) ? '' : '域名格式不正确,请输入有效的域名格式,如:example.com'
|
||||
}
|
||||
|
||||
const validateTransferCode = (code: string): string => {
|
||||
if (!code.trim()) return '转移码不能为空'
|
||||
if (code.trim().length < 6) return '转移码长度不能少于6位'
|
||||
return ''
|
||||
}
|
||||
|
||||
// 验证单个输入行
|
||||
const validateRow = (index: number, field: 'domain' | 'transfer_code', value: string) => {
|
||||
// 确保验证数组有足够的长度
|
||||
while (rowValidation.value.length <= index) {
|
||||
rowValidation.value.push({ domainError: '', transferCodeError: '' })
|
||||
}
|
||||
|
||||
const validation = rowValidation.value[index]
|
||||
if (!validation) return
|
||||
|
||||
if (field === 'domain') {
|
||||
validation.domainError = validateDomain(value)
|
||||
} else {
|
||||
validation.transferCodeError = validateTransferCode(value)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTransferListData = async <T = DomainTransferItem,>(
|
||||
params: DomainTransferListRequest = {},
|
||||
): Promise<TableResponse<T>> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { fetch, data } = fetchDomainTransferList(params)
|
||||
await fetch()
|
||||
const payload = data.value?.data
|
||||
return { list: (payload?.list as unknown as T[]) || [], total: Number(payload?.total || 0) }
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
message.error('加载转入列表失败')
|
||||
return { list: [] as unknown as T[], total: 0 }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
filterFormData,
|
||||
fetchTransferListData,
|
||||
|
||||
// 对话框状态与方法
|
||||
transferStep,
|
||||
rows,
|
||||
selectedTemplateId,
|
||||
setSelectedRealNameId,
|
||||
agree,
|
||||
payChannel,
|
||||
orderInfo,
|
||||
balanceAvailable,
|
||||
realNameOptions,
|
||||
transferPriceLoading,
|
||||
transferPriceList,
|
||||
transferPriceTotal,
|
||||
setRealNameOptions,
|
||||
setTransferPrice,
|
||||
setTransferPriceLoading,
|
||||
setStep,
|
||||
addRow,
|
||||
removeRow,
|
||||
setRowField,
|
||||
resetDialog,
|
||||
|
||||
// 验证相关
|
||||
rowValidation,
|
||||
isFormValid,
|
||||
validateDomain,
|
||||
validateTransferCode,
|
||||
validateRow,
|
||||
}
|
||||
})
|
||||
|
||||
export const useTransferJoinState = () => {
|
||||
const store = useTransferJoinStore()
|
||||
return { ...store, ...storeToRefs(store) }
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineComponent, computed, onUnmounted } from 'vue'
|
||||
import { NButton, NFlex, NGrid, NGridItem, NInput, NInputGroup, NSteps, NStep, NSelect, NCheckbox, NRadioButton, NRadioGroup, NTag, NQrCode, NDynamicInput, NAlert, NTooltip, NIcon } from 'naive-ui'
|
||||
import { useTransferState } from '../useStore'
|
||||
import { useController } from '../useController'
|
||||
import { useTransferJoinState } from './JoinIn/useStore'
|
||||
import { useController } from './JoinIn/useController'
|
||||
import { queryPaymentStatus, buyByBalance } from '@/api/order'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { InformationCircleOutline } from '@vicons/ionicons5'
|
||||
|
|
@ -22,7 +22,7 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
setup(props) {
|
||||
const state = useTransferState()
|
||||
const state = useTransferJoinState()
|
||||
const { checkDomains, createOrder, handleSelectRealName, loadRealNameOptions } = useController()
|
||||
const message = useMessage()
|
||||
const qrLink = computed(() => state.payChannel.value === 'wechat' ? state.orderInfo.value?.wx : state.orderInfo.value?.ali)
|
||||
|
|
|
|||
|
|
@ -1,119 +1,41 @@
|
|||
import { defineComponent, ref } from 'vue'
|
||||
import { NCard, NFlex, NButton, NDivider, NIcon, NAlert } from 'naive-ui'
|
||||
import { CloseOutline, SearchOutline } from '@vicons/ionicons5'
|
||||
/**
|
||||
* 域名转入转出
|
||||
*/
|
||||
import { defineComponent, defineAsyncComponent } from 'vue'
|
||||
import { NTabs, NTabPane, NCard } from 'naive-ui'
|
||||
import { useController } from './useController'
|
||||
import { useApp } from '@/components/layout/useStore'
|
||||
import type{ DomainTransferTabKey } from './types'
|
||||
|
||||
/**
|
||||
* 页面组件
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'DomainTransferView',
|
||||
setup() {
|
||||
const { TableComponent, PageComponent, FilterForm, openTransferDialog, formFetchSearch } = useController()
|
||||
const { isMobile } = useApp()
|
||||
const JoinIn = defineAsyncComponent(() => import('./components/JoinIn'))
|
||||
|
||||
// 移动端搜索表单显示状态
|
||||
const showSearchForm = ref(false)
|
||||
const toggleSearchForm = () => {
|
||||
showSearchForm.value = !showSearchForm.value
|
||||
}
|
||||
// 获取控制器
|
||||
const { loading, activeTab, switchTab } = useController()
|
||||
|
||||
const renderFilterSection = () => (
|
||||
<NCard class="card-shadow" bordered={false}>
|
||||
{isMobile.value ? (
|
||||
<NFlex vertical class="w-full" size="medium">
|
||||
<NFlex justify="space-between" align="center">
|
||||
<NButton type="primary" onClick={() => openTransferDialog()}>
|
||||
域名转入
|
||||
</NButton>
|
||||
<NButton onClick={toggleSearchForm} class="search-toggle-btn">
|
||||
{showSearchForm.value ? (
|
||||
<>
|
||||
<NIcon size="18">
|
||||
<CloseOutline />
|
||||
</NIcon>
|
||||
<span>关闭</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NIcon size="18">
|
||||
<SearchOutline />
|
||||
</NIcon>
|
||||
<span>搜索</span>
|
||||
</>
|
||||
)}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
{showSearchForm.value && (
|
||||
<>
|
||||
<NDivider class="!my-2" dashed />
|
||||
<div class="mobile-search-form">
|
||||
<FilterForm inline={false} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</NFlex>
|
||||
) : (
|
||||
<NFlex justify="space-between" align="center" class="w-full">
|
||||
<NFlex class="w-full !flex-row !flex-wrap" size="medium">
|
||||
<div>
|
||||
<NButton type="primary" onClick={() => openTransferDialog()}>
|
||||
域名转入
|
||||
</NButton>
|
||||
<a
|
||||
class="ml-4 text-[#20a53a] decoration-none hover:text-[#20a53a]-800 text-sm cursor-pointer"
|
||||
href="https://docs.bt.cn/domain/user-guide/domain-transfer-in"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
帮助文档
|
||||
</a>
|
||||
</div>
|
||||
<FilterForm class="flex-1 justify-end" inline={true} />
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
)}
|
||||
</NCard>
|
||||
)
|
||||
|
||||
const renderList = () => (
|
||||
<>
|
||||
{isMobile.value ? (
|
||||
<NFlex vertical>
|
||||
<NCard class="card-shadow" bordered={false}>
|
||||
<TableComponent />
|
||||
<NFlex justify="center">
|
||||
<PageComponent />
|
||||
</NFlex>
|
||||
</NCard>
|
||||
</NFlex>
|
||||
) : (
|
||||
<NCard class="card-shadow" bordered={false}>
|
||||
<TableComponent max-height={`calc(100vh - 560px)`} class="mb-4" />
|
||||
<NFlex justify="end">
|
||||
<PageComponent />
|
||||
</NFlex>
|
||||
</NCard>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
onMounted(() => {formFetchSearch()})
|
||||
return () => (
|
||||
<div class="flex flex-col justify-between min-h-[calc(100vh-160px)]">
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
{renderFilterSection()}
|
||||
{renderList()}
|
||||
</div>
|
||||
<NAlert title="重要提示" type="info" class="mt-4">
|
||||
<div class="mt-2">
|
||||
<ul class="list-disc text-sm text-gray-700 leading-relaxed">
|
||||
<li>如域名有效期已达10年(最长期限),则转入时仍需扣除费用但年限不增加。域名续费不足45天时不建议转入,否则可能导致年限减少。</li>
|
||||
<li>根据ICANN 相关规定,域名注册、转入后的60天内将不允许转移注册商。</li>
|
||||
<li>域名转入时,注册局需要重新对转入的域名进行前缀的合法性命名审核,如果合法命名审核无法通过,域名将无法转入。</li>
|
||||
<li>域名转入时必须选择已实名认证的域名信息模板。域名转入一般需要 3~7个工作日,具体取决于注册局审核时间。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</NAlert>
|
||||
<div class="domain-transfer-container">
|
||||
<NCard class="card-shadow" bordered={false}>
|
||||
{/* 标签页导航区 */}
|
||||
<NTabs
|
||||
value={activeTab.value}
|
||||
onUpdateValue={(val: DomainTransferTabKey) => switchTab(val)}
|
||||
type="line"
|
||||
animated
|
||||
class="mb-4"
|
||||
>
|
||||
<NTabPane name="join" tab="域名转入">
|
||||
<JoinIn/>
|
||||
</NTabPane>
|
||||
<NTabPane name="level" tab="域名转出">
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NCard>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* 域名转入转出页面标签页类型
|
||||
*/
|
||||
export type DomainTransferTabKey = 'join' | 'level'
|
||||
|
|
@ -1,361 +1,43 @@
|
|||
import { ref } from 'vue'
|
||||
import { NTag, NButton, NSpace, NTooltip, NIcon } from 'naive-ui'
|
||||
import { useTable, useForm, useFormHooks } from '@baota/naive-ui/hooks'
|
||||
import { useTransferState } from './useStore'
|
||||
import type { DomainTransferItem, DomainTransferListRequest } from '@/types/transfer'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import type { ContactTemplateItem } from '@/types/real-name'
|
||||
import { formatDate } from '@baota/utils/date'
|
||||
import { createTransferOrder, cancelTransfer } from '@/api/transfer'
|
||||
import { fetchContactUserDetail } from '@/api/real-name'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useRechargeState } from '@/views/recharge/useStore'
|
||||
import { useRechargeController } from '@/views/recharge/useController'
|
||||
import { useModal, useDialog } from '@baota/naive-ui/hooks'
|
||||
import TransferDialog from './components/TransferDialog'
|
||||
import TransferDetailsDialog from './components/TransferDetailsDialog'
|
||||
import DomainRegistrationForm from '@/views/real-name/components/DomainRegistrationForm/index'
|
||||
import { queryDomainPrice } from '@/api/domain'
|
||||
import { InformationCircleOutline } from '@vicons/ionicons5'
|
||||
/**
|
||||
* 域名详情页面控制器
|
||||
* 负责处理业务逻辑、事件响应和生命周期管理
|
||||
*/
|
||||
|
||||
const STATUS_TYPE: Record<number, 'default' | 'success' | 'warning' | 'error' | 'info'> = {
|
||||
0: 'warning', // 申请已提交
|
||||
1: 'error', // 申请失败
|
||||
2: 'default', // 取消转入
|
||||
3: 'error', // 转入失败
|
||||
4: 'success', // 转入成功
|
||||
}
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useDomainTransferStore } from './useStore'
|
||||
import type { DomainTransferTabKey } from './types.d'
|
||||
|
||||
const openTransferRef = ref()
|
||||
const openTransferDetailsRef = ref()
|
||||
/**
|
||||
* 域名转入转出页面控制器
|
||||
*/
|
||||
export function useController() {
|
||||
const { fetchTransferListData, filterFormData } = useTransferState()
|
||||
const store = useTransferState()
|
||||
const { useFormInput } = useFormHooks()
|
||||
const recharge = useRechargeState()
|
||||
const { loadAccountBalance } = useRechargeController()
|
||||
const message = useMessage()
|
||||
// 获取状态管理
|
||||
const { loading } = useDomainTransferStore()
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref<DomainTransferTabKey>('join')
|
||||
|
||||
/**
|
||||
* 处理取消订单
|
||||
* @param order 订单信息
|
||||
* 切换标签页
|
||||
* @param tab 标签页键值
|
||||
*/
|
||||
async function handleCancelTransfer(record_id: number) {
|
||||
useDialog({
|
||||
type: 'warning',
|
||||
title: '确认取消',
|
||||
area: '40',
|
||||
content: '确定要取消此转入申请吗?此操作不可恢复。',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
const { fetch, message } = cancelTransfer({ record_id })
|
||||
message.value = true
|
||||
await fetch()
|
||||
await gFetch()
|
||||
} catch (error) {
|
||||
console.error('删除模板失败:', error)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
// 打开转入对话框(可选预填)
|
||||
async function openTransferDetails(row: DomainTransferItem) {
|
||||
openTransferDetailsRef.value = useModal({
|
||||
title: '失败详情',
|
||||
area: '600px',
|
||||
component: TransferDetailsDialog,
|
||||
componentProps: {
|
||||
record: row,
|
||||
close: () => {
|
||||
openTransferDetailsRef.value?.close()
|
||||
},
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
const switchTab = (tab: DomainTransferTabKey) => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<DomainTransferItem> = [
|
||||
{ title: '域名', key: 'domain', width: 260 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status_text',
|
||||
width: 140,
|
||||
render: (r: DomainTransferItem) => {
|
||||
const tagElement = (
|
||||
<NTag type={STATUS_TYPE[r.status] || 'default'} bordered={false} size="small">
|
||||
{r.status_text}
|
||||
</NTag>
|
||||
)
|
||||
|
||||
if (r.status === 0) {
|
||||
return (
|
||||
<div class="flex items-center">
|
||||
{tagElement}
|
||||
<NTooltip trigger="click">
|
||||
{{
|
||||
default: () => (
|
||||
<span>
|
||||
域名转入未完成?请参考帮助文档:
|
||||
<a
|
||||
class="text-[#20a53a] hover:text-[#20a53a]-800 text-sm underline cursor-pointer ml-1"
|
||||
href="https://docs.bt.cn/domain/user-guide/domain-transfer-in"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
帮助文档
|
||||
</a>
|
||||
</span>
|
||||
),
|
||||
trigger: () => (
|
||||
<NIcon class="flex justify-center cursor-pointer ml-1">
|
||||
<InformationCircleOutline />
|
||||
</NIcon>
|
||||
),
|
||||
}}
|
||||
</NTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return tagElement
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '转入提交时间',
|
||||
key: 'created_at',
|
||||
render: (r: DomainTransferItem) => formatDate(r?.created_at || 0, 'yyyy-MM-dd'),
|
||||
},
|
||||
{
|
||||
title: '转入流程结束时间',
|
||||
key: 'complete_time',
|
||||
render: (r: DomainTransferItem) => formatDate(r?.complete_time || 0, 'yyyy-MM-dd'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
render: (r: DomainTransferItem) => (
|
||||
<NSpace justify="end">
|
||||
{r.status === 0 && (
|
||||
<NButton size="small" ghost onClick={() => handleCancelTransfer(r.id)}>
|
||||
取消
|
||||
</NButton>
|
||||
)}
|
||||
{(r.status === 1 || r.status === 3) && (
|
||||
<NButton size="small" ghost onClick={() => openTransferDetails(r)}>
|
||||
详情
|
||||
</NButton>
|
||||
)}
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const {
|
||||
TableComponent,
|
||||
PageComponent,
|
||||
data,
|
||||
loading,
|
||||
fetch: gFetch,
|
||||
param,
|
||||
} = useTable<DomainTransferItem, DomainTransferListRequest>({
|
||||
config: columns,
|
||||
request: fetchTransferListData,
|
||||
defaultValue: filterFormData,
|
||||
alias: { page: 'p', pageSize: 'rows' },
|
||||
watchValue: ['p', 'rows', 'keyword'],
|
||||
// 组件挂载时获取域名详情
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
|
||||
// 顶部搜索
|
||||
async function handleSearch() {
|
||||
await gFetch()
|
||||
}
|
||||
|
||||
// 打开转入对话框(可选预填)
|
||||
async function openTransferDialog(preset?: { domain?: string }) {
|
||||
// 先重置,避免覆盖随后加载的实名模板 options
|
||||
store.resetDialog()
|
||||
await loadRealNameOptions()
|
||||
if (preset?.domain) store.rows.value = [{ domain: preset.domain, transfer_code: '' }]
|
||||
openTransferRef.value = useModal({
|
||||
title: '域名转入',
|
||||
area: '720px',
|
||||
component: TransferDialog,
|
||||
componentProps: {
|
||||
refresh: gFetch,
|
||||
close: () => openTransferRef.value?.close?.(),
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
}
|
||||
|
||||
function closeTransferDialog() {
|
||||
openTransferRef.value?.close?.()
|
||||
}
|
||||
|
||||
// 查询域名(仅校验非空 → Step2)
|
||||
async function checkDomains() {
|
||||
const rows = store.rows.value.map(r => ({ domain: r.domain.trim().toLowerCase(), transfer_code: r.transfer_code.trim() }))
|
||||
const hasMissingCode = rows.some(r => r.domain && !r.transfer_code)
|
||||
if (hasMissingCode) { message.warning('转移码不能为空'); return }
|
||||
const valid = rows.filter(r => r.domain && r.transfer_code)
|
||||
if (valid.length === 0) { message.warning('请填写至少一条域名与转移码'); return }
|
||||
const seen = new Set<string>()
|
||||
for (const r of valid) { if (seen.has(r.domain)) { message.warning('域名已存在'); return } seen.add(r.domain) }
|
||||
store.setStep(2)
|
||||
await loadTransferPrice(valid.map(v => v.domain))
|
||||
}
|
||||
|
||||
async function loadTransferPrice(domains: string[]) {
|
||||
try {
|
||||
store.setTransferPriceLoading(true)
|
||||
const { fetch, data } = queryDomainPrice({ domain: domains.join(','), year: 1, type: 'transfer' } as any)
|
||||
await fetch()
|
||||
const results = (data.value as any)?.data?.results || []
|
||||
const list = Array.isArray(results)
|
||||
? results.map((it: any) => ({ domain: String(it.domain || ''), price: Number(it.price || 0), error: String(it.error || '') }))
|
||||
: []
|
||||
console.log(list,'--');
|
||||
store.setTransferPrice(list)
|
||||
} catch {
|
||||
store.setTransferPrice([])
|
||||
} finally {
|
||||
store.setTransferPriceLoading(false)
|
||||
}
|
||||
}
|
||||
/** 选择实名模板 */
|
||||
const handleSelectRealName = (val: number) => {
|
||||
if (val === -1) openCreateRealNameModal()
|
||||
else store.setSelectedRealNameId(val)
|
||||
}
|
||||
/** 创建实名模板窗口(步骤一入口中的快捷按钮) */
|
||||
const openCreateRealNameModal = () => {
|
||||
const modal = useModal({
|
||||
title: '创建实名模板',
|
||||
area: '1000px',
|
||||
component: DomainRegistrationForm,
|
||||
componentProps: {
|
||||
mode: 'add',
|
||||
refresh: async () => {
|
||||
await loadRealNameOptions()
|
||||
},
|
||||
},
|
||||
footer: false,
|
||||
})
|
||||
return modal
|
||||
}
|
||||
|
||||
// 优化:加载实名模板并写入 Store,失败兜底
|
||||
async function loadRealNameOptions() {
|
||||
try {
|
||||
const { fetch: dFetch, data: dData } = fetchContactUserDetail({ p: 1, rows: 50,status:2 })
|
||||
await dFetch()
|
||||
const payload = dData.value as any
|
||||
const list: ContactTemplateItem[] = ((payload && payload.msg && payload.msg.data) ||
|
||||
(payload && payload.data && payload.data.data) ||
|
||||
[]) as ContactTemplateItem[]
|
||||
const options = Array.isArray(list)
|
||||
? list.map((it) => ({ label: it.template_name || it.owner_name || String(it.id), value: it.id }))
|
||||
: []
|
||||
store.setRealNameOptions(options)
|
||||
// 默认选中第一个
|
||||
store.setSelectedRealNameId(options[0]?.value || null)
|
||||
} catch {
|
||||
message.error('加载实名模板失败')
|
||||
store.setRealNameOptions([])
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Step2 -> Step3 创建订单
|
||||
async function createOrder() {
|
||||
if (!store.selectedTemplateId.value) {
|
||||
message.warning('请选择实名模板')
|
||||
return
|
||||
}
|
||||
if (!store.agree.value) {
|
||||
message.warning('请勾选并同意相关协议')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
domain_list: store.rows.value
|
||||
.map((r) => ({ domain: r.domain.trim(), transfer_code: r.transfer_code.trim() }))
|
||||
.filter((r) => r.domain && r.transfer_code),
|
||||
real_name_template_id: store.selectedTemplateId.value as number,
|
||||
}
|
||||
const { fetch, data } = createTransferOrder(payload)
|
||||
await fetch()
|
||||
if (!data.value?.status) {
|
||||
message.error(data.value?.msg || '创建订单失败')
|
||||
return
|
||||
}
|
||||
store.orderInfo.value = data.value.data as any
|
||||
store.setStep(3)
|
||||
await ensureBalance()
|
||||
}
|
||||
|
||||
// 余额获取
|
||||
async function ensureBalance() {
|
||||
try {
|
||||
await loadAccountBalance()
|
||||
store.balanceAvailable.value = Number(recharge.overview.value?.balance || 0)
|
||||
} catch {
|
||||
store.balanceAvailable.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/** 表单配置(列表页顶部搜索) */
|
||||
const formConfig = () => [
|
||||
useFormInput(
|
||||
'',
|
||||
'keyword',
|
||||
{ placeholder: '搜索域名', clearable: true, class: 'w-64' },
|
||||
{ showLabel: false, showFeedback: false },
|
||||
),
|
||||
{
|
||||
type: 'custom' as const,
|
||||
render: () => (
|
||||
<NSpace>
|
||||
<NButton type="primary" onClick={() => handleSearch()}>
|
||||
搜索
|
||||
</NButton>
|
||||
</NSpace>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const { component: FilterForm, fetch: formFetchSearch } = useForm<DomainTransferListRequest>({
|
||||
config: formConfig(),
|
||||
defaultValue: filterFormData,
|
||||
request: handleSearch,
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
TableComponent,
|
||||
PageComponent,
|
||||
// 状态
|
||||
loading,
|
||||
data,
|
||||
FilterForm,
|
||||
formFetchSearch,
|
||||
// 页面动作
|
||||
handleSearch,
|
||||
openTransferDialog,
|
||||
// 对话框动作
|
||||
checkDomains,
|
||||
loadTransferPrice,
|
||||
createOrder,
|
||||
handleSelectRealName,
|
||||
loadRealNameOptions,
|
||||
closeTransferDialog,
|
||||
|
||||
activeTab,
|
||||
switchTab,
|
||||
// 方法
|
||||
|
||||
// 工具
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,173 +1,32 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { useMessage } from '@baota/naive-ui/hooks'
|
||||
import { useError } from '@baota/hooks/error'
|
||||
import { fetchDomainTransferList } from '@/api/domain'
|
||||
import type { TableResponse } from '@baota/naive-ui/types/table'
|
||||
import type { DomainTransferItem, DomainTransferListRequest } from '@/types/transfer'
|
||||
/**
|
||||
* 域名转入转出页面状态管理
|
||||
* 负责管理域名转入转出页面的数据、状态和操作
|
||||
*/
|
||||
|
||||
const message = useMessage()
|
||||
import { useError } from "@baota/hooks/error";
|
||||
|
||||
export const useTransferStore = defineStore('domain-transfer-store', () => {
|
||||
const { handleError } = useError();
|
||||
|
||||
/**
|
||||
* 域名转入转出页面状态Store
|
||||
*/
|
||||
export const useDomainTransferStore = defineStore("domain-transfer-store", () => {
|
||||
// -------------------- 状态定义 --------------------
|
||||
|
||||
/** 页面加载状态 */
|
||||
const loading = ref(false)
|
||||
const filterFormData = ref<DomainTransferListRequest>({ p: 1, rows: 10 })
|
||||
const { handleError } = useError()
|
||||
|
||||
// 对话框状态
|
||||
const transferStep = ref<1 | 2 | 3>(1)
|
||||
const rows = ref<Array<{ domain: string; transfer_code: string }>>([{ domain: '', transfer_code: '' }])
|
||||
const selectedTemplateId = ref<number | null>(null)
|
||||
const agree = ref<boolean>(false)
|
||||
const payChannel = ref<'balance' | 'wechat' | 'alipay'>('wechat')
|
||||
const orderInfo = ref<{ order_no: string; total_price: number; wx: string; ali: string } | null>(null)
|
||||
const balanceAvailable = ref<number>(0)
|
||||
const realNameOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
|
||||
// 单个域名模式的验证状态
|
||||
const rowValidation = ref<Array<{
|
||||
domainError: string
|
||||
transferCodeError: string
|
||||
}>>([])
|
||||
|
||||
// 表单是否有效的计算属性
|
||||
const isFormValid = computed(() => {
|
||||
if (rows.value.length === 0) return false
|
||||
// 检查是否有非空的域名和转移码,且没有验证错误
|
||||
const hasValidRows = rows.value.some(row => row.domain.trim() && row.transfer_code.trim())
|
||||
if (!hasValidRows) return false
|
||||
|
||||
const hasErrors = rowValidation.value.some((validation, index) => {
|
||||
if (index >= rows.value.length) return false
|
||||
const row = rows.value[index]
|
||||
if (!row) return false
|
||||
return (row.domain.trim() && validation.domainError) ||
|
||||
(row.transfer_code.trim() && validation.transferCodeError)
|
||||
})
|
||||
return !hasErrors
|
||||
})
|
||||
|
||||
// 价格查询
|
||||
const transferPriceLoading = ref(false)
|
||||
const transferPriceList = ref<Array<{ domain: string; price: number; error?: string }>>([])
|
||||
const transferPriceTotal = ref<number>(0)
|
||||
|
||||
const setRealNameOptions = (ops: Array<{ label: string; value: number }>) => {
|
||||
realNameOptions.value = ops || []
|
||||
}
|
||||
const setTransferPrice = (list: Array<{ domain: string; price: number; error?: string }>) => {
|
||||
transferPriceList.value = Array.isArray(list) ? list : []
|
||||
transferPriceTotal.value = transferPriceList.value.reduce((s, it) => s + Number(it.price || 0), 0)
|
||||
}
|
||||
const setTransferPriceLoading = (v: boolean) => (transferPriceLoading.value = !!v)
|
||||
|
||||
const setStep = (s: 1 | 2 | 3) => (transferStep.value = s)
|
||||
const setSelectedRealNameId = (id: number | null) => (selectedTemplateId.value = id)
|
||||
const addRow = () => rows.value.push({ domain: '', transfer_code: '' })
|
||||
const removeRow = (idx: number) => { if (rows.value.length > 1) rows.value.splice(idx, 1) }
|
||||
const setRowField = (idx: number, key: 'domain' | 'transfer_code', val: string) => {
|
||||
if (!rows.value[idx]) return
|
||||
rows.value[idx][key] = val
|
||||
}
|
||||
const resetDialog = () => {
|
||||
transferStep.value = 1
|
||||
rows.value = [{ domain: '', transfer_code: '' }]
|
||||
selectedTemplateId.value = null
|
||||
agree.value = false
|
||||
payChannel.value = 'wechat'
|
||||
orderInfo.value = null
|
||||
realNameOptions.value = []
|
||||
transferPriceLoading.value = false
|
||||
transferPriceList.value = []
|
||||
transferPriceTotal.value = 0
|
||||
// 重置验证状态
|
||||
rowValidation.value = []
|
||||
}
|
||||
|
||||
// 验证方法
|
||||
const validateDomain = (domain: string): string => {
|
||||
if (!domain.trim()) return '域名不能为空'
|
||||
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/
|
||||
return domainRegex.test(domain.trim()) ? '' : '域名格式不正确,请输入有效的域名格式,如:example.com'
|
||||
}
|
||||
|
||||
const validateTransferCode = (code: string): string => {
|
||||
if (!code.trim()) return '转移码不能为空'
|
||||
if (code.trim().length < 6) return '转移码长度不能少于6位'
|
||||
return ''
|
||||
}
|
||||
|
||||
// 验证单个输入行
|
||||
const validateRow = (index: number, field: 'domain' | 'transfer_code', value: string) => {
|
||||
// 确保验证数组有足够的长度
|
||||
while (rowValidation.value.length <= index) {
|
||||
rowValidation.value.push({ domainError: '', transferCodeError: '' })
|
||||
}
|
||||
|
||||
const validation = rowValidation.value[index]
|
||||
if (!validation) return
|
||||
|
||||
if (field === 'domain') {
|
||||
validation.domainError = validateDomain(value)
|
||||
} else {
|
||||
validation.transferCodeError = validateTransferCode(value)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTransferListData = async <T = DomainTransferItem,>(
|
||||
params: DomainTransferListRequest = {},
|
||||
): Promise<TableResponse<T>> => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { fetch, data } = fetchDomainTransferList(params)
|
||||
await fetch()
|
||||
const payload = data.value?.data
|
||||
return { list: (payload?.list as unknown as T[]) || [], total: Number(payload?.total || 0) }
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
message.error('加载转入列表失败')
|
||||
return { list: [] as unknown as T[], total: 0 }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// -------------------- 方法定义 --------------------
|
||||
|
||||
return {
|
||||
// 状态
|
||||
loading,
|
||||
filterFormData,
|
||||
fetchTransferListData,
|
||||
|
||||
// 对话框状态与方法
|
||||
transferStep,
|
||||
rows,
|
||||
selectedTemplateId,
|
||||
setSelectedRealNameId,
|
||||
agree,
|
||||
payChannel,
|
||||
orderInfo,
|
||||
balanceAvailable,
|
||||
realNameOptions,
|
||||
transferPriceLoading,
|
||||
transferPriceList,
|
||||
transferPriceTotal,
|
||||
setRealNameOptions,
|
||||
setTransferPrice,
|
||||
setTransferPriceLoading,
|
||||
setStep,
|
||||
addRow,
|
||||
removeRow,
|
||||
setRowField,
|
||||
resetDialog,
|
||||
|
||||
// 验证相关
|
||||
rowValidation,
|
||||
isFormValid,
|
||||
validateDomain,
|
||||
validateTransferCode,
|
||||
validateRow,
|
||||
// 方法
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export const useTransferState = () => {
|
||||
const store = useTransferStore()
|
||||
return { ...store, ...storeToRefs(store) }
|
||||
}
|
||||
export const useDomainDetailState = () => {
|
||||
const store = useDomainTransferStore()
|
||||
return { ...store, ...storeToRefs(store) };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,5 +29,5 @@
|
|||
"src/**/*.js",
|
||||
"src/**/*.jsx",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
, "src/views/domain-security/index.tsx" ]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,16 +144,16 @@ export default defineConfig({
|
|||
},
|
||||
fontSize: {
|
||||
// Naive UI 兼容的字体大小
|
||||
xs: ['0.75rem', { lineHeight: '1rem' }],
|
||||
sm: ['0.875rem', { lineHeight: '1.25rem' }],
|
||||
base: ['1rem', { lineHeight: '1.5rem' }],
|
||||
lg: ['1.125rem', { lineHeight: '1.75rem' }],
|
||||
xl: ['1.25rem', { lineHeight: '1.75rem' }],
|
||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
||||
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
|
||||
'5xl': ['3rem', { lineHeight: '1' }],
|
||||
'6xl': ['3.75rem', { lineHeight: '1' }],
|
||||
xs: ['0.75rem', { 'line-height': '1rem' }],
|
||||
sm: ['0.875rem', { 'line-height': '1.25rem' }],
|
||||
base: ['1rem', { 'line-height': '1.5rem' }],
|
||||
lg: ['1.125rem', { 'line-height': '1.75rem' }],
|
||||
xl: ['1.25rem', { 'line-height': '1.75rem' }],
|
||||
'2xl': ['1.5rem', { 'line-height': '2rem' }],
|
||||
'3xl': ['1.875rem', { 'line-height': '2.25rem' }],
|
||||
'4xl': ['2.25rem', { 'line-height': '2.5rem' }],
|
||||
'5xl': ['3rem', { 'line-height': '1' }],
|
||||
'6xl': ['3.75rem', { 'line-height': '1' }],
|
||||
},
|
||||
spacing: {
|
||||
// Naive UI 兼容的间距系统
|
||||
|
|
|
|||
|
|
@ -28,6 +28,21 @@
|
|||
<div class="w-full lg:w-3/4">
|
||||
<!-- 域名结果列表 -->
|
||||
<section class="bg-white rounded-sm shadow-sm">
|
||||
<!-- Tab切换区域 - 紧凑版 -->
|
||||
<div class="domain-search-tabs">
|
||||
<div class="tab-buttons flex">
|
||||
<button id="tab-normal" class="tab-btn active">
|
||||
域名注册
|
||||
</button>
|
||||
<button id="tab-ai" class="tab-btn">
|
||||
<i class="fa fa-search"></i>AI域名推荐
|
||||
<span class="new-badge">new</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 域名注册搜索面板 -->
|
||||
<div id="normal-search-panel" class="search-panel">
|
||||
<div class="flex w-full mb-2">
|
||||
<!-- 搜索框和按钮 -->
|
||||
<div id="search-section" class="flex flex-col sm:flex-row gap-3 w-full">
|
||||
|
|
@ -37,6 +52,54 @@
|
|||
<i class="fa fa-times-circle clear-input-button visible" id="clear-input-button" title="清空输入"></i>
|
||||
</div>
|
||||
<button class="primary-action-button text-lg">重新查询</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI推荐搜索面板 -->
|
||||
<div id="ai-search-panel" class="search-panel hidden">
|
||||
<!-- AI推荐搜索框和按钮 -->
|
||||
<div class="flex w-full mb-2">
|
||||
<!-- 整体渐变容器 -->
|
||||
<div class="ai-search-container">
|
||||
<div class="ai-search-inner">
|
||||
<form autocomplete="off">
|
||||
<div id="ai-search-section" class="flex flex-col lg:flex-row gap-3 w-full">
|
||||
<!-- 品牌名称输入框 -->
|
||||
<div class="ai-input-group flex-1">
|
||||
<i class="ai-input-icon fa fa-building"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="brand-name-input"
|
||||
class="ai-search-input"
|
||||
placeholder="请输入品牌/公司/个人/产品名称"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 行业信息输入框 -->
|
||||
<div class="ai-input-group flex-1">
|
||||
<i class="ai-input-icon fa fa-industry"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="industry-input"
|
||||
class="ai-search-input"
|
||||
placeholder="请输入行业信息"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- AI推荐按钮 -->
|
||||
<button type="button" id="ai-recommend-btn" class="ai-transparent-button">
|
||||
<i class="ai-button-icon fa fa-search"></i>
|
||||
<span class="ai-button-text">AI推荐</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 筛选器 -->
|
||||
|
|
@ -185,7 +248,9 @@
|
|||
{{#end if}}
|
||||
<div class="flex items-center justify-between p-4 gap-3">
|
||||
<div class="flex items-center">
|
||||
<div class="font-bold text-sm mr-4 {{#if item.isRegistered}}line-through text-gray-400{{#end if}}">{{item.domain}}</div>
|
||||
<div class="text-sm mr-4">
|
||||
<div class="font-bold {{#if item.isRegistered}}line-through text-gray-400{{#end if}}">
|
||||
{{item.domain}}
|
||||
{{#if item.isRegistered}}
|
||||
<div class="status-badge text-gray-600 border-gray-800">{{item.statusText}}</div>
|
||||
{{#end if}}
|
||||
|
|
@ -201,6 +266,14 @@
|
|||
{{#if item.hasNew}}
|
||||
<div class="status-badge status-badge-new">新人特惠</div>
|
||||
{{#end if}}
|
||||
</div>
|
||||
{{#if item.hasAiMeaning}}
|
||||
<!-- AI推荐域名意义解释 -->
|
||||
<div class="domain-meaning text-gray-400">
|
||||
AI解读:{{item.meaning}}
|
||||
</div>
|
||||
{{#end if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if item.isRegistered}}
|
||||
<!-- 已注册:显示查看WHOIS链接 -->
|
||||
|
|
@ -212,13 +285,11 @@
|
|||
<div class="flex items-center gap-6 {{#if item.isRegistered}}!hidden{{/if}}">
|
||||
<div class="text-right relative price-tooltip-container">
|
||||
<div class="flex items-center justify-end price-trigger cursor-pointer">
|
||||
{{#if item.hasOriginalPrice}}
|
||||
<span class="text-secondary-60 line-through mr-2 text-xs">{{item.formattedOriginalPrice}}</span>
|
||||
{{#end if}}
|
||||
<span class="text-sm text-orange-500">{{item.formattedPrice}}元/首年</span>
|
||||
<i class="fa fa-caret-down text-gray-400 ml-1 hover:text-primary transition-colors"></i>
|
||||
</div>
|
||||
<div class="text-xs text-secondary-60 mr-4">续费{{item.formattedOriginalPrice}}/年</div>
|
||||
<div class="text-xs text-secondary-60 mr-4">续费{{item.renewPrice}}/年</div>
|
||||
|
||||
<!-- 价格悬浮窗 -->
|
||||
<div class="price-tooltip absolute right-0 top-full bg-white rounded-lg shadow-xl border px-4 py-4 opacity-0 invisible transition-all duration-200 ease-out transform translate-y-1" style="z-index: 9999">
|
||||
|
|
@ -324,7 +395,7 @@
|
|||
|
||||
<!-- Modal内容模板 -->
|
||||
<script type="text/template" id="modal-content-template">
|
||||
<div class="bg-white rounded-sm {{sizeClass}} w-full transform transition-all scale-95 opacity-0 modal-content">
|
||||
<div class="bg-white rounded-sm {{sizeClass}} w-full transform transition-all scale-95">
|
||||
<div class="flex justify-between items-center p-5 border-b border-gray-100">
|
||||
<h3 class="text-xl font-bold text-dark">{{title}}</h3>
|
||||
{{#if closable}}
|
||||
|
|
@ -980,150 +1051,6 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<!-- WHOIS查询模态窗口 -->
|
||||
<script type="text/template" id="whois-modal-template">
|
||||
<div class="whois-modal">
|
||||
<div class="flex flex-col">
|
||||
{{#if isLoading}}
|
||||
<div class="text-center py-12">
|
||||
<div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary mb-4"></div>
|
||||
<p class="text-gray-600">正在查询WHOIS信息...</p>
|
||||
</div>
|
||||
{{#end if}}
|
||||
|
||||
{{#if hasError}}
|
||||
<div class="text-center py-12">
|
||||
<div class="text-red-500 text-6xl mb-4">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<h4 class="text-xl font-semibold text-gray-800 mb-2">查询失败</h4>
|
||||
<p class="text-gray-600">{{errorMessage}}</p>
|
||||
</div>
|
||||
{{#end if}}
|
||||
|
||||
{{#if hasData}}
|
||||
<!-- Tab 切换导航 -->
|
||||
<div class="whois-tab-container flex">
|
||||
<button class="whois-tab-btn active" data-tab="raw">
|
||||
<i class="fa fa-code mr-2"></i>原始数据
|
||||
</button>
|
||||
<button class="whois-tab-btn" data-tab="optimized">
|
||||
<i class="fa fa-eye mr-2"></i>优化视图
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 原始数据视图 -->
|
||||
<div id="whois-raw-view" class="whois-tab-content active">
|
||||
<div class="bg-gray-900 text-green-400 p-4 font-mono text-sm overflow-auto h-140">
|
||||
<pre class="whitespace-pre-wrap">{{rawData}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 优化视图内容 -->
|
||||
<div id="whois-optimized-view" class="whois-tab-content hidden">
|
||||
<!-- 域名概览卡片 -->
|
||||
<div class="whois-overview-card">
|
||||
<div class="whois-domain-header">
|
||||
<h3 class="whois-domain-name">{{domainName}}</h3>
|
||||
{{#if status}}
|
||||
<span class="whois-status-badge {{statusClass}}">{{status}}</span>
|
||||
{{#end if}}
|
||||
</div>
|
||||
{{#if registrar}}
|
||||
<div class="whois-registrar-info">
|
||||
<i class="fa fa-building"></i>
|
||||
<span>注册商:{{registrar}}</span>
|
||||
</div>
|
||||
{{#end if}}
|
||||
</div>
|
||||
|
||||
<!-- 信息卡片网格 -->
|
||||
<div class="whois-info-grid">
|
||||
<!-- 时间信息卡片 -->
|
||||
<div class="whois-info-card">
|
||||
<div class="whois-card-header">
|
||||
<i class="fa fa-clock-o whois-card-icon"></i>
|
||||
<h4 class="whois-card-title">时间信息</h4>
|
||||
</div>
|
||||
<div class="whois-card-content">
|
||||
{{#if creationDate}}
|
||||
<div class="whois-info-item">
|
||||
<span class="whois-info-label">注册时间</span>
|
||||
<span class="whois-info-value">{{creationDate}}</span>
|
||||
</div>
|
||||
{{#end if}}
|
||||
{{#if expirationDate}}
|
||||
<div class="whois-info-item">
|
||||
<span class="whois-info-label">到期时间</span>
|
||||
<span class="whois-info-value">{{expirationDate}}</span>
|
||||
</div>
|
||||
{{#end if}}
|
||||
{{#if updatedDate}}
|
||||
<div class="whois-info-item">
|
||||
<span class="whois-info-label">更新时间</span>
|
||||
<span class="whois-info-value">{{updatedDate}}</span>
|
||||
</div>
|
||||
{{#end if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 注册信息卡片 -->
|
||||
<div class="whois-info-card">
|
||||
<div class="whois-card-header">
|
||||
<i class="fa fa-user whois-card-icon"></i>
|
||||
<h4 class="whois-card-title">注册信息</h4>
|
||||
</div>
|
||||
<div class="whois-card-content">
|
||||
{{#if registrant}}
|
||||
<div class="whois-info-item">
|
||||
<span class="whois-info-label">注册人</span>
|
||||
<span class="whois-info-value">{{registrant}}</span>
|
||||
</div>
|
||||
{{#end if}}
|
||||
{{#if registrarName}}
|
||||
<div class="whois-info-item">
|
||||
<span class="whois-info-label">注册商</span>
|
||||
<span class="whois-info-value">{{registrarName}}</span>
|
||||
</div>
|
||||
{{#end if}}
|
||||
{{#if emails}}
|
||||
<div class="whois-info-item">
|
||||
<span class="whois-info-label">联系邮箱</span>
|
||||
<span class="whois-info-value">{{emails}}</span>
|
||||
</div>
|
||||
{{#end if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS信息卡片 -->
|
||||
{{#if nameServers}}
|
||||
<div class="whois-info-card whois-dns-card">
|
||||
<div class="whois-card-header">
|
||||
<i class="fa fa-server whois-card-icon"></i>
|
||||
<h4 class="whois-card-title">DNS信息</h4>
|
||||
</div>
|
||||
<div class="whois-card-content">
|
||||
<div class="whois-info-item">
|
||||
<span class="whois-info-label">域名服务器</span>
|
||||
<div class="whois-dns-servers">
|
||||
{{#each nameServers as server,index}}
|
||||
<span class="whois-dns-server">{{server}}</span>
|
||||
{{#end each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#end if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{#end if}}
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- 域名注册协议模态窗口 -->
|
||||
<script type="text/template" id="domain-agreement-modal-template">
|
||||
<div class="domain-agreement-modal">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>域名注册信息 (WHOIS) 查询 - 堡塔域名注册</title>
|
||||
<link href="/font-awesome.min.css" rel="stylesheet" />
|
||||
<link href="src/styles/whois.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="font-sans bg-white text-secondary">
|
||||
<!-- 搜索区域 -->
|
||||
<section class="pt-28 pb-20 md:pt-24 md:pb-28 bg-gradient-to-b">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="mx-auto text-center">
|
||||
<h1 class="text-clamp font-bold leading-tight text-dark mb-6 animate-fade-in">
|
||||
堡塔 <span class="text-primary">(WHOIS)</span>查询工具
|
||||
</h1>
|
||||
<p class="text-lg text-secondary-80 mb-10 max-w-2xl mx-auto">
|
||||
查询任意域名的注册信息、所有者、到期时间等详细信息
|
||||
</p>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<div class="input-container">
|
||||
<input type="text" placeholder="请输入要查询的域名,如:example.com"
|
||||
class="search-input" id="whois-domain-input" />
|
||||
<i class="fa fa-times-circle clear-input-button" id="whois-clear-input" title="清空输入"></i>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<button id="whois-search-btn"
|
||||
class="primary-action-button text-lg hover:shadow-primary-30 hover:-translate-y-1">
|
||||
<i class="fa fa-search mr-2"></i>立即查询
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-left"><a class="text-primary hover:text-primary" style="text-decoration: none;" href="/new/domain-register">域名注册 | .com低至53.9, .cn低至19.9元</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 结果展示区域 -->
|
||||
<section class="py-20 bg-gray-50 hidden" id="whois-results-section">
|
||||
<div class="container mx-auto px-4">
|
||||
<div id="whois-content-area">
|
||||
<!-- 动态内容将在这里渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<section class="py-20 bg-gray-50 hidden" id="whois-loading-section">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center py-12">
|
||||
<div class="inline-block animate-spin rounded-full w-12 h-12 border-b-2 border-primary mb-4"></div>
|
||||
<p class="text-gray-600 text-lg">正在查询 WHOIS 信息...</p>
|
||||
<p class="text-gray-500 text-sm mt-2">请稍等,通常需要几秒钟时间</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<section class="py-20 bg-gray-50 hidden" id="whois-error-section">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center py-12 max-w-2xl mx-auto">
|
||||
<h4 class="text-xl font-semibold text-gray-800 mb-2">查询失败</h4>
|
||||
<p class="text-gray-600 mb-6" id="whois-error-message">域名查询出现问题,请稍后重试</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script type="text/javascript" src="/jquery-3.6.0.min.js"></script>
|
||||
<script type="module" src="src/pages/whois.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -14,19 +14,35 @@
|
|||
<div class="container mx-auto px-4">
|
||||
<div class=" mx-auto text-center">
|
||||
<h1 class="text-[clamp(2rem,5vw,3.5rem)] font-bold leading-tight text-dark mb-6 animate-fade-in">
|
||||
堡塔<span class="text-primary">域名注册</span>重磅上线!<br />一站式搞定建站访问
|
||||
低价域名,就选<span class="text-primary">宝塔域名</span>
|
||||
</h1>
|
||||
<p class="text-[clamp(1rem,2vw,1.25rem)] text-secondary-80 mb-10 max-w-2xl mx-auto">
|
||||
.com低至53.9元,.cn低至19.9元!部分后缀新人9.9元注册
|
||||
</p>
|
||||
<!-- 搜索框和按钮 -->
|
||||
<div id="search-section" class="flex flex-col sm:flex-row max-w-3xl mx-auto gap-4 pb-20">
|
||||
<!-- Tab功能区域 -->
|
||||
<div class="max-w-3xl mx-auto" style="padding-bottom: 8px;">
|
||||
<!-- Tab切换按钮 -->
|
||||
<div class="domain-search-tabs">
|
||||
<div class="tab-buttons flex">
|
||||
<button id="tab-normal-index" class="tab-btn active">
|
||||
域名注册
|
||||
</button>
|
||||
<button id="tab-ai-index" class="tab-btn">
|
||||
<i class="fa fa-search"></i>AI域名推荐
|
||||
<span class="new-badge">new</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 域名注册面板 -->
|
||||
<div id="normal-search-panel-index" class="search-panel">
|
||||
<div id="search-section" class="flex flex-col sm:flex-row gap-4">
|
||||
<div class="input-container">
|
||||
<input type="text" placeholder="输入您想注册的域名(如:yourbrand)" class="search-input" id="domain-query-input" />
|
||||
<i class="fa fa-times-circle clear-input-button" id="clear-input-button" title="清空输入"></i>
|
||||
<div class="mt-2 text-left"><a class="text-primary hover:text-primary" style="text-decoration: none;" href="/domain/domain/transfer">域名转入 .cn地址29.9元</a></div>
|
||||
<div class="mt-2 text-left"><a class="text-primary hover:text-primary" style="text-decoration: none;" href="/domain/domain/transfer">域名转入 .cn低至29.9元</a></div>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex gap-4 relative">
|
||||
<a id="domain-query-button"
|
||||
class="primary-action-button text-lg hover:shadow-primary-30 hover:-translate-y-1">立即查询</a>
|
||||
<a id="cart-button"
|
||||
|
|
@ -35,6 +51,55 @@
|
|||
<i class="fa fa-shopping-cart mr-2"></i>
|
||||
<span>购物车</span>
|
||||
</a>
|
||||
<a class="absolute btlink" href="https://qm.qq.com/q/fxbto4wZkk" target="_blank" rel="noopener" style="text-decoration: none;bottom: 0;right: 0;">加入QQ群</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI推荐面板 -->
|
||||
<div id="ai-search-panel-index" class="search-panel hidden">
|
||||
<div class="flex w-full">
|
||||
<!-- 整体渐变容器 -->
|
||||
<div class="ai-search-container">
|
||||
<div class="ai-search-inner">
|
||||
<form autocomplete="off">
|
||||
<div class="flex flex-col lg:flex-row gap-3 w-full">
|
||||
<!-- 品牌名称输入框 -->
|
||||
<div class="ai-input-group flex-1">
|
||||
<i class="ai-input-icon fa fa-building"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="brand-name-input-index"
|
||||
class="ai-search-input"
|
||||
placeholder="请输入品牌/公司/个人/产品名称"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 行业信息输入框 -->
|
||||
<div class="ai-input-group flex-1">
|
||||
<i class="ai-input-icon fa fa-search"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="industry-input-index"
|
||||
class="ai-search-input"
|
||||
placeholder="请输入行业信息"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- AI推荐按钮 -->
|
||||
<button type="button" id="ai-recommend-btn-index" class="ai-transparent-button">
|
||||
<i class="ai-button-icon fa fa-search"></i>
|
||||
<span class="ai-button-text">AI推荐</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -53,7 +118,7 @@
|
|||
</div>
|
||||
<div class="rule-item flex items-center gap-2">
|
||||
<i class="fa fa-ban text-primary"></i>
|
||||
<span>每个账号仅限参与1次</span>
|
||||
<span>每个账号一个实名仅限参与1次</span>
|
||||
</div>
|
||||
<div class="rule-item flex items-center gap-2">
|
||||
<i class="fa fa-ticket text-primary"></i>
|
||||
|
|
@ -73,7 +138,7 @@
|
|||
</div>
|
||||
<div class="product-content">
|
||||
<h3 class="product-name">域名注册 0.01元秒杀</h3>
|
||||
<p class="product-desc">.top/.icu/.xyz/.cyou后缀每日限量100个名额,每日上午10:00准时开枪</p>
|
||||
<p class="product-desc">.icu/.xyz/.cyou后缀每日限量100个名额,每日上午10:00准时开枪</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -141,55 +206,111 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 组队领取top优惠券区域 -->
|
||||
<div id="team-coupon" class="activity-container act-super_discount pb-[100px]" style="scroll-margin-top: 100px;">
|
||||
<div class="title-container">
|
||||
<div class="title-container-title">组队领取0.01元.top及超低价.com/.cn购买资格</div>
|
||||
<div class="title-container-desc">
|
||||
满4人即可享受超低价首年购,数量有限,先到先得,数量领完则活动截止
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="activity-simulated-table opt-super_discount w-container">
|
||||
<div class="table-header">
|
||||
<div class="table-header-item">
|
||||
<div class="header-tr">
|
||||
<div
|
||||
class="table-header-th table-left border-left-[1px] border-top-[1px] bg-white">
|
||||
</div>
|
||||
<div
|
||||
class="table-header-th !h-[60px] bg-[#E9F6EB] border-left-[2px] border-top-[2px] bg-white">
|
||||
<div class="title-desc">超值优惠</div>
|
||||
<div class="title">
|
||||
<span>队长达标价格(首年)</span>
|
||||
<span class="tips">最划算</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="table-header-th !h-[60px] bg-[#E9F6EB] border-left-[2px] border-top-[2px] bg-white">
|
||||
<div class="title">
|
||||
<span>队员达标价格(首年)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-header-th border-top-[1px] bg-white">
|
||||
<div class="table-daily-price">
|
||||
<div class="title">日常价格 (首年)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<a href="javascript:void(0)" id="initiate-team-btn" class="initiate-team-btn !w-[400px]" style="margin-top:20px">
|
||||
<div class="team-icon"></div>
|
||||
<span>发起组队</span>
|
||||
</a>
|
||||
|
||||
<div class="action-activity-desc">
|
||||
* 优惠资格有效期:4人组队成功后开始计时,72小时内有效,逾期未使用则自动失效,可再次组队领取。
|
||||
<br>
|
||||
* 使用限制:每个用户实名只能使用1次,优惠资格不可折现、不可转赠。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 价格表格 -->
|
||||
<!-- 价格表格 -->
|
||||
<div class="mx-auto max-w-[900px]">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-clamp font-bold text-dark mb-4" style="margin-top: 80px">域名价格一览表</h2>
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-clamp font-bold text-dark mb-4" style="margin-top: 120px">域名价格一览表</h2>
|
||||
<div class="text-secondary-80 max-w-4xl mx-auto px-4">
|
||||
透明的价格体系,无隐藏费用,让您明明白白消费,更多服务请<a class="border-b border-dashed border-primary text-secondary-80" href="https://qm.qq.com/q/fxbto4wZkk" target="_blank" rel="noopener" style="text-decoration: none;">加入QQ群</a>或者<span
|
||||
class="contact-service-trigger cursor-pointer border-b border-dashed border-primary relative">请联系客服咨询
|
||||
<!-- 二维码悬浮层 -->
|
||||
<div
|
||||
class="qr-code-popup absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 opacity-0 invisible transition-all duration-300 z-50">
|
||||
<div class="bg-white rounded-lg shadow-xl p-4 border border-gray-200">
|
||||
<div class="text-center mb-2">
|
||||
<p class="text-sm font-medium text-gray-700">扫码联系客服</p>
|
||||
</div>
|
||||
<!-- 二维码SVG -->
|
||||
<img src="https://www.bt.cn/Public/new/images/wechat-qrcode.png" alt="二维码"
|
||||
class="w-24 h-24 mx-auto" />
|
||||
<div class="text-center mt-2">
|
||||
<p class="text-xs text-gray-500">微信客服</p>
|
||||
</div>
|
||||
<!-- 小箭头 -->
|
||||
<div
|
||||
class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-white">
|
||||
</div>
|
||||
<!-- 二维码悬浮层 -->
|
||||
<div
|
||||
class="qr-code-popup absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 opacity-0 invisible transition-all duration-300 z-50">
|
||||
<div class="bg-white rounded-lg shadow-xl p-4 border border-gray-200">
|
||||
<div class="text-center mb-2">
|
||||
<p class="text-sm font-medium text-gray-700">扫码联系客服</p>
|
||||
</div>
|
||||
<!-- 二维码SVG -->
|
||||
<img src="https://www.bt.cn/Public/new/images/wechat-qrcode.png" alt="二维码"
|
||||
class="w-24 h-24 mx-auto" />
|
||||
<div class="text-center mt-2">
|
||||
<p class="text-xs text-gray-500">微信客服</p>
|
||||
</div>
|
||||
<!-- 小箭头 -->
|
||||
<div
|
||||
class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-white">
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto px-4 md:px-0">
|
||||
<!-- 现代化价格表格 -->
|
||||
<div class="modern-price-table bg-white overflow-hidden shadow-lg">
|
||||
<!-- 表头 -->
|
||||
<div class="price-table-header bg-gradient-to-r from-primary to-green-600 text-white py-4 px-4">
|
||||
<div class="grid grid-cols-4 gap-4 text-center">
|
||||
<div class="text-sm md:text-base font-semibold">域名后缀</div>
|
||||
<div class="text-sm md:text-base font-semibold">首年价格</div>
|
||||
<div class="text-sm md:text-base font-semibold">续费价格</div>
|
||||
<div class="text-sm md:text-base font-semibold">转入续费</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表格内容 -->
|
||||
<div class="price-table-body" id="price-table-body">
|
||||
<!-- 动态生成的价格表格内容 -->
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto px-4 md:px-0">
|
||||
<!-- 现代化价格表格 -->
|
||||
<div class="modern-price-table bg-white overflow-hidden shadow-lg">
|
||||
<!-- 表头 -->
|
||||
<div class="price-table-header bg-gradient-to-r from-primary to-green-600 text-white py-4 px-4">
|
||||
<div class="grid grid-cols-4 gap-4 text-center">
|
||||
<div class="text-sm md:text-base font-semibold">域名后缀</div>
|
||||
<div class="text-sm md:text-base font-semibold">首年价格</div>
|
||||
<div class="text-sm md:text-base font-semibold">续费价格</div>
|
||||
<div class="text-sm md:text-base font-semibold">转入续费</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表格内容 -->
|
||||
<div class="price-table-body" id="price-table-body">
|
||||
<!-- 动态生成的价格表格内容 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -590,10 +711,170 @@
|
|||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="team-rule-modal-template">
|
||||
<div>
|
||||
<div class="rule-item">
|
||||
<div class="flex items-center gap-x-3 mb-4">
|
||||
<img src="/static/new/images/724/user-team.svg" />
|
||||
<h3 class="text-2xl font-bold">组队规则</h3>
|
||||
</div>
|
||||
<div class="pl-12 text-[#666] leading-loose">
|
||||
<p><span class="font-bold text-black">组队时间:</span>即日起 - 9月30日,逾期未完成组队视为自动放弃参与资格</p>
|
||||
<p><span class="font-bold text-black">活动对象:</span>所有宝塔平台注册用户均可参与本次组队活动</p>
|
||||
<p><span class="font-bold text-black">发起组队:</span>用户发起组队,无限制条件,新老用户均可参加,完成指定人数,领取对应的优惠资格。.top后缀4.9元组队活动与0.01元组队活动仅可参与一个,如需更换请
|
||||
<span class="cursor-pointer border-b border-dashed border-primary relative rule-customer">
|
||||
联系客服
|
||||
</span>。</p>
|
||||
<p><span
|
||||
class="font-bold text-black">组队邀请:</span>发起者通过活动页面生成专属邀请链接或二维码,邀请好友加入组队,被邀请用户需在活动时间内点击链接或扫描二维码并完成平台注册(已注册用户直接加入),才算成功加入组队。
|
||||
</p>
|
||||
<p><span
|
||||
class="font-bold text-black">组队成功:</span>达到所需组队人数后,系统自动判定组队成功,若在活动时间截止时,组队人数未达标,则该组队失败,队员无法获得优惠资格。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-item" style="margin-top: 15px;">
|
||||
<div class="flex items-center gap-x-3 mb-4">
|
||||
<img src="/static/new/images/724/coupon.svg" />
|
||||
<h3 class="text-2xl font-bold">优惠使用规则</h3>
|
||||
</div>
|
||||
<div class="pl-12 text-[#666] leading-loose">
|
||||
<p><span class="font-bold text-black">优惠资格有效期:</span>4人组队成功后开始计时,72小时内有效,逾期未使用则自动失效,可再次组队领取。</p>
|
||||
<p><span class="font-bold text-black">使用限制:</span>0.01元购买资格仅用于.xyz/.icu/.cyou后缀,且仅可以使用1次,优惠资格不可折现、不可转赠。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="team-modal-template">
|
||||
<div class="team-modal-container">
|
||||
<div class="team-modal-header">
|
||||
<h2 class="team-modal-title">确认发起组队</h2>
|
||||
<!-- <p class="team-modal-subtitle">组队成功后将根据最终人数自动发放对应的购买资格</p> -->
|
||||
</div>
|
||||
<div class="team-modal-details">
|
||||
<!-- <h4>优惠详情</h4> -->
|
||||
<div class="team-modal-table">
|
||||
<div class="team-modal-table-header">
|
||||
<div class="team-modal-table-cell" style="text-align: left;">参与活动的域名后缀</div>
|
||||
<div class="team-modal-table-cell">队长价格 (首年)</div>
|
||||
<div class="team-modal-table-cell">队员价格 (首年)</div>
|
||||
</div>
|
||||
<div class="team-modal-table-body">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-modal-notes">
|
||||
<h4>重要说明</h4>
|
||||
<ol>
|
||||
<li>每个账户只能参与1次组队,参与过队长/队员的,无法再次参与其他组队。</li>
|
||||
<li>4人组队成功后开始计时,72小时内有效,逾期未使用则自动失效,可再次组队领取。</li>
|
||||
<li>每个用户实名只能使用1次,优惠资格不可折现、不可转赠。</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="team-modal-footer">
|
||||
<button class="btn btn-cancel">取消</button>
|
||||
<button class="btn btn-confirm">确认发起</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="my-team-modal-template">
|
||||
<div class="my-team-container">
|
||||
<div class="team-modal-header">
|
||||
<h2 class="team-modal-title">我的队伍 (1人)</h2>
|
||||
<p class="team-modal-subtitle">继续邀请好友, 获得购买资格哦</p>
|
||||
</div>
|
||||
<div class="my-team-body">
|
||||
<div class="current-tier-section">
|
||||
<div class="tier-content-wrapper">
|
||||
<div class="tier-card">
|
||||
<div class="tier-info">
|
||||
<p id="my-team-milestone-info">当前 <strong>1</strong> 人,再邀请 <strong>2</strong> 人即可达标 <strong>【3人组队目标】</strong><span class="btn-quit refresh-team-icon"></span></p>
|
||||
<p class="tier-description" id="my-team-milestone-desc">3人组队目标将享受单域名78元优惠, 通配符588元优惠</p>
|
||||
</div>
|
||||
<!-- The inline styles for the progress bar are kept here as they are dynamically updated by JavaScript -->
|
||||
<div class="tier-progress-bar" id="my-team-progress-bar">
|
||||
<div class="progress-line" style="width: 0%;"></div>
|
||||
<div class="progress-milestone active" style="left: 10px" data-member="1">
|
||||
<div class="progress-milestone-icon no-icon"></div>
|
||||
<span>1人</span>
|
||||
</div>
|
||||
<div class="progress-milestone" style="left: 33.3%;" data-member="3">
|
||||
<div class="progress-milestone-icon">
|
||||
<img src="/static/new/images/724/discount.svg" alt="gift">
|
||||
</div>
|
||||
<span>2人</span>
|
||||
</div>
|
||||
<div class="progress-milestone" style="left: 66.6%;" data-member="5">
|
||||
<div class="progress-milestone-icon">
|
||||
<img src="/static/new/images/724/discount.svg" alt="gift">
|
||||
</div>
|
||||
<span>3人</span>
|
||||
</div>
|
||||
<div class="progress-milestone" style="right:-20px" data-member="10">
|
||||
<div class="progress-milestone-icon">
|
||||
<img src="/static/new/images/724/discount.svg" alt="gift">
|
||||
</div>
|
||||
<span>4人</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tier-members" id="my-team-members">
|
||||
<div class="member-item">
|
||||
<div class="member-avatar leader">
|
||||
<img src="/static/new/images/724/avatar.svg" alt="队长">
|
||||
</div>
|
||||
<span class="member-label">队长</span>
|
||||
</div>
|
||||
<div class="member-item add-member-item">
|
||||
<div class="add-member"></div>
|
||||
<span class="member-label">添加成员</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tier-status" id="my-team-status">
|
||||
<div class="status-icon"></div>
|
||||
<span>未达标</span>
|
||||
<p>继续邀请好友加入</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-modal-body my-team-share-section">
|
||||
<div id="claimed-coupons-section" class="claimed-coupons-section">
|
||||
<h4>已解锁的购买资格(已自动发放)</h4>
|
||||
<ul id="claimed-coupons-list" class="coupon-list"></ul>
|
||||
</div>
|
||||
<div id="my-team-qr-section" class="team-qr-code-section">
|
||||
<h4>微信扫码参与组队</h4>
|
||||
<div id="my-team-qr-code"></div>
|
||||
<p>使用微信扫码上方二维码即可快速加入队伍</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="team-modal-footer">
|
||||
<!-- <button class="btn btn-quit">退出组队</button> -->
|
||||
<button class="btn btn-invite">邀请好友加入</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- 组队模态框 -->
|
||||
<div id="custom-modal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<button class="modal-close" type="button" title="关闭"></button>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- JavaScript -->
|
||||
<script type="text/javascript" src="/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="https://www.bt.cn/Public/js/qrcode.min.js"></script>
|
||||
<script type="text/javascript" src="https://www.bt.cn/Public/js/clipboard.min.js?2.2"></script>
|
||||
<script type="module" src="src/pages/index.ts"></script>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import type { ApiResponse } from "../types/api";
|
|||
import type {
|
||||
DomainQueryCheckRequest,
|
||||
DomainQueryCheckResponseData,
|
||||
AiDomainQueryCheckRequest,
|
||||
AiDomainQueryCheckResponseData
|
||||
} from "../types/api-types/domain-query-check";
|
||||
import type {
|
||||
ContactGetUserDetailRequest,
|
||||
|
|
@ -56,6 +58,17 @@ export function domainQueryCheck(
|
|||
headers
|
||||
);
|
||||
}
|
||||
// AI -域名查询
|
||||
export function aiDomainQueryCheck(
|
||||
data: AiDomainQueryCheckRequest,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<ApiResponse<AiDomainQueryCheckResponseData>> {
|
||||
return api.post<AiDomainQueryCheckResponseData>(
|
||||
"/v1/domain/recommend/recommend_domains",
|
||||
data,
|
||||
headers,
|
||||
);
|
||||
}
|
||||
|
||||
// 获取实名信息模板列表
|
||||
export function getContactUserDetail(
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,455 @@
|
|||
/**
|
||||
* @fileoverview WHOIS 查询页面逻辑
|
||||
* @description 提供域名 WHOIS 信息查询功能,复用现有 API 和样式
|
||||
*/
|
||||
|
||||
import { queryWhois } from "@api/landing";
|
||||
|
||||
// 安全的jQuery访问函数
|
||||
function safe$() {
|
||||
return (window as any).$ as any;
|
||||
}
|
||||
|
||||
// 页面状态管理
|
||||
let currentDomain = '';
|
||||
let isInitialized = false;
|
||||
|
||||
/**
|
||||
* 初始化页面
|
||||
*/
|
||||
function init() {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
handleUrlParams();
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定事件
|
||||
*/
|
||||
function bindEvents() {
|
||||
const $ = safe$();
|
||||
if (!$) return;
|
||||
|
||||
// 搜索按钮点击事件
|
||||
$(document).on('click', '#whois-search-btn', handleSearch);
|
||||
|
||||
// 输入框回车事件
|
||||
$(document).on('keypress', '#whois-domain-input', (e: any) => {
|
||||
if (e.which === 13) {
|
||||
handleSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// 清空输入按钮
|
||||
$(document).on('click', '#whois-clear-input', () => {
|
||||
$('#whois-domain-input').val('').trigger('input');
|
||||
});
|
||||
|
||||
// 输入框变化事件
|
||||
$(document).on('input', '#whois-domain-input', (e: any) => {
|
||||
const value = $(e.target).val() as string;
|
||||
const $clearBtn = $('#whois-clear-input');
|
||||
|
||||
if (value && value.trim()) {
|
||||
$clearBtn.addClass('visible');
|
||||
} else {
|
||||
$clearBtn.removeClass('visible');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 URL 参数
|
||||
*/
|
||||
function handleUrlParams() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const domain = urlParams.get('domain');
|
||||
const $ = safe$();
|
||||
if (!$) return;
|
||||
|
||||
if (domain) {
|
||||
$('#whois-domain-input').val(domain).trigger('input');
|
||||
performSearch(domain);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证域名格式
|
||||
*/
|
||||
function isValidDomain(domain: string): boolean {
|
||||
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
return domainRegex.test(domain.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理搜索操作
|
||||
*/
|
||||
function handleSearch() {
|
||||
const $ = safe$();
|
||||
if (!$) return;
|
||||
|
||||
const domain = ($('#whois-domain-input').val() as string).trim();
|
||||
|
||||
if (!domain) {
|
||||
alert('请输入要查询的域名');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidDomain(domain)) {
|
||||
alert('请输入正确的域名格式');
|
||||
return;
|
||||
}
|
||||
|
||||
performSearch(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 WHOIS 查询
|
||||
*/
|
||||
async function performSearch(domain: string) {
|
||||
currentDomain = domain;
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await queryWhois(domain);
|
||||
// const response = {
|
||||
// code: 0,
|
||||
// status: true,
|
||||
// msg: "查询成功",
|
||||
// data: {
|
||||
// address: [
|
||||
// "3th Floor, BangNing Technology Park, 2 YuHua Avenue",
|
||||
// "Yuhuatai District",
|
||||
// "Nanjing Jiangsu",
|
||||
// "China",
|
||||
// "3th Floor, BangNing Technology Park, 2 YuHua Avenue",
|
||||
// "Yuhuatai District",
|
||||
// "Nanjing Jiangsu",
|
||||
// "China",
|
||||
// "3th Floor, BangNing Technology Park, 2 YuHua Avenue",
|
||||
// "Yuhuatai District",
|
||||
// "Nanjing Jiangsu",
|
||||
// "China",
|
||||
// ],
|
||||
// city: "",
|
||||
// country: "CN",
|
||||
// creation_date: "2014-07-24 10:45:35",
|
||||
// dnssec: "signed",
|
||||
// domain_name: "top",
|
||||
// emails: ["support@nic.top", "tech@nic.top"],
|
||||
// expiration_date: "",
|
||||
// name: "Sven Chen",
|
||||
// name_servers: [
|
||||
// "A.ZDNSCLOUD.CN 203.99.24.1",
|
||||
// "B.ZDNSCLOUD.CN 203.99.25.1",
|
||||
// "C.ZDNSCLOUD.COM 203.99.26.1",
|
||||
// "D.ZDNSCLOUD.COM 203.99.27.1",
|
||||
// "F.ZDNSCLOUD.CN 116.169.54.111",
|
||||
// "G.ZDNSCLOUD.COM 223.72.199.37",
|
||||
// "I.ZDNSCLOUD.CN 2401:8d00:1:0:0:0:0:1",
|
||||
// "J.ZDNSCLOUD.COM 2401:8d00:2:0:0:0:0:1",
|
||||
// ],
|
||||
// org: "Jiangsu Bangning Science & technology Co.,Ltd.",
|
||||
// phone: [
|
||||
// "+86 18936016161",
|
||||
// "Fax: +86 2586883476",
|
||||
// "+86 15895978960",
|
||||
// "Fax: +86 02586883476",
|
||||
// ],
|
||||
// referral_url: "whois.nic.top",
|
||||
// registrant_postal_code: "",
|
||||
// registrar: "",
|
||||
// registrar_url: "",
|
||||
// reseller: null,
|
||||
// state: "",
|
||||
// status: "ACTIVE",
|
||||
// updated_date: "2025-07-29 10:45:35",
|
||||
// whois_server: "whois.nic.top",
|
||||
// rawData:
|
||||
// "% IANA WHOIS server\n% for more information on IANA, visit http://www.iana.org\n% This query returned 1 object\n\nrefer: whois.nic.top\n\ndomain: TOP\n\norganisation: .TOP Registry\naddress: 3th Floor, BangNing Technology Park, 2 YuHua Avenue\naddress: Yuhuatai District\naddress: Nanjing Jiangsu\naddress: China\n\ncontact: administrative\nname: Sven Chen\norganisation: Jiangsu Bangning Science & technology Co.,Ltd.\naddress: 3th Floor, BangNing Technology Park, 2 YuHua Avenue\naddress: Yuhuatai District\naddress: Nanjing Jiangsu\naddress: China\nphone: +86 18936016161\nfax-no: +86 2586883476\ne-mail: support@nic.top\n\ncontact: technical\nname: YiFeng Shen\norganisation: Jiangsu Bangning Science & technology Co.,Ltd.\naddress: 3th Floor, BangNing Technology Park, 2 YuHua Avenue\naddress: Yuhuatai District\naddress: Nanjing Jiangsu\naddress: China\nphone: +86 15895978960\nfax-no: +86 02586883476\ne-mail: tech@nic.top\n\nnserver: A.ZDNSCLOUD.CN 203.99.24.1\nnserver: B.ZDNSCLOUD.CN 203.99.25.1\nnserver: C.ZDNSCLOUD.COM 203.99.26.1\nnserver: D.ZDNSCLOUD.COM 203.99.27.1\nnserver: F.ZDNSCLOUD.CN 116.169.54.111\nnserver: G.ZDNSCLOUD.COM 223.72.199.37\nnserver: I.ZDNSCLOUD.CN 2401:8d00:1:0:0:0:0:1\nnserver: J.ZDNSCLOUD.COM 2401:8d00:2:0:0:0:0:1\nds-rdata: 26780 8 2 5d6e7869ee8e3b536a617de89482ddd1dcb9db9dbb1ac33d6ed351e2ca095b1b\n\nwhois: whois.nic.top\n\nstatus: ACTIVE\nremarks: Registration information: http://www.nic.top\n\ncreated: 2014-07-24\nchanged: 2025-07-29\nsource: IANA",
|
||||
// },
|
||||
// };
|
||||
if (response.status && response.data) {
|
||||
renderResults(domain, response.data);
|
||||
showResults();
|
||||
updatePageState(domain);
|
||||
} else {
|
||||
throw new Error('查询失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('WHOIS查询失败:', error);
|
||||
showError(error.message || '查询失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染查询结果
|
||||
*/
|
||||
function renderResults(domain: string, data: any) {
|
||||
console.log(domain,'-21212');
|
||||
const $ = safe$();
|
||||
if (!$) return;
|
||||
|
||||
const currentTime = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
const resultsHtml = generateSimpleLayout(domain, data, currentTime);
|
||||
$('#whois-content-area').html(resultsHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成简洁表格式布局
|
||||
*/
|
||||
function generateSimpleLayout(domain: string, data: any, currentTime: string): string {
|
||||
const domainName = domain;
|
||||
|
||||
return `
|
||||
<div class="whois-simple-layout">
|
||||
<!-- 标题区域 -->
|
||||
<div class="whois-header">
|
||||
<h2 class="whois-title">
|
||||
<span class="title-decoration"></span>
|
||||
<span class="domain-name">${domainName}</span> 域名注册信息
|
||||
</h2>
|
||||
<div class="whois-meta">
|
||||
<span>以下信息获取时间:${currentTime}</span>
|
||||
<a href="#" class="refresh-link" onclick="performSearch('${domain}')">获取最新信息</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 信息表格 -->
|
||||
<div class="whois-table">
|
||||
${generateWhoisRows(data)}
|
||||
</div>
|
||||
|
||||
<!-- 原始数据区域 -->
|
||||
<div class="whois-raw-data-section">
|
||||
<div class="raw-data-content">
|
||||
<div class="raw-data-title">
|
||||
<span class="title-decoration"></span>
|
||||
<span>${domainName} 完整WHOIS信息</span>
|
||||
</div>
|
||||
<div class="raw-data-box">${formatRawData(data.rawData || '暂无原始数据')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成WHOIS信息行
|
||||
*/
|
||||
function generateWhoisRows(data: any): string {
|
||||
// 处理邮箱数据
|
||||
const emails = Array.isArray(data.emails)
|
||||
? data.emails.join(", ")
|
||||
: data.emails || "请联系当前域名注册商获取";
|
||||
|
||||
// 处理域名服务器数据
|
||||
const nameServers = Array.isArray(data.name_servers)
|
||||
? data.name_servers
|
||||
: data.name_servers ? [data.name_servers] : [];
|
||||
|
||||
// 处理状态
|
||||
const status = data.status || "未知状态";
|
||||
let statusBadgeClass = "status-unknown";
|
||||
if (status.includes("ACTIVE") || status.includes("ok")) {
|
||||
statusBadgeClass = "status-active";
|
||||
} else if (status.includes("Transfer")) {
|
||||
statusBadgeClass = "status-transfer";
|
||||
}
|
||||
|
||||
const rows = [
|
||||
{
|
||||
labelCn: "域名所有者",
|
||||
labelEn: "Registrant",
|
||||
value: data.name || "请联系当前域名注册商获取",
|
||||
},
|
||||
{
|
||||
labelCn: "所有者邮箱",
|
||||
labelEn: "Registrant Email",
|
||||
value: emails,
|
||||
},
|
||||
{
|
||||
labelCn: "注册商",
|
||||
labelEn: "Registrar",
|
||||
value:
|
||||
data.registrar ||
|
||||
data.org ||
|
||||
"Guizhou Zhongyu Zhike Network Technology Co., Ltd.",
|
||||
},
|
||||
{
|
||||
labelCn: "注册时间",
|
||||
labelEn: "Registration Date",
|
||||
value:
|
||||
formatDateTime(data.creation_date) || "2020-10-24 16:00:12 (北京时间)",
|
||||
},
|
||||
{
|
||||
labelCn: "到期时间",
|
||||
labelEn: "Expiration Date",
|
||||
value:
|
||||
formatDateTime(data.expiration_date) ||
|
||||
"2026-10-24 16:00:12 (北京时间)",
|
||||
extraInfo: data.expiration_date
|
||||
? `
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #666; line-height: 1.4;">
|
||||
· 该域名到期时间仅供参考,实际到期时间请咨询对应注册商,费时云资源域名以控制台到期时间和消息提醒为准。<br>
|
||||
· 请在实际到期时间前续费,否则将导致该域名使用异常。
|
||||
</div>
|
||||
`
|
||||
: "",
|
||||
},
|
||||
{
|
||||
labelCn: "域名状态",
|
||||
labelEn: "Domain Status",
|
||||
value: `
|
||||
<div class="whois-status-value">
|
||||
<span>${status}</span>
|
||||
<span class="whois-status-badge-simple ${statusBadgeClass}">(${status === 'ACTIVE'?'正常':status})</span>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
// 如果有DNS服务器,添加DNS信息行
|
||||
if (nameServers.length > 0) {
|
||||
rows.push({
|
||||
labelCn: 'DNS 服务器',
|
||||
labelEn: 'Name Server',
|
||||
value: `
|
||||
<div class="whois-value-multiple">
|
||||
${nameServers.map((server: string) => `<div class="whois-dns-item">${server}</div>`).join('')}
|
||||
</div>
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
return rows.map(row => `
|
||||
<div class="whois-row">
|
||||
<div class="whois-label">
|
||||
<div class="label-cn">${row.labelCn}</div>
|
||||
<div class="label-en">${row.labelEn}</div>
|
||||
</div>
|
||||
<div class="whois-value">
|
||||
${row.value}
|
||||
${(row as any).extraInfo || ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
function formatDateTime(dateString: string | null | undefined): string | null {
|
||||
if (!dateString) return null;
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZoneName: 'short'
|
||||
}).replace('GMT+8', '北京时间');
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化原始数据
|
||||
*/
|
||||
function formatRawData(rawData: string): string {
|
||||
if (!rawData || rawData === '暂无原始数据') {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
// 确保原始数据保持原有格式,只是清理多余的空行
|
||||
return rawData.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示加载状态
|
||||
*/
|
||||
function showLoading() {
|
||||
const $ = safe$();
|
||||
if (!$) return;
|
||||
|
||||
$('#whois-results-section').addClass('hidden');
|
||||
$('#whois-error-section').addClass('hidden');
|
||||
$('#whois-loading-section').removeClass('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示结果
|
||||
*/
|
||||
function showResults() {
|
||||
const $ = safe$();
|
||||
if (!$) return;
|
||||
|
||||
$('#whois-loading-section').addClass('hidden');
|
||||
$('#whois-error-section').addClass('hidden');
|
||||
$('#whois-results-section').removeClass('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息
|
||||
*/
|
||||
function showError(message: string) {
|
||||
const $ = safe$();
|
||||
if (!$) return;
|
||||
|
||||
$('#whois-error-message').text(message);
|
||||
$('#whois-loading-section').addClass('hidden');
|
||||
$('#whois-results-section').addClass('hidden');
|
||||
$('#whois-error-section').removeClass('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新页面状态(标题和URL)
|
||||
*/
|
||||
function updatePageState(domain: string) {
|
||||
document.title = `${domain} - WHOIS 查询结果 - 堡塔域名注册`;
|
||||
|
||||
const newUrl = `${window.location.pathname}?domain=${encodeURIComponent(domain)}`;
|
||||
window.history.replaceState({ domain }, '', newUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暴露必要的函数到全局作用域
|
||||
*/
|
||||
(window as any).performSearch = performSearch;
|
||||
|
||||
// 页面初始化逻辑
|
||||
function safeInit() {
|
||||
// 确保DOM和jQuery都已准备就绪
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
// DOM已经准备就绪
|
||||
if (safe$()) {
|
||||
// jQuery也已加载
|
||||
init();
|
||||
} else {
|
||||
// 等待jQuery加载
|
||||
window.addEventListener('load', init);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始初始化
|
||||
safeInit();
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,860 @@
|
|||
/**
|
||||
* WHOIS查询页面专用样式文件
|
||||
* 包含页面所需的所有原子样式和组件样式
|
||||
*/
|
||||
|
||||
/* =========================== */
|
||||
/* CSS变量定义 (复用主项目变量) */
|
||||
/* =========================== */
|
||||
:root {
|
||||
/* 主色调 */
|
||||
--primary: #20a53a;
|
||||
--secondary: #363636;
|
||||
--light: #fff;
|
||||
--dark: #1a1a1a;
|
||||
--success: #00b42a;
|
||||
--danger: #f53f3f;
|
||||
|
||||
/* 透明度变体 */
|
||||
--primary-5: rgba(32, 165, 58, 0.05);
|
||||
--primary-10: rgba(32, 165, 58, 0.1);
|
||||
--primary-90: rgba(32, 165, 58, 0.9);
|
||||
--secondary-40: rgba(54, 54, 54, 0.4);
|
||||
--secondary-70: rgba(54, 54, 54, 0.7);
|
||||
--secondary-80: rgba(54, 54, 54, 0.8);
|
||||
}
|
||||
|
||||
/* =========================== */
|
||||
/* 基础样式重置 */
|
||||
/* =========================== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background-color: var(--light);
|
||||
color: var(--secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
button {
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* =========================== */
|
||||
/* 布局工具类 */
|
||||
/* =========================== */
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.flex-row { flex-direction: row; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
.relative { position: relative; }
|
||||
.absolute { position: absolute; }
|
||||
.inline-block { display: inline-block; }
|
||||
.block { display: block; }
|
||||
.hidden { display: none !important; }
|
||||
.w-full { width: 100%; }
|
||||
.h-full { height: 100%; }
|
||||
.max-w-2xl { max-width: 42rem; }
|
||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
|
||||
/* =========================== */
|
||||
/* 间距工具类 */
|
||||
/* =========================== */
|
||||
.py-12 { padding-top: 3rem; padding-bottom: 3rem; }
|
||||
.py-20 { padding-top: 5rem; padding-bottom: 5rem; }
|
||||
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||
.p-4 { padding: 1rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
.mb-10 { margin-bottom: 2.5rem; }
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.mt-6 { margin-top: 1.5rem; }
|
||||
.mt-8 { margin-top: 2rem; }
|
||||
.mr-2 { margin-right: 0.5rem; }
|
||||
.ml-auto { margin-left: auto; }
|
||||
|
||||
/* =========================== */
|
||||
/* 字体大小 */
|
||||
/* =========================== */
|
||||
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||
.text-base { font-size: 1rem; line-height: 1.5rem; }
|
||||
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
||||
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
|
||||
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||||
.text-6xl { font-size: 3.75rem; line-height: 1; }
|
||||
.text-clamp { font-size: clamp(1.5rem, 3vw, 2.5rem); }
|
||||
|
||||
/* =========================== */
|
||||
/* 字体权重 */
|
||||
/* =========================== */
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.font-mono { font-family: Monaco, Menlo, 'Ubuntu Mono', monospace; }
|
||||
|
||||
/* =========================== */
|
||||
/* 颜色工具类 */
|
||||
/* =========================== */
|
||||
.text-primary { color: var(--primary); }
|
||||
.text-dark { color: var(--dark); }
|
||||
.text-secondary { color: var(--secondary); }
|
||||
.text-secondary-70 { color: var(--secondary-70); }
|
||||
.text-secondary-80 { color: var(--secondary-80); }
|
||||
.text-white { color: white; }
|
||||
.text-gray-500 { color: #6b7280; }
|
||||
.text-gray-600 { color: #4b5563; }
|
||||
.text-gray-700 { color: #374151; }
|
||||
.text-gray-800 { color: #1f2937; }
|
||||
.text-red-500 { color: #ef4444; }
|
||||
.text-green-400 { color: #4ade80; }
|
||||
.bg-white { background-color: white; }
|
||||
.bg-gray-50 { background-color: #f9fafb; }
|
||||
.bg-gray-100 { background-color: #f3f4f6; }
|
||||
.bg-gray-800 { background-color: #1f2937; }
|
||||
.bg-gray-900 { background-color: #111827; }
|
||||
.bg-green-100 { background-color: #dcfce7; }
|
||||
.bg-green-800 { background-color: #166534; }
|
||||
.bg-yellow-100 { background-color: #fef3c7; }
|
||||
.bg-yellow-800 { color: #ffffff; background-color: #92400e; }
|
||||
|
||||
/* =========================== */
|
||||
/* 边框和圆角 */
|
||||
/* =========================== */
|
||||
.border { border-width: 1px; }
|
||||
.border-b { border-bottom-width: 1px; }
|
||||
.border-gray-200 { border-color: #e5e7eb; }
|
||||
.border-dashed { border-style: dashed; }
|
||||
.border-primary { border-color: var(--primary); }
|
||||
.border-b-2 { border-bottom-width: 2px; }
|
||||
.border-2 { border-width: 2px; }
|
||||
.rounded { border-radius: 0.25rem; }
|
||||
.rounded-lg { border-radius: 0.5rem; }
|
||||
.rounded-xl { border-radius: 0.75rem; }
|
||||
.rounded-full { border-radius: 9999px; }
|
||||
|
||||
/* =========================== */
|
||||
/* 阴影 */
|
||||
/* =========================== */
|
||||
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
|
||||
.shadow-xl { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); }
|
||||
|
||||
/* =========================== */
|
||||
/* 过渡和动画 */
|
||||
/* =========================== */
|
||||
.transition-all { transition: all 0.15s ease-in-out; }
|
||||
.transition-transform { transition: transform 0.15s ease-in-out; }
|
||||
.duration-300 { transition-duration: 300ms; }
|
||||
.transform { transform: translateZ(0); }
|
||||
.-translate-y-1 { transform: translateY(-0.25rem); }
|
||||
|
||||
/* 旋转动画 */
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 淡入动画 */
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================== */
|
||||
/* 尺寸工具类 */
|
||||
/* =========================== */
|
||||
.w-12 { width: 3rem; }
|
||||
.h-12 { height: 3rem; }
|
||||
.max-h-96 { max-height: 24rem; }
|
||||
|
||||
/* =========================== */
|
||||
/* 其他工具类 */
|
||||
/* =========================== */
|
||||
.cursor-pointer { cursor: pointer; }
|
||||
.whitespace-pre-wrap { white-space: pre-wrap; }
|
||||
.overflow-auto { overflow: auto; }
|
||||
.font-sans { font-family: 'Inter', system-ui, sans-serif; }
|
||||
.leading-tight { line-height: 1.25; }
|
||||
|
||||
/* =========================== */
|
||||
/* 特殊背景样式 */
|
||||
/* =========================== */
|
||||
.bg-gradient-to-b {
|
||||
background: linear-gradient(to bottom, #f8fafc, #ffffff);
|
||||
}
|
||||
|
||||
/* =========================== */
|
||||
/* 边框颜色 */
|
||||
/* =========================== */
|
||||
.border-primary { border-color: var(--primary); }
|
||||
|
||||
/* =========================== */
|
||||
/* hover效果 */
|
||||
/* =========================== */
|
||||
.hover\:text-primary:hover { color: var(--primary); }
|
||||
.hover\:shadow-primary-30:hover { box-shadow: 0 0 0 1px rgba(32, 165, 58, 0.3); }
|
||||
|
||||
/* =========================== */
|
||||
/* 响应式工具类 */
|
||||
/* =========================== */
|
||||
@media (min-width: 640px) {
|
||||
.sm\:flex-row { flex-direction: row; }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:pt-24 { padding-top: 6rem; }
|
||||
.md\:pb-28 { padding-bottom: 7rem; }
|
||||
}
|
||||
|
||||
/* =========================== */
|
||||
/* 复用主项目的组件样式 */
|
||||
/* =========================== */
|
||||
|
||||
/* 容器 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* 搜索输入框 */
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.125rem;
|
||||
border: 1px solid var(--primary);
|
||||
font-size: 1rem;
|
||||
line-height: 1.75rem;
|
||||
outline: none;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px var(--primary-10);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.clear-input-button {
|
||||
font-size: 1.6rem;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 14.5px;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.clear-input-button:hover {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.clear-input-button.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* 主要操作按钮 */
|
||||
.primary-action-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.125rem;
|
||||
transition: all 0.15s ease-in-out;
|
||||
box-shadow: 0 4px 6px -2px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.primary-action-button:hover {
|
||||
background-color: var(--primary-90);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-button {
|
||||
border: 1px solid var(--primary);
|
||||
background-color: white;
|
||||
color: var(--primary);
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.125rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-in-out;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background-color: var(--primary-5);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* 悬停提升效果 */
|
||||
.hover-lift {
|
||||
transition:
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-0.125rem);
|
||||
}
|
||||
|
||||
/* =========================== */
|
||||
/* WHOIS页面专用组件样式 */
|
||||
/* =========================== */
|
||||
|
||||
/* 简洁表格式布局 */
|
||||
.whois-simple-layout {
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.whois-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.whois-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin: 0 0 10px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.title-decoration {
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
background: var(--primary);
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.domain-name {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.whois-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.refresh-link {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.refresh-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.whois-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.whois-row {
|
||||
display: flex;
|
||||
min-height: 45px;
|
||||
padding: 0 0 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.whois-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.whois-label {
|
||||
width: 160px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.label-cn {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.label-en {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.whois-value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.whois-value-multiple {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.whois-dns-item {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: #f8f9fa;
|
||||
padding: 4px 8px;
|
||||
border-radius: 2px;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.whois-status-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.whois-status-badge-simple {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: #e8f5e8;
|
||||
color: #2d5a2d;
|
||||
border: 1px solid #c3e6c3;
|
||||
}
|
||||
|
||||
.whois-status-badge-simple.status-active {
|
||||
background: #e8f5e8;
|
||||
color: #2d5a2d;
|
||||
border-color: #c3e6c3;
|
||||
}
|
||||
|
||||
.whois-status-badge-simple.status-transfer {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border-color: #ffeaa7;
|
||||
}
|
||||
|
||||
.whois-status-badge-simple.status-unknown {
|
||||
background: #e2e3e5;
|
||||
color: #495057;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
/* 原始数据区域 */
|
||||
.whois-raw-data-section {
|
||||
margin-top: 30px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.raw-data-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.raw-data-toggle:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.raw-data-toggle i {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.raw-data-toggle.expanded i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.raw-data-content {
|
||||
display: block; /* 默认展开 */
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.raw-data-content.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.raw-data-box {
|
||||
background: #f8f9fa;
|
||||
color: #495057;
|
||||
padding: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.raw-data-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.raw-data-title .title-decoration {
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
/* 原始数据滚动条样式 */
|
||||
.raw-data-box::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.raw-data-box::-webkit-scrollbar-track {
|
||||
background: #f1f3f4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.raw-data-box::-webkit-scrollbar-thumb {
|
||||
background: #c1c8cd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.raw-data-box::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8b2ba;
|
||||
}
|
||||
|
||||
/* 原始数据文字样式优化 */
|
||||
.raw-data-box {
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
/* 原始数据字段高亮 */
|
||||
.raw-data-box {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 优化行高和间距 */
|
||||
.raw-data-content {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.raw-data-content.hide {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
/* 切换按钮样式优化 */
|
||||
.raw-data-toggle {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.raw-data-toggle:hover {
|
||||
background-color: rgba(32, 165, 58, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* WHOIS概览卡片 - 保持兼容性,但隐藏 */
|
||||
.whois-overview-card {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.whois-overview-card:hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.whois-domain-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.whois-domain-name {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.whois-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.whois-registrar-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.whois-registrar-info i {
|
||||
color: var(--primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* WHOIS信息网格 */
|
||||
.whois-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.whois-info-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.whois-card-header {
|
||||
background: linear-gradient(135deg, var(--primary-5) 0%, #f8fafc 100%);
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.whois-card-icon {
|
||||
color: var(--primary);
|
||||
font-size: 18px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.whois-card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.whois-card-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.whois-info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.whois-info-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.whois-info-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.whois-info-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.whois-info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #1e293b;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* DNS服务器特殊样式 */
|
||||
.whois-dns-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.whois-dns-servers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.whois-dns-server {
|
||||
display: inline-block;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.whois-dns-server:hover {
|
||||
background: var(--primary-5);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* =========================== */
|
||||
/* 响应式设计 */
|
||||
/* =========================== */
|
||||
@media (max-width: 768px) {
|
||||
.whois-simple-layout {
|
||||
padding: 20px 16px;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.whois-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.title-decoration {
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.whois-meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.whois-value-multiple {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.whois-dns-item {
|
||||
font-size: 12px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
.whois-label{
|
||||
width: 85px;
|
||||
}
|
||||
.whois-value{
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.whois-simple-layout {
|
||||
padding: 16px 12px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.whois-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.title-decoration {
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 旧版响应式样式 - 保持兼容性 */
|
||||
@media (max-width: 768px) {
|
||||
.whois-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.whois-domain-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.whois-domain-name {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.whois-info-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.whois-info-value {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.whois-dns-servers {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
|
@ -61,8 +61,6 @@ export const TPL_CONFIRM_DELETE_MODAL = 'confirm-delete-modal-template' as const
|
|||
// 域名注册协议
|
||||
export const TPL_DOMAIN_AGREEMENT_MODAL = 'domain-agreement-modal-template' as const
|
||||
|
||||
// WHOIS查询
|
||||
export const TPL_WHOIS_MODAL = 'whois-modal-template' as const
|
||||
|
||||
/** 模板常量字典,便于遍历或注入 */
|
||||
export const TEMPLATES = {
|
||||
|
|
@ -92,5 +90,4 @@ export const TEMPLATES = {
|
|||
TPL_ORDER_SUCCESS,
|
||||
TPL_CONFIRM_DELETE_MODAL,
|
||||
TPL_DOMAIN_AGREEMENT_MODAL,
|
||||
TPL_WHOIS_MODAL,
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -84,3 +84,21 @@ export interface DomainQueryCheckRequest {
|
|||
/** 推荐类型(可选),用于筛选特定类型的推荐域名后缀 */
|
||||
recommend_type?: number
|
||||
}
|
||||
|
||||
export interface AiDomainQueryCheckRequest {
|
||||
/** 品牌名称 */
|
||||
brand?: string;
|
||||
/** 所属行业 */
|
||||
industry?: string
|
||||
}
|
||||
|
||||
export interface AiDomainQueryCheckResponseData {
|
||||
/** 数据 */
|
||||
data: AiDatum[];
|
||||
}
|
||||
|
||||
// 继承Datum基础上增加meaning字段
|
||||
export interface AiDatum extends Datum {
|
||||
/** 含义 */
|
||||
meaning: string;
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ export default defineConfig({
|
|||
build: {
|
||||
minify: "terser", // 混淆器,terser构建后文件体积更小
|
||||
sourcemap: false,
|
||||
cssCodeSplit: false, // 不分割css代码
|
||||
cssCodeSplit: true, // 不分割css代码
|
||||
reportCompressedSize: false, // 不统计gzip压缩后的文件大小
|
||||
chunkSizeWarningLimit: 800, // 警告阈值
|
||||
assetsInlineLimit: 2048, // 小于2kb的资源内联
|
||||
|
|
@ -51,6 +51,7 @@ export default defineConfig({
|
|||
input: {
|
||||
main: resolve(__dirname, "index.html"),
|
||||
registration: resolve(__dirname, "domain-query-register.html"),
|
||||
whois: resolve(__dirname, "domain-whois.html"),
|
||||
},
|
||||
strictDeprecations: true, // 严格弃用
|
||||
output: {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,231 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { glob } from "glob";
|
||||
import os from "os";
|
||||
|
||||
// 全局随机参数变量,在插件初始化时设置
|
||||
let randomParam = null;
|
||||
|
||||
// Windows兼容性常量
|
||||
const WINDOWS_RESERVED_NAMES = [
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"NUL",
|
||||
"COM1",
|
||||
"COM2",
|
||||
"COM3",
|
||||
"COM4",
|
||||
"COM5",
|
||||
"COM6",
|
||||
"COM7",
|
||||
"COM8",
|
||||
"COM9",
|
||||
"LPT1",
|
||||
"LPT2",
|
||||
"LPT3",
|
||||
"LPT4",
|
||||
"LPT5",
|
||||
"LPT6",
|
||||
"LPT7",
|
||||
"LPT8",
|
||||
"LPT9",
|
||||
];
|
||||
const WINDOWS_INVALID_CHARS = /[<>:"|?*\x00-\x1f]/g;
|
||||
const MAX_PATH_LENGTH = 260; // Windows MAX_PATH限制
|
||||
const MAX_FILENAME_LENGTH = 255; // 大多数文件系统的文件名长度限制
|
||||
|
||||
/**
|
||||
* Windows兼容性工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 检查是否为Windows系统
|
||||
* @returns {boolean} 是否为Windows
|
||||
*/
|
||||
function isWindows() {
|
||||
return os.platform() === "win32";
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化Windows文件名
|
||||
* @param {string} filename - 原始文件名
|
||||
* @returns {string} 规范化后的文件名
|
||||
*/
|
||||
function normalizeWindowsFilename(filename) {
|
||||
if (!isWindows()) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
// 移除无效字符
|
||||
let normalized = filename.replace(WINDOWS_INVALID_CHARS, "_");
|
||||
|
||||
// 移除尾部的点和空格
|
||||
normalized = normalized.replace(/[. ]+$/, "");
|
||||
|
||||
// 检查保留名称
|
||||
const baseName = normalized.split(".")[0].toUpperCase();
|
||||
if (WINDOWS_RESERVED_NAMES.includes(baseName)) {
|
||||
normalized = `_${normalized}`;
|
||||
}
|
||||
|
||||
// 限制文件名长度
|
||||
if (normalized.length > MAX_FILENAME_LENGTH) {
|
||||
const ext = path.extname(normalized);
|
||||
const nameWithoutExt = path.basename(normalized, ext);
|
||||
const maxNameLength = MAX_FILENAME_LENGTH - ext.length;
|
||||
normalized = nameWithoutExt.substring(0, maxNameLength) + ext;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径长度是否超出Windows限制
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {boolean} 是否超出限制
|
||||
*/
|
||||
function isPathTooLong(filePath) {
|
||||
return isWindows() && filePath.length > MAX_PATH_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Windows兼容的安全时间戳
|
||||
* @returns {string} 安全的时间戳字符串
|
||||
*/
|
||||
function generateSafeTimestamp() {
|
||||
const now = new Date();
|
||||
const timestamp = now
|
||||
.toISOString()
|
||||
.replace(/[:.]/g, "-")
|
||||
.replace("T", "_")
|
||||
.substring(0, 19); // 移除毫秒部分
|
||||
|
||||
return normalizeWindowsFilename(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化glob模式以确保Windows兼容性
|
||||
* @param {string} pattern - glob模式
|
||||
* @returns {string} 规范化后的glob模式
|
||||
*/
|
||||
function normalizeGlobPattern(pattern) {
|
||||
if (!pattern) return pattern;
|
||||
|
||||
// 在Windows系统中,确保glob模式使用正斜杠
|
||||
// glob库在Windows中也期望使用正斜杠作为路径分隔符
|
||||
if (isWindows()) {
|
||||
return pattern.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Windows兼容的glob选项
|
||||
* @param {Object} baseOptions - 基础选项
|
||||
* @returns {Object} 兼容的glob选项
|
||||
*/
|
||||
function createWindowsCompatibleGlobOptions(baseOptions = {}) {
|
||||
const options = { ...baseOptions };
|
||||
|
||||
if (isWindows()) {
|
||||
// Windows特定的glob选项
|
||||
options.windowsPathsNoEscape = true;
|
||||
options.nonull = false; // 避免返回无匹配的模式
|
||||
options.dot = options.dot !== false; // 默认包含点文件
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化目录路径用于glob搜索
|
||||
* @param {string} dirPath - 目录路径
|
||||
* @returns {string} 规范化后的目录路径
|
||||
*/
|
||||
function normalizeGlobDirectory(dirPath) {
|
||||
if (!dirPath) return dirPath;
|
||||
|
||||
// 规范化路径
|
||||
let normalized = path.normalize(dirPath);
|
||||
|
||||
// 在Windows中,将反斜杠转换为正斜杠用于glob
|
||||
if (isWindows()) {
|
||||
normalized = normalized.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化文件路径,确保Windows兼容性
|
||||
* @param {string} filePath - 原始文件路径
|
||||
* @returns {string} 规范化后的路径
|
||||
*/
|
||||
function normalizeFilePath(filePath) {
|
||||
if (!filePath) return filePath;
|
||||
|
||||
// 使用path.normalize处理路径分隔符
|
||||
let normalized = path.normalize(filePath);
|
||||
|
||||
if (isWindows()) {
|
||||
// 处理文件名部分
|
||||
const dir = path.dirname(normalized);
|
||||
const filename = path.basename(normalized);
|
||||
const normalizedFilename = normalizeWindowsFilename(filename);
|
||||
normalized = path.join(dir, normalizedFilename);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件路径的Windows兼容性
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Object} 验证结果 {valid: boolean, error?: string, normalized?: string}
|
||||
*/
|
||||
function validateFilePath(filePath, options = {}) {
|
||||
const result = { valid: true };
|
||||
|
||||
try {
|
||||
// 规范化路径
|
||||
const normalized = normalizeFilePath(filePath);
|
||||
result.normalized = normalized;
|
||||
|
||||
// 检查路径长度
|
||||
if (isPathTooLong(normalized)) {
|
||||
result.valid = false;
|
||||
result.error = `路径长度超出限制 (${normalized.length} > ${MAX_PATH_LENGTH})`;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 检查文件名
|
||||
const filename = path.basename(normalized);
|
||||
if (isWindows() && WINDOWS_INVALID_CHARS.test(filename)) {
|
||||
result.valid = false;
|
||||
result.error = `文件名包含Windows不支持的字符: ${filename}`;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 检查保留名称
|
||||
const baseName = path
|
||||
.basename(filename, path.extname(filename))
|
||||
.toUpperCase();
|
||||
if (isWindows() && WINDOWS_RESERVED_NAMES.includes(baseName)) {
|
||||
result.valid = false;
|
||||
result.error = `文件名使用了Windows保留名称: ${baseName}`;
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
result.valid = false;
|
||||
result.error = `路径验证失败: ${error.message}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机数参数
|
||||
* @param {Object} options - 配置选项
|
||||
|
|
@ -132,8 +353,6 @@ function processFileContent(content, options = {}) {
|
|||
}v=${randomParam}${suffix}`
|
||||
: match;
|
||||
}
|
||||
console.log(randomParam, "时间戳");
|
||||
// console.log(match, prefix, filePath, suffix);
|
||||
return `${prefix}${filePath}${
|
||||
filePath.includes("?") ? "&" : "?"
|
||||
}v=${randomParam}${suffix}`;
|
||||
|
|
@ -197,9 +416,9 @@ function processSingleFile(filePath, options = {}) {
|
|||
|
||||
if (content !== processedContent) {
|
||||
fs.writeFileSync(filePath, processedContent, "utf8");
|
||||
if (options.logger) {
|
||||
options.logger(`✅ 已处理: ${filePath}`);
|
||||
}
|
||||
// if (options.logger) {
|
||||
// options.logger(`✅ 已处理: ${filePath}`);
|
||||
// }
|
||||
return true;
|
||||
} else {
|
||||
if (options.logger) {
|
||||
|
|
@ -249,9 +468,23 @@ function processBatchFiles(directory, options = {}) {
|
|||
? directory
|
||||
: path.resolve(directory);
|
||||
|
||||
// 验证目录路径的Windows兼容性
|
||||
const pathValidation = validateFilePath(absoluteDir);
|
||||
if (!pathValidation.valid) {
|
||||
const error = `目录路径不兼容: ${pathValidation.error}`;
|
||||
if (mergedOptions.logger) {
|
||||
mergedOptions.logger(`❌ ${error}`);
|
||||
}
|
||||
stats.errors.push({ file: "DIRECTORY_VALIDATION", error });
|
||||
return stats;
|
||||
}
|
||||
|
||||
// 使用规范化后的路径
|
||||
const normalizedDir = pathValidation.normalized || absoluteDir;
|
||||
|
||||
// 检查目录是否存在
|
||||
if (!fs.existsSync(absoluteDir)) {
|
||||
const error = `目录不存在: ${absoluteDir}`;
|
||||
if (!fs.existsSync(normalizedDir)) {
|
||||
const error = `目录不存在: ${normalizedDir}`;
|
||||
if (mergedOptions.logger) {
|
||||
mergedOptions.logger(`❌ ${error}`);
|
||||
}
|
||||
|
|
@ -260,7 +493,7 @@ function processBatchFiles(directory, options = {}) {
|
|||
}
|
||||
|
||||
if (mergedOptions.logger) {
|
||||
mergedOptions.logger(`🔍 扫描目录: ${absoluteDir}`);
|
||||
mergedOptions.logger(`🔍 扫描目录: ${normalizedDir}`);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -268,30 +501,66 @@ function processBatchFiles(directory, options = {}) {
|
|||
let allFiles = [];
|
||||
|
||||
mergedOptions.patterns.forEach((pattern) => {
|
||||
const searchPattern = path.join(absoluteDir, pattern);
|
||||
const ignorePatterns = mergedOptions.ignore.map((ig) =>
|
||||
path.join(absoluteDir, ig)
|
||||
);
|
||||
// 规范化glob模式以确保Windows兼容性
|
||||
const normalizedPattern = normalizeGlobPattern(pattern);
|
||||
const globDir = normalizeGlobDirectory(normalizedDir);
|
||||
const searchPattern = path.posix.join(globDir, normalizedPattern);
|
||||
const ignorePatterns = mergedOptions.ignore.map((ig) => {
|
||||
const normalizedIgnore = normalizeGlobPattern(ig);
|
||||
return path.posix.join(globDir, normalizedIgnore);
|
||||
});
|
||||
|
||||
try {
|
||||
const files = glob.sync(searchPattern, {
|
||||
const globOptions = createWindowsCompatibleGlobOptions({
|
||||
ignore: ignorePatterns,
|
||||
absolute: true,
|
||||
nodir: true, // 只返回文件,不包括目录
|
||||
});
|
||||
|
||||
if (mergedOptions.logger && files.length > 0) {
|
||||
// Windows调试信息
|
||||
if (isWindows() && mergedOptions.logger) {
|
||||
mergedOptions.logger(`🔍 Windows模式 - 搜索模式: ${searchPattern}`);
|
||||
mergedOptions.logger(
|
||||
`📄 找到 ${files.length} 个匹配文件 (${pattern})`
|
||||
`🔍 Windows模式 - 忽略模式: ${ignorePatterns.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
const files = glob.sync(searchPattern, globOptions);
|
||||
|
||||
if (mergedOptions.logger) {
|
||||
if (files.length > 0) {
|
||||
mergedOptions.logger(
|
||||
`📄 找到 ${files.length} 个匹配文件 (${pattern})`
|
||||
);
|
||||
} else {
|
||||
mergedOptions.logger(
|
||||
`⚠️ 模式 "${pattern}" 未找到匹配文件${
|
||||
isWindows() ? " (Windows系统)" : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
allFiles.push(...files);
|
||||
} catch (globError) {
|
||||
const error = `模式匹配失败 (${pattern}): ${globError.message}`;
|
||||
stats.errors.push({ file: pattern, error });
|
||||
const error = `模式匹配失败 (${pattern}): ${globError.message}${
|
||||
isWindows() ? " [Windows系统]" : ""
|
||||
}`;
|
||||
stats.errors.push({
|
||||
file: pattern,
|
||||
error,
|
||||
platform: isWindows() ? "Windows" : "Unix",
|
||||
searchPattern,
|
||||
ignorePatterns,
|
||||
});
|
||||
if (mergedOptions.logger) {
|
||||
mergedOptions.logger(`⚠️ ${error}`);
|
||||
mergedOptions.logger(`❌ ${error}`);
|
||||
if (isWindows()) {
|
||||
mergedOptions.logger(`🔍 调试信息 - 搜索路径: ${searchPattern}`);
|
||||
mergedOptions.logger(
|
||||
`🔍 调试信息 - 目录存在: ${fs.existsSync(normalizedDir)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -315,12 +584,27 @@ function processBatchFiles(directory, options = {}) {
|
|||
if (mergedOptions.logger) {
|
||||
mergedOptions.logger(
|
||||
`\n[${index + 1}/${stats.totalCount}] 处理: ${path.relative(
|
||||
absoluteDir,
|
||||
normalizedDir,
|
||||
file
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
// 验证单个文件路径
|
||||
const fileValidation = validateFilePath(file);
|
||||
if (!fileValidation.valid) {
|
||||
stats.failedCount++;
|
||||
stats.errors.push({
|
||||
file: path.relative(normalizedDir, file),
|
||||
error: `文件路径验证失败: ${fileValidation.error}`,
|
||||
});
|
||||
if (mergedOptions.logger) {
|
||||
mergedOptions.logger(`❌ 跳过无效文件: ${fileValidation.error}`);
|
||||
}
|
||||
return; // 跳过此文件
|
||||
}
|
||||
|
||||
const normalizedFile = fileValidation.normalized || file;
|
||||
let retries = 0;
|
||||
let success = false;
|
||||
|
||||
|
|
@ -330,7 +614,7 @@ function processBatchFiles(directory, options = {}) {
|
|||
|
||||
if (mergedOptions.createBackup) {
|
||||
// 使用安全处理函数
|
||||
const result = processSingleFileSafe(file, mergedOptions);
|
||||
const result = processSingleFileSafe(normalizedFile, mergedOptions);
|
||||
|
||||
if (result.success) {
|
||||
success = true;
|
||||
|
|
@ -347,7 +631,7 @@ function processBatchFiles(directory, options = {}) {
|
|||
}
|
||||
} else {
|
||||
// 使用原有的处理函数
|
||||
const modified = processSingleFile(file, mergedOptions);
|
||||
const modified = processSingleFile(normalizedFile, mergedOptions);
|
||||
success = true;
|
||||
|
||||
if (modified) {
|
||||
|
|
@ -525,6 +809,7 @@ export default function randomCachePlugin(userOptions = {}) {
|
|||
}
|
||||
},
|
||||
|
||||
// 编辑结束
|
||||
buildEnd() {
|
||||
if (options.enableLog) {
|
||||
logger("✨ Random Cache Plugin 处理完成");
|
||||
|
|
@ -543,11 +828,20 @@ function createBackup(filePath, options = {}) {
|
|||
try {
|
||||
const backupDir =
|
||||
options.backupDir || path.join(path.dirname(filePath), ".backup");
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const timestamp = generateSafeTimestamp();
|
||||
const fileName = path.basename(filePath);
|
||||
const backupFileName = `${fileName}.${timestamp}.backup`;
|
||||
const backupFileName = normalizeWindowsFilename(
|
||||
`${fileName}.${timestamp}.backup`
|
||||
);
|
||||
const backupPath = path.join(backupDir, backupFileName);
|
||||
|
||||
// 检查路径长度
|
||||
if (isPathTooLong(backupPath)) {
|
||||
throw new Error(
|
||||
`备份路径过长 (${backupPath.length} > ${MAX_PATH_LENGTH}): ${backupPath}`
|
||||
);
|
||||
}
|
||||
|
||||
// 确保备份目录存在
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
|
|
@ -725,24 +1019,76 @@ function batchReplaceWithRandomCache(target, options = {}) {
|
|||
? target
|
||||
: path.resolve(target);
|
||||
|
||||
if (!fs.existsSync(absoluteDir)) {
|
||||
throw new Error(`目标目录不存在: ${absoluteDir}`);
|
||||
// 验证目录路径的Windows兼容性
|
||||
const pathValidation = validateFilePath(absoluteDir);
|
||||
if (!pathValidation.valid) {
|
||||
throw new Error(`目标目录路径不兼容: ${pathValidation.error}`);
|
||||
}
|
||||
|
||||
logger(`🔍 扫描目录: ${absoluteDir}`);
|
||||
const normalizedDir = pathValidation.normalized || absoluteDir;
|
||||
|
||||
if (!fs.existsSync(normalizedDir)) {
|
||||
throw new Error(`目标目录不存在: ${normalizedDir}`);
|
||||
}
|
||||
|
||||
logger(`🔍 扫描目录: ${normalizedDir}`);
|
||||
|
||||
mergedOptions.patterns.forEach((pattern) => {
|
||||
const searchPattern = path.join(absoluteDir, pattern);
|
||||
const ignorePatterns = mergedOptions.ignore.map((ig) =>
|
||||
path.join(absoluteDir, ig)
|
||||
);
|
||||
// 规范化glob模式以确保Windows兼容性
|
||||
const normalizedPattern = normalizeGlobPattern(pattern);
|
||||
const globDir = normalizeGlobDirectory(normalizedDir);
|
||||
const searchPattern = path.posix.join(globDir, normalizedPattern);
|
||||
const ignorePatterns = mergedOptions.ignore.map((ig) => {
|
||||
const normalizedIgnore = normalizeGlobPattern(ig);
|
||||
return path.posix.join(globDir, normalizedIgnore);
|
||||
});
|
||||
|
||||
const files = glob.sync(searchPattern, {
|
||||
const globOptions = createWindowsCompatibleGlobOptions({
|
||||
ignore: ignorePatterns,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
filesToProcess.push(...files);
|
||||
// Windows调试信息
|
||||
if (isWindows() && mergedOptions.enableLog) {
|
||||
logger(`🔍 Windows模式 - 搜索模式: ${searchPattern}`);
|
||||
logger(`🔍 Windows模式 - 忽略模式: ${ignorePatterns.join(", ")}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const files = glob.sync(searchPattern, globOptions);
|
||||
|
||||
if (mergedOptions.enableLog) {
|
||||
if (files.length > 0) {
|
||||
logger(`📄 模式 "${pattern}" 找到 ${files.length} 个文件`);
|
||||
} else {
|
||||
logger(
|
||||
`⚠️ 模式 "${pattern}" 未找到匹配文件${
|
||||
isWindows() ? " (Windows系统)" : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
filesToProcess.push(...files);
|
||||
} catch (globError) {
|
||||
const error = `模式匹配失败 (${pattern}): ${globError.message}${
|
||||
isWindows() ? " [Windows系统]" : ""
|
||||
}`;
|
||||
stats.errors.push({
|
||||
file: pattern,
|
||||
error,
|
||||
platform: isWindows() ? "Windows" : "Unix",
|
||||
searchPattern,
|
||||
ignorePatterns,
|
||||
});
|
||||
if (mergedOptions.enableLog) {
|
||||
logger(`❌ ${error}`);
|
||||
if (isWindows()) {
|
||||
logger(`🔍 调试信息 - 搜索路径: ${searchPattern}`);
|
||||
logger(`🔍 调试信息 - 目录存在: ${fs.existsSync(normalizedDir)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 去重
|
||||
|
|
@ -824,4 +1170,14 @@ export {
|
|||
restoreFromBackup,
|
||||
processSingleFileSafe,
|
||||
batchReplaceWithRandomCache,
|
||||
// Windows兼容性工具函数
|
||||
isWindows,
|
||||
normalizeWindowsFilename,
|
||||
normalizeFilePath,
|
||||
validateFilePath,
|
||||
isPathTooLong,
|
||||
generateSafeTimestamp,
|
||||
normalizeGlobPattern,
|
||||
createWindowsCompatibleGlobOptions,
|
||||
normalizeGlobDirectory,
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,8 +5,8 @@
|
|||
<link rel="icon" href="./favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AllinSSL</title>
|
||||
<script type="module" crossorigin src="./static/js/main-BVxorWyM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./static/css/style-Cb9FPhWh.css">
|
||||
<script type="module" crossorigin src="./static/js/main-Dxgnl0V0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./static/css/style-C8g9wpmL.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{u as t}from"./index-PcCxPxDa.js";import{d as s,c as o}from"./main-BVxorWyM.js";import"./useStore-CPojdwSJ.js";import"./index-yTfFo-nL.js";import"./access-tNugO07J.js";import"./index-BhUxUanl.js";import"./index-D7kMPrCO.js";import"./throttle-C61W3BCh.js";import"./DownloadOutline-B1jf4tcR.js";import"./data-BmphZhVh.js";import"./index-D_D2Mlc-.js";import"./business-DFqzW9Fz.js";import"./index-leYjYcVi.js";import"./text-D7JJPoiP.js";import"./Flex-B1kHIVtt.js";const e=s({name:"CAManageForm",props:{isEdit:{type:Boolean,default:!1},editId:{type:String,default:""}},setup(s){const{CAForm:e}=t(s);return()=>o(e,{labelPlacement:"top"},null)}});export{e as default};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue