Files
getDomain/domain-web/src/views/settings/SettingsView.vue
Your Name 32efff1670 dev
2026-04-16 13:05:07 +08:00

400 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<PageCard title="系统设置" description="保存后会同步写入本地 JSON、Redis以及 Web 后台自己的运行配置。">
<el-skeleton :loading="loading" animated>
<template #template>
<el-skeleton-item variant="rect" style="width: 100%; height: 480px" />
</template>
<div class="settings-grid">
<PageCard title="运行配置">
<el-form label-position="top">
<el-form-item label="检测线程数">
<el-input-number v-model="threadCount" :min="1" :max="64" />
</el-form-item>
<el-form-item label="Worker 运行模式">
<el-select v-model="runtimeSettings.worker_mode">
<el-option label="Windows 本地" value="windows-local" />
<el-option label="Linux systemd" value="linux-systemd" />
</el-select>
</el-form-item>
<el-form-item label="Worker 服务名">
<el-input v-model="runtimeSettings.worker_service_name" />
</el-form-item>
<el-form-item label="API 服务名">
<el-input v-model="runtimeSettings.api_service_name" />
</el-form-item>
</el-form>
</PageCard>
<PageCard title="代理配置">
<el-form label-position="top">
<el-form-item label="启用代理">
<el-switch v-model="proxyConfig.proxy_enable" />
</el-form-item>
<el-form-item label="允许直连兜底">
<el-switch v-model="proxyConfig.allow_direct" />
</el-form-item>
<el-form-item label="首选代理池链接">
<el-input v-model="proxyConfig.proxy_url" placeholder="优先使用的代理池链接" />
</el-form-item>
<el-form-item label="代理池列表">
<el-input
v-model="proxyUrlsText"
type="textarea"
:rows="8"
placeholder="每行一个代理池链接"
/>
</el-form-item>
</el-form>
</PageCard>
<PageCard title="检测选项" class="span-2">
<div class="detect-items">
<div v-for="(item, index) in detectItems" :key="item.key" class="detect-item">
<div class="detect-main">
<span class="order">{{ index + 1 }}</span>
<el-switch v-model="item.enabled" />
<span class="label">{{ item.label }}</span>
</div>
<div class="detect-actions">
<el-button size="small" :disabled="index === 0" @click="moveItem(index, -1)">上移</el-button>
<el-button size="small" :disabled="index === detectItems.length - 1" @click="moveItem(index, 1)">
下移
</el-button>
</div>
</div>
</div>
</PageCard>
<div class="actions span-2">
<el-button type="primary" @click="saveSettings">保存当前配置</el-button>
<el-button @click="loadSettings">重新读取</el-button>
<el-button @click="createBackup">创建配置备份</el-button>
<el-button @click="exportSettingsSnapshot">导出配置快照</el-button>
<el-button @click="triggerImport">导入配置快照</el-button>
<input
ref="importInput"
type="file"
accept="application/json,.json"
class="hidden-input"
@change="handleImport"
/>
</div>
<PageCard title="配置备份记录" class="span-2">
<div class="backup-toolbar">
<span class="backup-tip">导入配置前系统会自动生成一份导入前备份</span>
<el-button size="small" @click="loadBackups">刷新备份列表</el-button>
</div>
<el-table :data="backups" border>
<el-table-column prop="filename" label="文件名" min-width="280" show-overflow-tooltip />
<el-table-column prop="backup_reason" label="备份原因" min-width="120" />
<el-table-column prop="modified_at" label="生成时间" min-width="180" />
<el-table-column prop="size" label="大小" min-width="100" />
<el-table-column label="操作" min-width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="downloadBackup(row.filename)">下载</el-button>
</template>
</el-table-column>
<el-table-column prop="path" label="文件路径" min-width="360" show-overflow-tooltip />
</el-table>
</PageCard>
</div>
</el-skeleton>
</PageCard>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import PageCard from "@/components/PageCard.vue";
import { settingsApi } from "@/api/modules";
type DetectItem = {
key: string;
label: string;
enabled: boolean;
};
type BackupRecord = {
filename: string;
path: string;
size: number;
modified_at: string;
backup_reason?: string;
};
const DETECT_LABELS: Record<string, string> = {
detect_register: "检查注册",
detect_baidu_site: "百度 site 查询",
detect_360_site: "360 site 查询",
detect_chinaz: "站长之家查询",
detect_aizhan: "爱站网查询",
detect_wayback: "时光机检测",
detect_jucha: "聚查查询",
detect_juziseo: "桔子查询"
};
const loading = ref(true);
const threadCount = ref(2);
const detectItems = ref<DetectItem[]>([]);
const backups = ref<BackupRecord[]>([]);
const proxyConfig = ref<Record<string, any>>({
proxy_enable: false,
allow_direct: false,
proxy_url: "",
proxy_urls: []
});
const proxyUrlsText = ref("");
const runtimeSettings = ref({
worker_mode: "windows-local",
worker_service_name: "domaincheck-worker",
api_service_name: "domaincheck-api"
});
const importInput = ref<HTMLInputElement | null>(null);
const normalizeDetectItems = (detectOptions: Record<string, boolean | string[]>) => {
const order = Array.isArray(detectOptions.detect_order) ? [...(detectOptions.detect_order as string[])] : [];
const knownKeys = Object.keys(DETECT_LABELS);
knownKeys.forEach((key) => {
if (!order.includes(key)) {
order.push(key);
}
});
detectItems.value = order
.filter((key) => DETECT_LABELS[key])
.map((key) => ({
key,
label: DETECT_LABELS[key],
enabled: Boolean(detectOptions[key])
}));
};
const moveItem = (index: number, offset: -1 | 1) => {
const targetIndex = index + offset;
if (targetIndex < 0 || targetIndex >= detectItems.value.length) return;
const items = [...detectItems.value];
const [current] = items.splice(index, 1);
items.splice(targetIndex, 0, current);
detectItems.value = items;
};
const loadBackups = async () => {
try {
const response = await settingsApi.getSettingsBackups();
backups.value = response.data || [];
} catch {
ElMessage.error("读取配置备份列表失败");
}
};
const loadSettings = async () => {
loading.value = true;
try {
const response = await settingsApi.getSettings();
threadCount.value = response.data.thread_count;
normalizeDetectItems(response.data.detect_options || {});
proxyConfig.value = {
proxy_enable: Boolean(response.data.proxy_config?.proxy_enable),
allow_direct: Boolean(response.data.proxy_config?.allow_direct),
proxy_url: response.data.proxy_config?.proxy_url || "",
proxy_urls: response.data.proxy_config?.proxy_urls || []
};
proxyUrlsText.value = (proxyConfig.value.proxy_urls || []).join("\n");
runtimeSettings.value = {
worker_mode: response.data.runtime_settings?.worker_mode || "windows-local",
worker_service_name: response.data.runtime_settings?.worker_service_name || "domaincheck-worker",
api_service_name: response.data.runtime_settings?.api_service_name || "domaincheck-api"
};
} catch {
ElMessage.error("读取系统设置失败");
} finally {
loading.value = false;
}
};
const buildSettingsPayload = () => {
const proxyUrls = proxyUrlsText.value
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
const detectOptions: Record<string, boolean | string[]> = {
detect_order: detectItems.value.map((item) => item.key)
};
detectItems.value.forEach((item) => {
detectOptions[item.key] = item.enabled;
});
return {
thread_count: threadCount.value,
detect_options: detectOptions,
proxy_config: {
proxy_enable: Boolean(proxyConfig.value.proxy_enable),
allow_direct: Boolean(proxyConfig.value.allow_direct),
proxy_url: proxyConfig.value.proxy_url || proxyUrls[0] || "",
proxy_urls: proxyUrls
},
runtime_settings: runtimeSettings.value
};
};
const saveSettings = async () => {
try {
await settingsApi.updateSettings(buildSettingsPayload());
ElMessage.success("系统设置已保存");
await loadSettings();
} catch {
ElMessage.error("保存系统设置失败");
}
};
const createBackup = async () => {
try {
const response = await settingsApi.backupSettings();
ElMessage.success(`配置备份已生成:${response.data.filename}`);
await loadBackups();
} catch {
ElMessage.error("创建配置备份失败");
}
};
const exportSettingsSnapshot = async () => {
try {
const response = await settingsApi.exportSettings();
const blob = new Blob([JSON.stringify(response.data, null, 2)], {
type: "application/json;charset=utf-8"
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `domaincheck_settings_${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
link.click();
window.URL.revokeObjectURL(url);
ElMessage.success("配置快照已导出");
} catch {
ElMessage.error("导出配置快照失败");
}
};
const triggerImport = () => {
importInput.value?.click();
};
const handleImport = async (event: Event) => {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
try {
const text = await file.text();
const payload = JSON.parse(text);
await settingsApi.validateImportSettings(payload);
await settingsApi.importSettings(payload);
ElMessage.success("配置快照已导入,系统已自动创建导入前备份");
await Promise.all([loadSettings(), loadBackups()]);
} catch {
ElMessage.error("导入配置快照失败,请确认文件内容有效且结构正确");
} finally {
input.value = "";
}
};
const downloadBackup = (filename: string) => {
window.open(settingsApi.backupDownloadUrl(filename), "_blank");
};
onMounted(async () => {
await Promise.all([loadSettings(), loadBackups()]);
});
</script>
<style scoped>
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 16px;
}
.span-2 {
grid-column: 1 / -1;
}
.detect-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 12px;
}
.detect-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid var(--el-border-color);
border-radius: 12px;
background: var(--el-fill-color-blank);
}
.detect-main {
display: flex;
align-items: center;
gap: 12px;
}
.order {
width: 24px;
height: 24px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.label {
font-weight: 500;
}
.detect-actions {
display: flex;
gap: 8px;
}
.actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.backup-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.backup-tip {
color: #64748b;
font-size: 13px;
}
.hidden-input {
display: none;
}
</style>