Compare commits

...

5 Commits

Author SHA1 Message Date
www
8884f9c416 feat: open storage artifacts via admin preview 2026-04-19 21:29:51 +08:00
www
e801249e21 fix: prefer github blob links in spider md dialog 2026-04-19 19:44:27 +08:00
www
31aecb388a feat: show github-only spider md links 2026-04-19 19:41:42 +08:00
www
992ae11b21 feat: improve spider md github links and copy actions 2026-04-19 19:37:53 +08:00
root
2d1834422b feat: add metadata console and seo ai rule pages 2026-04-18 20:23:30 +08:00
36 changed files with 2091 additions and 173 deletions

View File

@@ -2,6 +2,7 @@ export const loginApiUrl = "/guest/login";
export const getSystemConfigListApiUrl = "/system/config/list"; export const getSystemConfigListApiUrl = "/system/config/list";
export const getSystemConfigSaveApiUrl = "/system/config/save"; export const getSystemConfigSaveApiUrl = "/system/config/save";
export const getSystemConfigSeoAiRulesResetApiUrl = "/system/config/seo-ai-rules/reset";
export const getSystemPlanListApiUrl = "/system/plan/list"; export const getSystemPlanListApiUrl = "/system/plan/list";
export const getSystemPlanSaveApiUrl = "/system/plan/save"; export const getSystemPlanSaveApiUrl = "/system/plan/save";

View File

@@ -264,6 +264,7 @@ export namespace SiteList {
export interface DomainSeoCopyHistoryRollbackData { export interface DomainSeoCopyHistoryRollbackData {
host?: string; host?: string;
rollback_from_job_id?: string; rollback_from_job_id?: string;
job?: DomainImportAsyncJobStatusData;
written_pages?: Array<{ written_pages?: Array<{
scene?: string; scene?: string;
page_key?: string; page_key?: string;
@@ -924,7 +925,15 @@ export namespace SiteList {
skipped_template_missing_count?: number; skipped_template_missing_count?: number;
skipped_duplicate_in_file_count?: number; skipped_duplicate_in_file_count?: number;
strategy_profile_counts?: Record<string, number>; strategy_profile_counts?: Record<string, number>;
inserted_domains?: Array<{ d_id?: number; d_domain?: string; strategy_profile?: SeoStrategyProfile }>; inserted_domains?: Array<{
d_id?: number;
d_domain?: string;
d_name?: string;
t_id?: number;
strategy_profile?: SeoStrategyProfile;
tkd_provider?: string;
tkd_mode?: string;
}>;
skipped_domains?: Array<{ d_domain?: string; reason?: string; t_id?: number }>; skipped_domains?: Array<{ d_domain?: string; reason?: string; t_id?: number }>;
run_root?: string; run_root?: string;
uploaded_excel_path?: string; uploaded_excel_path?: string;
@@ -934,6 +943,67 @@ export namespace SiteList {
probe_failed_count?: number; probe_failed_count?: number;
summary_json_path?: string; summary_json_path?: string;
summary_html_path?: string; summary_html_path?: string;
next_step_notice?: string[];
codex_continue_prompt?: string;
post_import_workflow?: {
enabled?: boolean;
auto_ai_after_import?: boolean;
summary?: string;
inserted_domain_count?: number;
inserted_domain_hosts?: string[];
contains_openai_domains?: boolean;
artifact_audit?: {
summary?: {
total?: number;
state_ready_count?: number;
manifest_ready_count?: number;
positioning_ready_count?: number;
published_copy_ready_count?: number;
sitemap_generated_count?: number;
ai_pending_count?: number;
};
items?: Array<{
host?: string;
site_name?: string;
state_exists?: boolean;
manifest_exists?: boolean;
site_positioning_exists?: boolean;
published_copy_exists?: boolean;
ai_status?: string;
wait_for_ai_before_publish?: boolean;
sitemap_status?: "generated" | "queued_pending" | string;
existing_sitemaps?: string[];
notes?: string[];
}>;
};
recommended_for_operator?: "openai_continue" | "codex_continue";
recommended_for_technical?: "openai_continue" | "codex_continue";
notices?: string[];
choices?: Array<{
key?: "openai_continue" | "codex_continue";
label?: string;
audience?: string;
description?: string;
}>;
openai?: {
domain_ids?: number[];
domain_id_csv?: string;
hosts?: string[];
probe_status?: "passed" | "partial" | "failed" | "skipped" | string;
probe_summary_html_path?: string;
probe_summary_json_path?: string;
operator_steps?: string[];
};
codex?: {
workspace_path?: string;
open_in_vscode_hint?: string;
prompt?: string;
};
ui_suggestion?: {
message?: string;
button_labels?: string[];
};
};
} }
export interface DomainImportRunSummaryItem { export interface DomainImportRunSummaryItem {
@@ -1449,6 +1519,7 @@ export namespace SiteList {
run_id?: string; run_id?: string;
generated_at?: string; generated_at?: string;
status?: string; status?: string;
mode?: string;
run_root?: string; run_root?: string;
domain_count?: number; domain_count?: number;
aggregate_count?: number; aggregate_count?: number;
@@ -1461,8 +1532,13 @@ export namespace SiteList {
gitee_push_attempted?: number; gitee_push_attempted?: number;
gitee_push_status?: string; gitee_push_status?: string;
gitee_push_error?: string; gitee_push_error?: string;
github_pushed?: number;
github_push_attempted?: number;
github_push_status?: string;
github_push_error?: string;
files?: DomainSpiderMdFileItem[]; files?: DomainSpiderMdFileItem[];
gitee_files?: DomainSpiderMdFileItem[]; gitee_files?: DomainSpiderMdFileItem[];
github_files?: DomainSpiderMdFileItem[];
} }
export interface DomainSpiderMdSummaryData { export interface DomainSpiderMdSummaryData {
@@ -1495,6 +1571,15 @@ export namespace SiteList {
root?: string; root?: string;
sources?: Record<string, string>; sources?: Record<string, string>;
}; };
github?: {
enabled?: number;
configured?: number;
owner?: string;
repo?: string;
branch?: string;
root?: string;
sources?: Record<string, string>;
};
latest_run?: DomainSpiderMdRunSummaryItem | null; latest_run?: DomainSpiderMdRunSummaryItem | null;
runs?: DomainSpiderMdRunSummaryItem[]; runs?: DomainSpiderMdRunSummaryItem[];
} }
@@ -1814,6 +1899,7 @@ export namespace SiteList {
generated_at?: string; generated_at?: string;
mode?: string; mode?: string;
note?: string; note?: string;
hero_summary?: string;
real_external_metrics?: boolean; real_external_metrics?: boolean;
host_count?: number; host_count?: number;
ready_count?: number; ready_count?: number;
@@ -1879,6 +1965,7 @@ export namespace SiteList {
generated_at?: string; generated_at?: string;
mode?: string; mode?: string;
note?: string; note?: string;
hero_summary?: string;
real_external_metrics?: boolean; real_external_metrics?: boolean;
host_count?: number; host_count?: number;
ready_count?: number; ready_count?: number;

View File

@@ -9,6 +9,9 @@ export namespace ConfigList {
sc_val: string; sc_val: string;
sc_description: string; sc_description: string;
} }
export interface resetSeoAiRuleParams {
sc_code?: string;
}
export interface delParams { export interface delParams {
sc_id: string; sc_id: string;
} }
@@ -18,5 +21,8 @@ export namespace ConfigList {
sc_val: string; sc_val: string;
sc_description: string; sc_description: string;
} }
export interface SeoAiRuleCard extends ResList {
note?: Partial<ResList> | null;
}
export interface ResData extends ResultData {} export interface ResData extends ResultData {}
} }

View File

@@ -78,4 +78,125 @@ export namespace VideoList {
tag_list?: any; tag_list?: any;
} }
export interface ResData extends ResultData {} export interface ResData extends ResultData {}
export interface MetadataMissingTaskPoolPlanTaskStatus {
exists?: boolean;
enabled?: boolean;
limit_seconds?: number;
limit_human?: string;
status_label?: string;
db_ready?: boolean;
db_error?: string;
pt_id?: number;
pt_name?: string;
pt_code?: string;
last_run_at?: string;
}
export interface MetadataMissingTaskPoolStatusData {
generated_at?: string;
recommended_action?: string;
operator_hint?: string[];
workbench?: {
generated_at?: string;
videos_with_any_missing_metadata?: number;
queue_size?: number;
workbench_index_path?: string;
latest_run?: Record<string, any>;
};
task_pool?: {
generated_at?: string;
batch_count?: number;
pending_count?: number;
processing_count?: number;
done_count?: number;
skipped_count?: number;
task_pool_index_path?: string;
};
plan_task?: MetadataMissingTaskPoolPlanTaskStatus;
}
export interface MetadataMissingTaskPoolOpsData {
summary?: MetadataMissingTaskPoolStatusData;
ops?: {
generated_at?: string;
recommended_action?: string;
command_entries?: Array<{
label?: string;
command?: string;
}>;
artifact_entries?: Array<{
label?: string;
path?: string;
}>;
};
}
export interface MetadataMissingWorkbenchSummaryData {
generated_at?: string;
sample_limit?: number;
queue_limit?: number;
prompt_limit?: number;
run_id?: string;
top_missing_fields?: Array<{
field?: string;
missing_count?: number;
missing_ratio?: number;
}>;
next_actions?: Array<{
key?: string;
label?: string;
summary?: string;
}>;
operator_notes?: Array<{
title?: string;
content?: string;
}>;
queue?: {
queue_size?: number;
items?: Array<Record<string, any>>;
};
audit?: {
total_videos?: number;
videos_with_any_missing_metadata?: number;
field_stats?: Array<Record<string, any>>;
};
codex_dispatch_prompt?: string;
}
export interface MetadataMissingTaskPoolData {
generated_at?: string;
source_generated_at?: string;
batch_size?: number;
batch_limit?: number;
total_videos?: number;
videos_with_any_missing_metadata?: number;
queue_size?: number;
operator_hint?: string;
top_missing_fields?: Array<{
field?: string;
missing_count?: number;
missing_ratio?: number;
}>;
todo_summary?: Array<{
batch_id?: string;
label?: string;
focus_field?: string;
items_count?: number;
priority_level?: string;
}>;
batches?: Array<{
batch_id?: string;
batch_no?: number;
batch_label?: string;
focus_field?: string;
priority_level?: string;
items_count?: number;
status_label?: string;
owner?: string;
history_count?: number;
codex_prompt?: string;
items?: Array<Record<string, any>>;
}>;
}
} }

View File

@@ -1,6 +1,6 @@
import { ResPage, ResultData } from "@/api/interface/index"; import { ResPage, ResultData } from "@/api/interface/index";
import { ConfigList } from "@/api/interface/system/config"; import { ConfigList } from "@/api/interface/system/config";
import { getSystemConfigListApiUrl, getSystemConfigSaveApiUrl } from "@/api/config"; import { getSystemConfigListApiUrl, getSystemConfigSaveApiUrl, getSystemConfigSeoAiRulesResetApiUrl } from "@/api/config";
import http from "@/api"; import http from "@/api";
@@ -12,3 +12,7 @@ export const getConfigList = (params: ConfigList.ReqParams) => {
export const SaveApi = (params: ConfigList.saveParams) => { export const SaveApi = (params: ConfigList.saveParams) => {
return http.post<ResPage<ResultData>>(getSystemConfigSaveApiUrl, params); return http.post<ResPage<ResultData>>(getSystemConfigSaveApiUrl, params);
}; };
export const resetSeoAiRulesApi = (params: ConfigList.resetSeoAiRuleParams) => {
return http.post<ResPage<ResultData>>(getSystemConfigSeoAiRulesResetApiUrl, params);
};

View File

@@ -19,3 +19,27 @@ export const batchData = (params: VideoList.delParams) => {
export const batchVipData = (params: VideoList.delParams) => { export const batchVipData = (params: VideoList.delParams) => {
return http.post<ResPage<ResultData>>(getVideoSetLevelApiUrl, params); return http.post<ResPage<ResultData>>(getVideoSetLevelApiUrl, params);
}; };
export const getMetadataMissingTaskPoolStatus = () => {
return http.get<VideoList.MetadataMissingTaskPoolStatusData>(`/video/metadata/missing/task-pool/status`);
};
export const getMetadataMissingTaskPoolOps = () => {
return http.get<VideoList.MetadataMissingTaskPoolOpsData>(`/video/metadata/missing/task-pool/ops`);
};
export const getMetadataMissingWorkbenchSummary = () => {
return http.get<VideoList.MetadataMissingWorkbenchSummaryData>(`/video/metadata/missing/workbench`);
};
export const getMetadataMissingTaskPoolSummary = () => {
return http.get<VideoList.MetadataMissingTaskPoolData>(`/video/metadata/missing/task-pool`);
};
export const runMetadataMissingWorkbench = () => {
return http.post<ResultData>(`/video/metadata/missing/workbench/run`, {});
};
export const runMetadataMissingTaskPool = () => {
return http.post<ResultData>(`/video/metadata/missing/task-pool/run`, {});
};

View File

@@ -134,6 +134,21 @@
"isKeepAlive": true "isKeepAlive": true
} }
}, },
{
"path": "/system/config/seo-ai-rules",
"name": "systemConfigSeoAiRules",
"component": "/system/config/seoAiRules",
"meta": {
"an_id": 1202,
"icon": "MagicStick",
"title": "SEO AI规则",
"isLink": "",
"isHide": false,
"isFull": false,
"isAffix": false,
"isKeepAlive": true
}
},
{ {
"path": "/resource/endpoint/list", "path": "/resource/endpoint/list",
"name": "endpointList", "name": "endpointList",
@@ -458,6 +473,66 @@
"isKeepAlive": true "isKeepAlive": true
} }
}, },
{
"path": "/video/metadata/missing/workbench",
"name": "videoMetadataMissingWorkbench",
"component": "/video/metadataMissing/index",
"meta": {
"an_id": 2110,
"icon": "DataAnalysis",
"title": "缺字段工作台",
"isLink": "",
"isHide": false,
"isFull": false,
"isAffix": false,
"isKeepAlive": true
}
},
{
"path": "/video/metadata/missing/task-pool",
"name": "videoMetadataMissingTaskPool",
"component": "/video/metadataMissing/index",
"meta": {
"an_id": 2111,
"icon": "Tickets",
"title": "缺字段任务池",
"isLink": "",
"isHide": false,
"isFull": false,
"isAffix": false,
"isKeepAlive": true
}
},
{
"path": "/video/metadata/missing/task-pool/status",
"name": "videoMetadataMissingTaskPoolStatus",
"component": "/video/metadataMissing/index",
"meta": {
"an_id": 2113,
"icon": "DataLine",
"title": "缺字段状态总览",
"isLink": "",
"isHide": true,
"isFull": false,
"isAffix": false,
"isKeepAlive": true
}
},
{
"path": "/video/metadata/missing/task-pool/ops",
"name": "videoMetadataMissingTaskPoolOps",
"component": "/video/metadataMissing/index",
"meta": {
"an_id": 2114,
"icon": "Tools",
"title": "缺字段运维总览",
"isLink": "",
"isHide": true,
"isFull": false,
"isAffix": false,
"isKeepAlive": true
}
},
{ {
"path": "/searchGroup/index", "path": "/searchGroup/index",

View File

@@ -0,0 +1,54 @@
import axios from "axios";
import { ElMessage } from "element-plus";
import { useUserStore } from "@/stores/modules/user";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const STORAGE_PREFIXES = ["external-seo/", "video-metadata-missing/"];
const buildDirectUrl = (path: string) => `${BASE_URL}/${String(path).replace(/^\/+/, "")}`;
const normalizePath = (path?: string) => String(path || "").replace(/^\/+/, "").trim();
const shouldReadByStorageProxy = (path: string) => {
return STORAGE_PREFIXES.some(prefix => path.startsWith(prefix));
};
const buildStoragePreviewUrl = (path: string) => {
const url = new URL(`${BASE_URL}/system/storage/file`);
url.searchParams.set("path", path);
return url.toString();
};
export const openAdminArtifact = async (path?: string) => {
const normalizedPath = normalizePath(path);
if (!normalizedPath) return;
if (!shouldReadByStorageProxy(normalizedPath)) {
window.open(buildDirectUrl(normalizedPath), "_blank", "noopener");
return;
}
const userStore = useUserStore();
const token = String(userStore.token || "").trim();
if (!token) {
ElMessage.error("当前登录态已失效,请重新登录后再打开产物");
return;
}
try {
const response = await axios.get(buildStoragePreviewUrl(normalizedPath), {
headers: {
"admin-token": token
},
responseType: "blob"
});
const blob = response.data instanceof Blob ? response.data : new Blob([response.data]);
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl, "_blank", "noopener");
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
} catch (_error) {
ElMessage.error("打开产物失败,请稍后重试");
}
};

View File

@@ -6,3 +6,101 @@
width: 100%; width: 100%;
gap: 20px; gap: 20px;
} }
.home-metadata-console {
border: 1px solid #e5e7eb;
border-radius: 20px;
background: linear-gradient(135deg, #fffdf4 0%, #ffffff 58%, #eef6ff 100%);
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
&__title {
font-size: 22px;
font-weight: 800;
line-height: 1.2;
color: #111827;
}
&__subtitle {
margin-top: 8px;
color: #6b7280;
line-height: 1.7;
}
&__actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
&__stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-top: 18px;
}
&__hint {
margin-top: 16px;
padding: 14px 16px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.82);
border: 1px solid #e5e7eb;
line-height: 1.8;
color: #374151;
}
&__links {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
margin-top: 12px;
}
}
.home-metadata-stat {
padding: 16px 18px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.9);
border: 1px solid #e5e7eb;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
&__label {
font-size: 13px;
color: #6b7280;
}
&__value {
margin-top: 8px;
font-size: 30px;
font-weight: 800;
line-height: 1.1;
color: #111827;
}
&__tip {
margin-top: 10px;
color: #6b7280;
line-height: 1.7;
font-size: 13px;
}
}
.home-bootstrap-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 0 0 16px;
}
@media (max-width: 960px) {
.home-metadata-console__stats {
grid-template-columns: 1fr;
}
}

View File

@@ -1,5 +1,60 @@
<template> <template>
<div class="home card"> <div class="home card">
<el-card shadow="never" class="home-metadata-console">
<template #header>
<div class="home-metadata-console__header">
<div>
<div class="home-metadata-console__title">视频缺字段控制台</div>
<div class="home-metadata-console__subtitle">后台打开先看这里工作台任务池和计划任务状态都汇总到首屏</div>
</div>
<div class="home-metadata-console__actions">
<el-button :loading="metadataLoading" @click="loadMetadataConsole">刷新状态</el-button>
<el-button type="primary" :loading="workbenchRunning" @click="handleRunWorkbench">刷新工作台</el-button>
<el-button type="warning" plain :loading="taskPoolRunning" @click="handleRunTaskPool">刷新任务池</el-button>
</div>
</div>
</template>
<el-alert
:type="metadataAlertType"
:closable="false"
show-icon
:title="metadataAlertTitle"
:description="metadataAlertDescription"
/>
<div class="home-metadata-console__stats">
<div class="home-metadata-stat">
<div class="home-metadata-stat__label">缺字段视频总量</div>
<div class="home-metadata-stat__value">{{ metadataSummary.workbench?.videos_with_any_missing_metadata || 0 }}</div>
<div class="home-metadata-stat__tip">越大越需要先看工作台再按批次派给 Codex</div>
</div>
<div class="home-metadata-stat">
<div class="home-metadata-stat__label">重采优先队列</div>
<div class="home-metadata-stat__value">{{ metadataSummary.workbench?.queue_size || 0 }}</div>
<div class="home-metadata-stat__tip">这是最适合优先处理的一批适合先做首轮修复</div>
</div>
<div class="home-metadata-stat">
<div class="home-metadata-stat__label">任务池批次数</div>
<div class="home-metadata-stat__value">{{ metadataSummary.task_pool?.batch_count || 0 }}</div>
<div class="home-metadata-stat__tip">批次越清晰后续技术和 Codex 的协作越顺</div>
</div>
</div>
<div class="home-metadata-console__hint">
<strong>当前建议</strong>{{ metadataSummary.recommended_action || "先刷新状态,再看工作台和任务池。" }}
<br />
<strong>首条提示</strong>{{ metadataFirstHint }}
</div>
<div class="home-metadata-console__links">
<el-button text type="primary" @click="openMetadataRoute('/video/metadata/missing/task-pool/status')">打开状态总览</el-button>
<el-button text type="primary" @click="openMetadataRoute('/video/metadata/missing/task-pool/ops')">打开运维总览</el-button>
<el-button text type="primary" @click="openMetadataRoute('/video/metadata/missing/task-pool')">打开任务池</el-button>
<el-button text type="primary" @click="openMetadataRoute('/video/metadata/missing/workbench')">打开工作台</el-button>
</div>
</el-card>
<div class="home-bootstrap-actions"> <div class="home-bootstrap-actions">
<el-button type="primary" @click="openBootstrapCenter('overview')">打开 Bootstrap 总控台</el-button> <el-button type="primary" @click="openBootstrapCenter('overview')">打开 Bootstrap 总控台</el-button>
<el-button type="warning" plain @click="openBootstrapCenter({ tab: 'ops', opsFocus: { panel: 'oncall', stage: 'all', clearHostFocus: true, hostConsolePanel: 'oncall' } })">值班待办</el-button> <el-button type="warning" plain @click="openBootstrapCenter({ tab: 'ops', opsFocus: { panel: 'oncall', stage: 'all', clearHostFocus: true, hostConsolePanel: 'oncall' } })">值班待办</el-button>
@@ -13,14 +68,25 @@
</template> </template>
<script setup lang="ts" name="home"> <script setup lang="ts" name="home">
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import BootstrapOpsOverview from "@/views/site/bootstrapOpsOverview.vue"; import BootstrapOpsOverview from "@/views/site/bootstrapOpsOverview.vue";
import BootstrapEnvOverview from "@/views/site/bootstrapEnvOverview.vue"; import BootstrapEnvOverview from "@/views/site/bootstrapEnvOverview.vue";
import BootstrapAdminCenter from "@/views/site/bootstrapAdminCenter.vue"; import BootstrapAdminCenter from "@/views/site/bootstrapAdminCenter.vue";
import { ref } from "vue"; import {
getMetadataMissingTaskPoolStatus,
runMetadataMissingTaskPool,
runMetadataMissingWorkbench
} from "@/api/modules/video/list";
import type { VideoList } from "@/api/interface/video/list";
const router = useRouter(); const router = useRouter();
const bootstrapAdminCenterRef = ref<InstanceType<typeof BootstrapAdminCenter> | null>(null); const bootstrapAdminCenterRef = ref<InstanceType<typeof BootstrapAdminCenter> | null>(null);
const metadataLoading = ref(false);
const workbenchRunning = ref(false);
const taskPoolRunning = ref(false);
const metadataSummary = ref<VideoList.MetadataMissingTaskPoolStatusData>({});
type BootstrapFocusMode = "all" | "task" | "oncall" | "pipeline" | "import" | "release"; type BootstrapFocusMode = "all" | "task" | "oncall" | "pipeline" | "import" | "release";
type BootstrapFocusPayload = BootstrapFocusMode | BootstrapFocusMode[]; type BootstrapFocusPayload = BootstrapFocusMode | BootstrapFocusMode[];
type BootstrapComparePreset = type BootstrapComparePreset =
@@ -77,6 +143,69 @@ const openBootstrapCenter = (payload: BootstrapAdminOpenPayload = "overview") =>
bootstrapAdminCenterRef.value?.openDialog(payload); bootstrapAdminCenterRef.value?.openDialog(payload);
}; };
const loadMetadataConsole = async () => {
metadataLoading.value = true;
try {
const response = await getMetadataMissingTaskPoolStatus();
metadataSummary.value = response.data || {};
} finally {
metadataLoading.value = false;
}
};
const handleRunWorkbench = async () => {
workbenchRunning.value = true;
try {
const response = await runMetadataMissingWorkbench();
ElMessage.success(response.msg || "工作台刷新已触发");
await loadMetadataConsole();
} finally {
workbenchRunning.value = false;
}
};
const handleRunTaskPool = async () => {
taskPoolRunning.value = true;
try {
const response = await runMetadataMissingTaskPool();
ElMessage.success(response.msg || "任务池刷新已触发");
await loadMetadataConsole();
} finally {
taskPoolRunning.value = false;
}
};
const openMetadataRoute = (path: string) => {
router.push(path);
};
const metadataFirstHint = computed(() => metadataSummary.value.operator_hint?.[0] || "当前还没有额外提示,建议先看状态总览。");
const metadataAlertType = computed(() => {
const status = metadataSummary.value.plan_task?.status_label || "missing";
if (status === "enabled") return "success";
if (status === "disabled") return "warning";
if (status === "db_blocked") return "error";
return "info";
});
const metadataAlertTitle = computed(() => {
const status = metadataSummary.value.plan_task?.status_label || "missing";
if (status === "enabled") return "视频缺字段计划任务已启用";
if (status === "disabled") return "视频缺字段计划任务已存在但未启用";
if (status === "db_blocked") return "当前环境暂时无法读取视频缺字段计划任务";
return "视频缺字段计划任务尚未补齐";
});
const metadataAlertDescription = computed(() => {
const status = metadataSummary.value.plan_task?.status_label || "missing";
const action = metadataSummary.value.recommended_action || "先刷新状态,再决定下一步。";
if (status === "enabled") return `当前工作台和任务池已经可持续刷新。${action}`;
if (status === "disabled") return `当前后台已经有这条计划任务,但还没有启用。${action}`;
if (status === "db_blocked") return `这通常说明当前后台环境没读到完整数据库上下文。${action}`;
return `当前还没补齐这条计划任务,建议先补齐再观察。${action}`;
});
const goSitePage = (payload: BootstrapDialogPayload = "all") => { const goSitePage = (payload: BootstrapDialogPayload = "all") => {
const normalizedFocus = typeof payload === "string" || Array.isArray(payload) ? payload : payload.focusMode ?? "all"; const normalizedFocus = typeof payload === "string" || Array.isArray(payload) ? payload : payload.focusMode ?? "all";
const comparePreset = typeof payload === "string" || Array.isArray(payload) ? undefined : payload.comparePreset; const comparePreset = typeof payload === "string" || Array.isArray(payload) ? undefined : payload.comparePreset;
@@ -93,6 +222,10 @@ const goSitePage = (payload: BootstrapDialogPayload = "all") => {
} }
}); });
}; };
onMounted(() => {
loadMetadataConsole();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -55,15 +55,14 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoAizhanKeywordAttentionQueue } from "@/api/modules/site/list"; import { getDomainExternalSeoAizhanKeywordAttentionQueue } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const queue = ref<SiteList.DomainExternalSeoAizhanKeywordAttentionQueueData>({}); const queue = ref<SiteList.DomainExternalSeoAizhanKeywordAttentionQueueData>({});
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadQueue = async () => { const loadQueue = async () => {

View File

@@ -69,10 +69,10 @@ import { computed, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoAizhanKeywordProbeSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoAizhanKeywordProbeSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
import DomainExternalSeoAizhanKeywordAttentionDialog from "./domainExternalSeoAizhanKeywordAttentionDialog.vue"; import DomainExternalSeoAizhanKeywordAttentionDialog from "./domainExternalSeoAizhanKeywordAttentionDialog.vue";
import DomainExternalSeoAizhanKeywordTrendDialog from "./domainExternalSeoAizhanKeywordTrendDialog.vue"; import DomainExternalSeoAizhanKeywordTrendDialog from "./domainExternalSeoAizhanKeywordTrendDialog.vue";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoAizhanKeywordProbeData>({}); const summary = ref<SiteList.DomainExternalSeoAizhanKeywordProbeData>({});
@@ -80,8 +80,7 @@ const attentionDialogRef = ref();
const trendDialogRef = ref(); const trendDialogRef = ref();
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const alertTitle = computed(() => { const alertTitle = computed(() => {

View File

@@ -59,8 +59,8 @@ import { computed, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoAizhanKeywordTrendSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoAizhanKeywordTrendSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoAizhanKeywordTrendData>({}); const summary = ref<SiteList.DomainExternalSeoAizhanKeywordTrendData>({});
@@ -74,8 +74,7 @@ const alertType = computed(() => {
}); });
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadSummary = async () => { const loadSummary = async () => {

View File

@@ -65,15 +65,14 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoBaiduFeedbackSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoBaiduFeedbackSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoBaiduFeedbackSummaryData>({}); const summary = ref<SiteList.DomainExternalSeoBaiduFeedbackSummaryData>({});
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadSummary = async () => { const loadSummary = async () => {

View File

@@ -61,6 +61,7 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoBaiduProviderPlan } from "@/api/modules/site/list"; import { getDomainExternalSeoBaiduProviderPlan } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
import DomainExternalSeoBaiduPushDialog from "./domainExternalSeoBaiduPushDialog.vue"; import DomainExternalSeoBaiduPushDialog from "./domainExternalSeoBaiduPushDialog.vue";
import DomainExternalSeoBaiduFeedbackDialog from "./domainExternalSeoBaiduFeedbackDialog.vue"; import DomainExternalSeoBaiduFeedbackDialog from "./domainExternalSeoBaiduFeedbackDialog.vue";
import DomainExternalSeoSiteQueryDialog from "./domainExternalSeoSiteQueryDialog.vue"; import DomainExternalSeoSiteQueryDialog from "./domainExternalSeoSiteQueryDialog.vue";
@@ -68,7 +69,6 @@ import DomainExternalSeoBaiduPushTaskDialog from "./domainExternalSeoBaiduPushTa
import DomainExternalSeoBaiduPushAttentionDialog from "./domainExternalSeoBaiduPushAttentionDialog.vue"; import DomainExternalSeoBaiduPushAttentionDialog from "./domainExternalSeoBaiduPushAttentionDialog.vue";
import DomainExternalSeoBaiduPushTrendDialog from "./domainExternalSeoBaiduPushTrendDialog.vue"; import DomainExternalSeoBaiduPushTrendDialog from "./domainExternalSeoBaiduPushTrendDialog.vue";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoBaiduProviderPlanData>({}); const summary = ref<SiteList.DomainExternalSeoBaiduProviderPlanData>({});
@@ -80,8 +80,7 @@ const attentionDialogRef = ref();
const trendDialogRef = ref(); const trendDialogRef = ref();
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const openPushSummaryDialog = () => { const openPushSummaryDialog = () => {

View File

@@ -68,15 +68,14 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoBaiduPushAttentionQueue } from "@/api/modules/site/list"; import { getDomainExternalSeoBaiduPushAttentionQueue } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const queue = ref<SiteList.DomainExternalSeoBaiduPushAttentionQueueData>({}); const queue = ref<SiteList.DomainExternalSeoBaiduPushAttentionQueueData>({});
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadQueue = async () => { const loadQueue = async () => {

View File

@@ -70,11 +70,11 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoBaiduPushSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoBaiduPushSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
import DomainExternalSeoBaiduPushTaskDialog from "./domainExternalSeoBaiduPushTaskDialog.vue"; import DomainExternalSeoBaiduPushTaskDialog from "./domainExternalSeoBaiduPushTaskDialog.vue";
import DomainExternalSeoBaiduPushAttentionDialog from "./domainExternalSeoBaiduPushAttentionDialog.vue"; import DomainExternalSeoBaiduPushAttentionDialog from "./domainExternalSeoBaiduPushAttentionDialog.vue";
import DomainExternalSeoBaiduPushTrendDialog from "./domainExternalSeoBaiduPushTrendDialog.vue"; import DomainExternalSeoBaiduPushTrendDialog from "./domainExternalSeoBaiduPushTrendDialog.vue";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoBaiduPushSummaryData>({}); const summary = ref<SiteList.DomainExternalSeoBaiduPushSummaryData>({});
@@ -83,8 +83,7 @@ const attentionDialogRef = ref();
const trendDialogRef = ref(); const trendDialogRef = ref();
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadSummary = async () => { const loadSummary = async () => {

View File

@@ -66,8 +66,8 @@ import { computed, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoBaiduPushTrendSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoBaiduPushTrendSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoBaiduPushTrendData>({}); const summary = ref<SiteList.DomainExternalSeoBaiduPushTrendData>({});
@@ -80,8 +80,7 @@ const alertType = computed(() => {
}); });
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadSummary = async () => { const loadSummary = async () => {

View File

@@ -43,16 +43,15 @@ import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoFailedQueue } from "@/api/modules/site/list"; import { getDomainExternalSeoFailedQueue } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const rows = ref<SiteList.DomainExternalSeoFailedQueueItem[]>([]); const rows = ref<SiteList.DomainExternalSeoFailedQueueItem[]>([]);
const queue = ref<SiteList.DomainExternalSeoFailedQueueData>({}); const queue = ref<SiteList.DomainExternalSeoFailedQueueData>({});
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const showCommand = async (title: string, command?: string) => { const showCommand = async (title: string, command?: string) => {

View File

@@ -39,17 +39,14 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoManualImportRuns } from "@/api/modules/site/list"; import { getDomainExternalSeoManualImportRuns } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const rows = ref<SiteList.DomainExternalSeoManualImportRunItem[]>([]); const rows = ref<SiteList.DomainExternalSeoManualImportRunItem[]>([]);
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) { void openAdminArtifact(path);
return;
}
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadRuns = async () => { const loadRuns = async () => {

View File

@@ -370,6 +370,7 @@
import { ref } from "vue"; import { ref } from "vue";
import { ElMessage, UploadRawFile } from "element-plus"; import { ElMessage, UploadRawFile } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
import { import {
getDomainExternalSeoManualImportTemplate, getDomainExternalSeoManualImportTemplate,
getDomainExternalSeoCnOverview, getDomainExternalSeoCnOverview,
@@ -606,7 +607,7 @@ const downloadManualTemplate = async () => {
ElMessage.warning("模板生成成功但没有可打开的文件路径"); ElMessage.warning("模板生成成功但没有可打开的文件路径");
return; return;
} }
window.open(`${import.meta.env.VITE_API_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener"); await openAdminArtifact(path);
} catch (_error) { } catch (_error) {
ElMessage.error("生成站外手工导入模板失败"); ElMessage.error("生成站外手工导入模板失败");
} }
@@ -620,7 +621,7 @@ const downloadSearchConsolePropertyMapTemplate = async () => {
ElMessage.warning("映射模板生成成功但没有可打开的文件路径"); ElMessage.warning("映射模板生成成功但没有可打开的文件路径");
return; return;
} }
window.open(`${import.meta.env.VITE_API_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener"); await openAdminArtifact(path);
} catch (_error) { } catch (_error) {
ElMessage.error("生成 Search Console 映射模板失败"); ElMessage.error("生成 Search Console 映射模板失败");
} }
@@ -663,7 +664,7 @@ const uploadManualReport = async (param: any) => {
ElMessage.success(`导入完成:共 ${res.data.rows_total || 0} 行,写入 ${res.data.imported_count || 0} 行`); ElMessage.success(`导入完成:共 ${res.data.rows_total || 0} 行,写入 ${res.data.imported_count || 0} 行`);
const summaryHtmlPath = res.data.summary_html_path; const summaryHtmlPath = res.data.summary_html_path;
if (summaryHtmlPath) { if (summaryHtmlPath) {
window.open(`${import.meta.env.VITE_API_URL}/${String(summaryHtmlPath).replace(/^\/+/, "")}`, "_blank", "noopener"); await openAdminArtifact(summaryHtmlPath);
} }
await loadSummary(); await loadSummary();
openRealSummaryDialog(); openRealSummaryDialog();
@@ -697,7 +698,7 @@ const uploadSearchConsolePropertyMap = async (param: any) => {
ElMessage.success(`Search Console 映射上传完成:${res.data.mapped_hosts_count || 0} 个 host`); ElMessage.success(`Search Console 映射上传完成:${res.data.mapped_hosts_count || 0} 个 host`);
const summaryHtmlPath = res.data.summary_html_path; const summaryHtmlPath = res.data.summary_html_path;
if (summaryHtmlPath) { if (summaryHtmlPath) {
window.open(`${import.meta.env.VITE_API_URL}/${String(summaryHtmlPath).replace(/^\/+/, "")}`, "_blank", "noopener"); await openAdminArtifact(summaryHtmlPath);
} }
await loadSummary(); await loadSummary();
openRealSummaryDialog(); openRealSummaryDialog();

View File

@@ -52,6 +52,7 @@ import { computed, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoRealSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoRealSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
@@ -90,8 +91,7 @@ const loadSummary = async () => {
}; };
const openSummaryFile = (path?: string) => { const openSummaryFile = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${import.meta.env.VITE_API_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const statusLabel = (status?: string) => { const statusLabel = (status?: string) => {

View File

@@ -145,6 +145,7 @@ import { computed, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoRealSummary, getDomainExternalSeoSnapshotRuns, getDomainExternalSeoSummary, getDomainExternalSeoTrendSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoRealSummary, getDomainExternalSeoSnapshotRuns, getDomainExternalSeoSummary, getDomainExternalSeoTrendSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
import DomainExternalSeoSnapshotRunDialog from "./domainExternalSeoSnapshotRunDialog.vue"; import DomainExternalSeoSnapshotRunDialog from "./domainExternalSeoSnapshotRunDialog.vue";
const visible = ref(false); const visible = ref(false);
@@ -215,8 +216,7 @@ const formatDateTime = (value?: string) => {
}; };
const openSummaryFile = (path?: string) => { const openSummaryFile = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${import.meta.env.VITE_API_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const openSnapshotRunsDialog = () => { const openSnapshotRunsDialog = () => {

View File

@@ -71,15 +71,14 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoSearchConsoleFetchSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoSearchConsoleFetchSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoSearchConsoleFetchData>({}); const summary = ref<SiteList.DomainExternalSeoSearchConsoleFetchData>({});
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadSummary = async () => { const loadSummary = async () => {

View File

@@ -66,17 +66,16 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoSearchConsoleProviderPlan } from "@/api/modules/site/list"; import { getDomainExternalSeoSearchConsoleProviderPlan } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
import DomainExternalSeoSearchConsoleFetchDialog from "./domainExternalSeoSearchConsoleFetchDialog.vue"; import DomainExternalSeoSearchConsoleFetchDialog from "./domainExternalSeoSearchConsoleFetchDialog.vue";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoSearchConsoleProviderPlanData>({}); const summary = ref<SiteList.DomainExternalSeoSearchConsoleProviderPlanData>({});
const fetchDialogRef = ref(); const fetchDialogRef = ref();
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const openFetchDialog = () => { const openFetchDialog = () => {

View File

@@ -62,15 +62,14 @@ import { computed, ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoSiteQueryProbeSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoSiteQueryProbeSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoSiteQueryProbeData>({}); const summary = ref<SiteList.DomainExternalSeoSiteQueryProbeData>({});
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const alertTitle = computed(() => { const alertTitle = computed(() => {

View File

@@ -130,16 +130,15 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoSnapshotRuns } from "@/api/modules/site/list"; import { getDomainExternalSeoSnapshotRuns } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const rows = ref<SiteList.DomainExternalSeoSnapshotRunItem[]>([]); const rows = ref<SiteList.DomainExternalSeoSnapshotRunItem[]>([]);
const summary = ref<SiteList.DomainExternalSeoSnapshotRunData | null>(null); const summary = ref<SiteList.DomainExternalSeoSnapshotRunData | null>(null);
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadRuns = async () => { const loadRuns = async () => {

View File

@@ -137,16 +137,15 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoSnapshotValidateRuns } from "@/api/modules/site/list"; import { getDomainExternalSeoSnapshotValidateRuns } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const rows = ref<SiteList.DomainExternalSeoSnapshotValidateRunItem[]>([]); const rows = ref<SiteList.DomainExternalSeoSnapshotValidateRunItem[]>([]);
const summary = ref<SiteList.DomainExternalSeoSnapshotValidateRunData | null>(null); const summary = ref<SiteList.DomainExternalSeoSnapshotValidateRunData | null>(null);
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const loadRuns = async () => { const loadRuns = async () => {

View File

@@ -81,13 +81,13 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
import DomainExternalSeoFailedQueueDialog from "./domainExternalSeoFailedQueueDialog.vue"; import DomainExternalSeoFailedQueueDialog from "./domainExternalSeoFailedQueueDialog.vue";
import DomainExternalSeoTrendDialog from "./domainExternalSeoTrendDialog.vue"; import DomainExternalSeoTrendDialog from "./domainExternalSeoTrendDialog.vue";
import DomainExternalSeoRealSummaryDialog from "./domainExternalSeoRealSummaryDialog.vue"; import DomainExternalSeoRealSummaryDialog from "./domainExternalSeoRealSummaryDialog.vue";
import DomainExternalSeoProviderDialog from "./domainExternalSeoProviderDialog.vue"; import DomainExternalSeoProviderDialog from "./domainExternalSeoProviderDialog.vue";
import DomainExternalSeoSearchConsolePlanDialog from "./domainExternalSeoSearchConsolePlanDialog.vue"; import DomainExternalSeoSearchConsolePlanDialog from "./domainExternalSeoSearchConsolePlanDialog.vue";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const rows = ref<SiteList.DomainExternalSeoHostItem[]>([]); const rows = ref<SiteList.DomainExternalSeoHostItem[]>([]);
@@ -99,10 +99,7 @@ const providerDialogRef = ref();
const searchConsolePlanDialogRef = ref(); const searchConsolePlanDialogRef = ref();
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) { void openAdminArtifact(path);
return;
}
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const trendLabel = () => { const trendLabel = () => {

View File

@@ -72,15 +72,14 @@ import { ref } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list"; import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoTrendSummary } from "@/api/modules/site/list"; import { getDomainExternalSeoTrendSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoTrendData>({}); const summary = ref<SiteList.DomainExternalSeoTrendData>({});
const openPublicPath = (path?: string) => { const openPublicPath = (path?: string) => {
if (!path) return; void openAdminArtifact(path);
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const trendLabel = () => { const trendLabel = () => {

View File

@@ -6,13 +6,13 @@
<el-button type="warning" plain @click="runForceGenerate">强制重生成</el-button> <el-button type="warning" plain @click="runForceGenerate">强制重生成</el-button>
<el-button plain :disabled="!lastJobId" @click="reopenLastTask">继续查看任务</el-button> <el-button plain :disabled="!lastJobId" @click="reopenLastTask">继续查看任务</el-button>
<el-button plain @click="loadData">刷新</el-button> <el-button plain @click="loadData">刷新</el-button>
<span class="spider-md-dialog__toolbar-note">当天若仅 Gitee 推送失败可直接再次点击生成并推送重试无需强制重生成</span> <span class="spider-md-dialog__toolbar-note">当天若仅远端推送失败可直接再次点击生成并推送重试无需强制重生成</span>
</div> </div>
<el-alert <el-alert
:title="giteeTitle" :title="remoteTitle"
:description="giteeDescription" :description="remoteDescription"
:type="summary.gitee?.configured ? 'success' : 'warning'" :type="remoteConfigured ? 'success' : 'warning'"
:closable="false" :closable="false"
show-icon show-icon
/> />
@@ -20,10 +20,15 @@
<el-descriptions :column="2" border size="small"> <el-descriptions :column="2" border size="small">
<el-descriptions-item label="本地目录">{{ summary.run_root || "-" }}</el-descriptions-item> <el-descriptions-item label="本地目录">{{ summary.run_root || "-" }}</el-descriptions-item>
<el-descriptions-item label="Gitee 根目录">{{ summary.gitee?.root || "-" }}</el-descriptions-item> <el-descriptions-item label="Gitee 根目录">{{ summary.gitee?.root || "-" }}</el-descriptions-item>
<el-descriptions-item label="GitHub 根目录">{{ summary.github?.root || "-" }}</el-descriptions-item>
<el-descriptions-item label="仓库"> <el-descriptions-item label="仓库">
{{ summary.gitee?.owner && summary.gitee?.repo ? `${summary.gitee.owner}/${summary.gitee.repo}` : "-" }} {{ summary.gitee?.owner && summary.gitee?.repo ? `${summary.gitee.owner}/${summary.gitee.repo}` : "-" }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="GitHub 仓库">
{{ summary.github?.owner && summary.github?.repo ? `${summary.github.owner}/${summary.github.repo}` : "-" }}
</el-descriptions-item>
<el-descriptions-item label="分支">{{ summary.gitee?.branch || "-" }}</el-descriptions-item> <el-descriptions-item label="分支">{{ summary.gitee?.branch || "-" }}</el-descriptions-item>
<el-descriptions-item label="GitHub 分支">{{ summary.github?.branch || "-" }}</el-descriptions-item>
<el-descriptions-item label="单文件上限">{{ summary.runtime_config?.max_links_per_file || "-" }}</el-descriptions-item> <el-descriptions-item label="单文件上限">{{ summary.runtime_config?.max_links_per_file || "-" }}</el-descriptions-item>
<el-descriptions-item label="详情/泛链"> <el-descriptions-item label="详情/泛链">
{{ summary.runtime_config?.detail_limit_per_host || 0 }} / {{ summary.runtime_config?.forge_limit_per_host || 0 }} {{ summary.runtime_config?.detail_limit_per_host || 0 }} / {{ summary.runtime_config?.forge_limit_per_host || 0 }}
@@ -35,6 +40,9 @@
<el-descriptions-item label="配置来源"> <el-descriptions-item label="配置来源">
Gitee开关 {{ summary.gitee?.sources?.enabled || "-" }} / 根目录 {{ summary.gitee?.sources?.root || "-" }} Gitee开关 {{ summary.gitee?.sources?.enabled || "-" }} / 根目录 {{ summary.gitee?.sources?.root || "-" }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="GitHub来源">
GitHub开关 {{ summary.github?.sources?.enabled || "-" }} / 根目录 {{ summary.github?.sources?.root || "-" }}
</el-descriptions-item>
</el-descriptions> </el-descriptions>
<el-alert <el-alert
@@ -48,7 +56,7 @@
<el-alert <el-alert
title="推荐配置方式蜘蛛池MD 相关配置统一走后台系统配置" title="推荐配置方式蜘蛛池MD 相关配置统一走后台系统配置"
description="可后台维护SPIDER_MD_MAX_LINKS_PER_FILE、各类链接上限、Gitee 开关/owner/repo/branch/root/tokentoken 在运行摘要中会自动打码,不直接回显。" description="可后台维护SPIDER_MD_MAX_LINKS_PER_FILE、各类链接上限、Gitee / GitHub 开关ownerrepobranchroottokentoken 在运行摘要中会自动打码,不直接回显。"
type="info" type="info"
:closable="false" :closable="false"
show-icon show-icon
@@ -60,24 +68,52 @@
<template #default="scope"> <template #default="scope">
<div class="spider-md-dialog__expand"> <div class="spider-md-dialog__expand">
<el-empty <el-empty
v-if="!(scope.row.gitee_files || []).length" v-if="!hasRemoteFiles(scope.row)"
:description="scope.row.gitee_push_status === 'failed' || scope.row.gitee_push_status === 'partial_failed' :description="emptyRemoteDescription(scope.row)"
? `Gitee 推送失败:${scope.row.gitee_push_error || '请查看任务日志'}`
: '当前这轮还没有可复制的 Gitee 链接'"
/> />
<template v-else> <template v-else>
<div v-if="remoteMdFiles(scope.row, 'gitee').length" class="spider-md-dialog__provider-block">
<div class="spider-md-dialog__expand-toolbar"> <div class="spider-md-dialog__expand-toolbar">
<el-button size="small" type="primary" plain @click="copyAllHtmlLinks(scope.row.gitee_files || [])">批量复制页面链接</el-button> <div class="spider-md-dialog__provider-title">Gitee 链接</div>
<el-button
size="small"
type="primary"
plain
@click="copyAllMdLinks(remoteMdFiles(scope.row, 'gitee'), 'gitee')"
>
一键复制 Gitee MD
</el-button>
</div> </div>
<el-table :data="scope.row.gitee_files || []" border size="small"> <el-table :data="remoteMdFiles(scope.row, 'gitee')" border size="small">
<el-table-column prop="file_name" label="文件" min-width="180" /> <el-table-column prop="file_name" label="文件" min-width="180" />
<el-table-column prop="html_url" label="Gitee 链接" min-width="420" show-overflow-tooltip /> <el-table-column label="MD链接" min-width="420" show-overflow-tooltip>
<el-table-column label="操作" width="140" fixed="right"> <template #default="{ row }">
<template #default="fileScope"> {{ getRemoteLink(row, "gitee") }}
<el-button link type="primary" @click="copyText(fileScope.row.html_url)">复制页面链接</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</div>
<div v-if="remoteMdFiles(scope.row, 'github').length" class="spider-md-dialog__provider-block">
<div class="spider-md-dialog__expand-toolbar">
<div class="spider-md-dialog__provider-title">GitHub 链接</div>
<el-button
size="small"
type="success"
plain
@click="copyAllMdLinks(remoteMdFiles(scope.row, 'github'), 'github')"
>
一键复制 GitHub MD
</el-button>
</div>
<el-table :data="remoteMdFiles(scope.row, 'github')" border size="small">
<el-table-column prop="file_name" label="文件" min-width="180" />
<el-table-column label="MD链接" min-width="420" show-overflow-tooltip>
<template #default="{ row }">
{{ getRemoteLink(row, "github") }}
</template>
</el-table-column>
</el-table>
</div>
</template> </template>
</div> </div>
</template> </template>
@@ -97,21 +133,20 @@
聚合 {{ scope.row.aggregate_count || 0 }} / 详情 {{ scope.row.detail_count || 0 }} / 泛链 {{ scope.row.forge_count || 0 }} 聚合 {{ scope.row.aggregate_count || 0 }} / 详情 {{ scope.row.detail_count || 0 }} / 泛链 {{ scope.row.forge_count || 0 }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="推送状态" min-width="110"> <el-table-column label="推送状态" min-width="180">
<template #default="scope"> <template #default="scope">
<el-tag <el-tag
size="small" size="small"
:type="scope.row.gitee_push_status === 'success' ? 'success' : (scope.row.gitee_push_status === 'failed' || scope.row.gitee_push_status === 'partial_failed' ? 'danger' : 'info')" :type="remoteStatus(scope.row, 'gitee') === 'success' ? 'success' : (remoteStatus(scope.row, 'gitee') === 'failed' || remoteStatus(scope.row, 'gitee') === 'partial_failed' ? 'danger' : 'info')"
> >
{{ Gitee{{ statusLabel(remoteStatus(scope.row, "gitee")) }}
scope.row.gitee_push_status === 'success' </el-tag>
? "已推送" <el-tag
: scope.row.gitee_push_status === 'failed' size="small"
? "推送失败" style="margin-left:8px;"
: scope.row.gitee_push_status === 'partial_failed' :type="remoteStatus(scope.row, 'github') === 'success' ? 'success' : (remoteStatus(scope.row, 'github') === 'failed' || remoteStatus(scope.row, 'github') === 'partial_failed' ? 'danger' : 'info')"
? "部分失败" >
: "仅本地" GitHub{{ statusLabel(remoteStatus(scope.row, "github")) }}
}}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
@@ -120,21 +155,34 @@
<el-tag size="small" type="success">本地成功</el-tag> <el-tag size="small" type="success">本地成功</el-tag>
<el-tag <el-tag
size="small" size="small"
:type="scope.row.gitee_push_status === 'success' ? 'success' : (scope.row.gitee_push_status === 'failed' || scope.row.gitee_push_status === 'partial_failed' ? 'danger' : 'info')" :type="remoteOverallStatus(scope.row) === 'success' ? 'success' : (remoteOverallStatus(scope.row) === 'failed' || remoteOverallStatus(scope.row) === 'partial_failed' ? 'danger' : 'info')"
style="margin-left:8px;" style="margin-left:8px;"
> >
{{ {{
scope.row.gitee_push_status === 'success' remoteOverallStatus(scope.row) === 'success'
? "远端成功" ? "远端成功"
: scope.row.gitee_push_status === 'failed' : remoteOverallStatus(scope.row) === 'failed'
? "远端失败" ? "远端失败"
: scope.row.gitee_push_status === 'partial_failed' : remoteOverallStatus(scope.row) === 'partial_failed'
? "远端部分失败" ? "远端部分失败"
: "未推送远端" : "未推送远端"
}} }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="scope">
<el-button
size="small"
type="primary"
plain
:disabled="!hasRemoteFiles(scope.row)"
@click="copyPreferredMdLinks(scope.row)"
>
一键复制MD
</el-button>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
</el-dialog> </el-dialog>
@@ -165,20 +213,34 @@ const formatDateTime = (value?: string) => {
return `${year}-${month}-${day} ${hour}:${minute}:${second}`; return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}; };
const giteeTitle = computed(() => { type RemoteProvider = "gitee" | "github";
if (summary.value.gitee?.configured) {
return "当前已配置 Gitee 自动推送,生成后会自动返回可复制链接"; const remoteConfigured = computed(() => {
} return Boolean(summary.value.gitee?.configured || summary.value.github?.configured);
if (summary.value.gitee?.enabled) {
return "Gitee 已启用但未配完整,当前只能保留本地文件";
}
return "当前未启用 Gitee 推送,生成后仅保留本地文件";
}); });
const giteeDescription = computed(() => { const remoteTitle = computed(() => {
if (summary.value.gitee?.configured && summary.value.github?.configured) {
return "当前已同时配置 Gitee 和 GitHub 自动推送,生成后会分别返回可复制链接";
}
if (summary.value.gitee?.configured) {
return "当前已配置 Gitee 自动推送,生成后会返回 Gitee 可复制链接";
}
if (summary.value.github?.configured) {
return "当前已配置 GitHub 自动推送,生成后会返回 GitHub 可复制链接";
}
if (summary.value.gitee?.enabled || summary.value.github?.enabled) {
return "远端推送已启用但未配完整,当前可能只保留本地文件";
}
return "当前未启用远端推送,生成后仅保留本地文件";
});
const remoteDescription = computed(() => {
const gitee = summary.value.gitee || {}; const gitee = summary.value.gitee || {};
const repo = gitee.owner && gitee.repo ? `${gitee.owner}/${gitee.repo}` : "未配置仓库"; const github = summary.value.github || {};
return `仓库:${repo};分支:${gitee.branch || "-"};根目录:${gitee.root || "-"}`; const giteeRepo = gitee.owner && gitee.repo ? `${gitee.owner}/${gitee.repo}` : "未配置";
const githubRepo = github.owner && github.repo ? `${github.owner}/${github.repo}` : "未配置";
return `Gitee${giteeRepo} / ${gitee.branch || "-"} / ${gitee.root || "-"}GitHub${githubRepo} / ${github.branch || "-"} / ${github.root || "-"}`;
}); });
const latestRunTitle = computed(() => { const latestRunTitle = computed(() => {
@@ -194,19 +256,76 @@ const latestRunDescription = computed(() => {
const fileCount = Number(row.file_count || 0); const fileCount = Number(row.file_count || 0);
const totalLinks = Number(row.total_links || 0); const totalLinks = Number(row.total_links || 0);
const runRoot = String(row.run_root || "").trim() || "-"; const runRoot = String(row.run_root || "").trim() || "-";
const pushStatus = String(row.gitee_push_status || "").trim(); const githubStatus = String(row.github_push_status || "").trim();
const pushError = String(row.gitee_push_error || "").trim(); const githubError = String(row.github_push_error || "").trim();
let pushed = "当前仅保留本地"; const giteeStatus = String(row.gitee_push_status || "").trim();
if (pushStatus === "success") { const giteeError = String(row.gitee_push_error || "").trim();
pushed = "已推送到 Gitee"; const statusParts: string[] = [];
} else if (pushStatus === "failed") { const errorParts: string[] = [];
pushed = "本地生成成功Gitee 推送失败";
} else if (pushStatus === "partial_failed") { statusParts.push(`Gitee ${statusLabel(giteeStatus)}`);
pushed = "本地生成成功Gitee 部分推送后失败"; statusParts.push(`GitHub ${statusLabel(githubStatus)}`);
} if (giteeError) errorParts.push(`Gitee${giteeError}`);
return `目录:${runRoot};文件数:${fileCount};链接数:${totalLinks};状态:${pushed}${pushError ? `;错误${pushError}` : ""}`; if (githubError) errorParts.push(`GitHub${githubError}`);
return `目录:${runRoot};文件数:${fileCount};链接数:${totalLinks};状态:${statusParts.join(" / ")}${errorParts.length ? `;错误:${errorParts.join("")}` : ""}`;
}); });
const statusLabel = (status?: string) => {
if (status === "success") return "已推送";
if (status === "failed") return "失败";
if (status === "partial_failed") return "部分失败";
return "仅本地";
};
const remoteFiles = (row: SiteList.DomainSpiderMdRunSummaryItem, provider: RemoteProvider) => {
return provider === "github" ? (row.github_files || []) : (row.gitee_files || []);
};
const remoteMdFiles = (row: SiteList.DomainSpiderMdRunSummaryItem, provider: RemoteProvider) => {
return remoteFiles(row, provider).filter(item => String(item?.file_name || "").toLowerCase().endsWith(".md"));
};
const hasRemoteFiles = (row: SiteList.DomainSpiderMdRunSummaryItem) => {
return Boolean(remoteMdFiles(row, "gitee").length || remoteMdFiles(row, "github").length);
};
const remoteStatus = (row: SiteList.DomainSpiderMdRunSummaryItem, provider: RemoteProvider) => {
return String(provider === "github" ? row.github_push_status : row.gitee_push_status || "").trim();
};
const remoteOverallStatus = (row: SiteList.DomainSpiderMdRunSummaryItem) => {
const gitee = remoteStatus(row, "gitee");
const github = remoteStatus(row, "github");
const list = [gitee, github].filter(Boolean);
if (!list.length || list.every(item => !item || item === "local")) return "";
if (list.includes("failed")) return "failed";
if (list.includes("partial_failed")) return "partial_failed";
if (list.includes("success")) return "success";
return "";
};
const getRemoteLink = (item: { download_url?: string; html_url?: string }, provider: RemoteProvider) => {
if (provider === "github") {
return String(item?.html_url || item?.download_url || "").trim();
}
return String(item?.download_url || item?.html_url || "").trim();
};
const emptyRemoteDescription = (row: SiteList.DomainSpiderMdRunSummaryItem) => {
const giteeStatus = String(row.gitee_push_status || "").trim();
const giteeError = String(row.gitee_push_error || "").trim();
const githubStatus = String(row.github_push_status || "").trim();
const githubError = String(row.github_push_error || "").trim();
if (giteeStatus === "failed" || giteeStatus === "partial_failed") {
return `Gitee 推送失败:${giteeError || "请查看任务日志"}`;
}
if (githubStatus === "failed" || githubStatus === "partial_failed") {
return `GitHub 推送失败:${githubError || "请查看任务日志"}`;
}
return "当前这轮还没有可复制的远端链接";
};
const loadData = async () => { const loadData = async () => {
const res = await getDomainSpiderMdSummary({ limit: 10 }); const res = await getDomainSpiderMdSummary({ limit: 10 });
summary.value = res.data || {}; summary.value = res.data || {};
@@ -227,15 +346,31 @@ const copyText = async (text?: string) => {
} }
}; };
const copyAllHtmlLinks = async (items: Array<{ html_url?: string }>) => { const copyAllMdLinks = async (items: Array<{ download_url?: string; html_url?: string }>, provider: RemoteProvider) => {
const lines = (items || []).map(item => String(item?.html_url || "").trim()).filter(Boolean); const lines = (items || [])
.map(item => getRemoteLink(item, provider))
.filter(Boolean);
if (!lines.length) { if (!lines.length) {
ElMessage.warning("当前没有可复制的页面链接"); ElMessage.warning("当前没有可复制的MD链接");
return; return;
} }
await copyText(lines.join("\n")); await copyText(lines.join("\n"));
}; };
const copyPreferredMdLinks = async (row: SiteList.DomainSpiderMdRunSummaryItem) => {
const githubItems = remoteMdFiles(row, "github");
if (githubItems.length) {
await copyAllMdLinks(githubItems, "github");
return;
}
const giteeItems = remoteMdFiles(row, "gitee");
if (giteeItems.length) {
await copyAllMdLinks(giteeItems, "gitee");
return;
}
ElMessage.warning("当前没有可复制的MD链接");
};
const runGenerate = async () => { const runGenerate = async () => {
const res = await runDomainSpiderMdGenerate({}); const res = await runDomainSpiderMdGenerate({});
const jobId = String(res.data?.job?.job_id || "").trim(); const jobId = String(res.data?.job?.job_id || "").trim();
@@ -315,7 +450,19 @@ defineExpose({
.spider-md-dialog__expand-toolbar { .spider-md-dialog__expand-toolbar {
display: flex; display: flex;
align-items: center;
justify-content: flex-end; justify-content: flex-end;
margin-bottom: 8px; margin-bottom: 8px;
} }
.spider-md-dialog__provider-block + .spider-md-dialog__provider-block {
margin-top: 16px;
}
.spider-md-dialog__provider-title {
margin-right: auto;
font-size: 13px;
font-weight: 600;
color: #303133;
}
</style> </style>

View File

@@ -113,6 +113,193 @@
<BootstrapEnvDialog v-if="dialogMount.bootstrapEnvDialog" ref="bootstrapEnvDialogRef" /> <BootstrapEnvDialog v-if="dialogMount.bootstrapEnvDialog" ref="bootstrapEnvDialogRef" />
<DomainSupplyDialog v-if="dialogMount.domainSupplyDialog" ref="domainSupplyDialogRef" /> <DomainSupplyDialog v-if="dialogMount.domainSupplyDialog" ref="domainSupplyDialogRef" />
<DomainImportTaskConsoleDialog ref="domainImportTaskConsoleDialogRef" /> <DomainImportTaskConsoleDialog ref="domainImportTaskConsoleDialogRef" />
<el-dialog
v-model="postImportWorkflowDialog.visible"
title="导入完成,选择下一步"
width="760px"
draggable
destroy-on-close
>
<div class="post-import-workflow" v-if="postImportWorkflowDialog.payload">
<el-alert
:title="postImportWorkflowDialog.payload.post_import_workflow?.summary || '本批域名已导入完成,请选择后续推进方式'"
:type="postImportWorkflowDialog.payload.probe_failed_count ? 'warning' : 'success'"
:closable="false"
show-icon
/>
<div class="post-import-workflow__metrics">
<el-tag type="success">新增 {{ postImportWorkflowDialog.payload.inserted_count || 0 }} </el-tag>
<el-tag type="info">探测 {{ postImportWorkflowDialog.payload.probe_processed_count || 0 }} </el-tag>
<el-tag :type="(postImportWorkflowDialog.payload.probe_failed_count || 0) > 0 ? 'danger' : 'success'">
失败 {{ postImportWorkflowDialog.payload.probe_failed_count || 0 }}
</el-tag>
</div>
<div
v-if="(postImportWorkflowDialog.payload.next_step_notice || []).length"
class="post-import-workflow__section"
>
<div class="post-import-workflow__title">下一步提醒</div>
<ul class="post-import-workflow__list">
<li v-for="(item, index) in postImportWorkflowDialog.payload.next_step_notice" :key="`notice-${index}`">
{{ item }}
</li>
</ul>
</div>
<div class="post-import-workflow__section">
<div class="post-import-workflow__grid">
<div class="post-import-workflow__card">
<div class="post-import-workflow__title">技术继续Codex</div>
<div class="post-import-workflow__desc">
{{
postImportWorkflowDialog.payload.post_import_workflow?.choices?.find(item => item.key === "codex_continue")
?.description || "复制提示词给 Codex让 Codex 接手这批新站的 SEO 收口。"
}}
</div>
<div class="post-import-workflow__hint">
{{ postImportWorkflowDialog.payload.post_import_workflow?.codex?.open_in_vscode_hint || "先打开 VS Code 工作区,再把提示词发给 Codex" }}
</div>
<div class="post-import-workflow__path" v-if="postImportWorkflowDialog.payload.post_import_workflow?.codex?.workspace_path">
工作区{{ postImportWorkflowDialog.payload.post_import_workflow?.codex?.workspace_path }}
</div>
<div class="post-import-workflow__actions">
<el-button type="primary" @click="copyPostImportCodexPrompt">复制Codex提示词</el-button>
</div>
</div>
<div class="post-import-workflow__card">
<div class="post-import-workflow__title">运营继续OpenAI</div>
<div class="post-import-workflow__desc">
{{
postImportWorkflowDialog.payload.post_import_workflow?.choices?.find(item => item.key === "openai_continue")
?.description || "先确认探测结果,再显式提交后台 AI 生成队列。"
}}
</div>
<ul
v-if="(postImportWorkflowDialog.payload.post_import_workflow?.openai?.operator_steps || []).length"
class="post-import-workflow__list"
>
<li
v-for="(item, index) in postImportWorkflowDialog.payload.post_import_workflow?.openai?.operator_steps || []"
:key="`operator-${index}`"
>
{{ item }}
</li>
</ul>
<div class="post-import-workflow__actions">
<el-button type="success" :loading="postImportWorkflowDialog.submittingOpenAi" @click="continuePostImportWithOpenAi">
OpenAI继续
</el-button>
</div>
</div>
</div>
</div>
<div class="post-import-workflow__section">
<div class="post-import-workflow__title">探测与导入产物</div>
<div class="post-import-workflow__actions">
<el-button plain :disabled="!postImportWorkflowDialog.payload.summary_html_path" @click="openPublicPath(postImportWorkflowDialog.payload.summary_html_path)">
打开探测HTML摘要
</el-button>
<el-button plain :disabled="!postImportWorkflowDialog.payload.summary_json_path" @click="openPublicPath(postImportWorkflowDialog.payload.summary_json_path)">
打开探测JSON摘要
</el-button>
<el-button plain :disabled="!postImportWorkflowDialog.payload.uploaded_excel_path" @click="openPublicPath(postImportWorkflowDialog.payload.uploaded_excel_path)">
打开原始Excel
</el-button>
</div>
</div>
<div
v-if="(postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.items || []).length"
class="post-import-workflow__section"
>
<div class="post-import-workflow__title">自动产物审计</div>
<div class="post-import-workflow__metrics">
<el-tag type="success">
state {{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.state_ready_count || 0 }}/{{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.total || 0 }}
</el-tag>
<el-tag type="success">
manifest {{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.manifest_ready_count || 0 }}/{{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.total || 0 }}
</el-tag>
<el-tag type="success">
positioning {{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.positioning_ready_count || 0 }}/{{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.total || 0 }}
</el-tag>
<el-tag type="success">
copy {{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.published_copy_ready_count || 0 }}/{{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.total || 0 }}
</el-tag>
<el-tag :type="(postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.sitemap_generated_count || 0) > 0 ? 'success' : 'warning'">
sitemap {{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.sitemap_generated_count || 0 }}/{{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.total || 0 }}
</el-tag>
<el-tag type="warning">
ai_pending {{ postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.summary?.ai_pending_count || 0 }}
</el-tag>
</div>
<el-table
:data="postImportWorkflowDialog.payload.post_import_workflow?.artifact_audit?.items || []"
size="small"
border
>
<el-table-column prop="host" label="域名" min-width="180" />
<el-table-column prop="site_name" label="站点名" min-width="140" />
<el-table-column label="state" width="82">
<template #default="{ row }">
<el-tag :type="row.state_exists ? 'success' : 'danger'">{{ row.state_exists ? "已生成" : "缺失" }}</el-tag>
</template>
</el-table-column>
<el-table-column label="manifest" width="90">
<template #default="{ row }">
<el-tag :type="row.manifest_exists ? 'success' : 'danger'">{{ row.manifest_exists ? "已生成" : "缺失" }}</el-tag>
</template>
</el-table-column>
<el-table-column label="定位资源" width="96">
<template #default="{ row }">
<el-tag :type="row.site_positioning_exists ? 'success' : 'danger'">{{ row.site_positioning_exists ? "已生成" : "缺失" }}</el-tag>
</template>
</el-table-column>
<el-table-column label="starter copy" width="96">
<template #default="{ row }">
<el-tag :type="row.published_copy_exists ? 'success' : 'warning'">{{ row.published_copy_exists ? "已落盘" : "未落盘" }}</el-tag>
</template>
</el-table-column>
<el-table-column label="AI状态" min-width="100">
<template #default="{ row }">
<el-tag :type="row.ai_status === 'ai_pending' ? 'warning' : 'info'">{{ row.ai_status || "unknown" }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Sitemap" min-width="120">
<template #default="{ row }">
<el-tag :type="row.sitemap_status === 'generated' ? 'success' : 'warning'">
{{ row.sitemap_status === "generated" ? "已生成" : "已入队待生成" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="说明" min-width="260">
<template #default="{ row }">
<div class="post-import-workflow__audit-notes">
{{ (row.notes || []).join("") }}
</div>
</template>
</el-table-column>
</el-table>
</div>
<div
v-if="postImportWorkflowDialog.payload.codex_continue_prompt"
class="post-import-workflow__section"
>
<div class="post-import-workflow__title">Codex提示词预览</div>
<el-input
:model-value="postImportWorkflowDialog.payload.codex_continue_prompt"
type="textarea"
:rows="10"
readonly
/>
</div>
</div>
<template #footer>
<el-button @click="postImportWorkflowDialog.visible = false">稍后再说</el-button>
<el-button type="primary" plain @click="copyPostImportCodexPrompt">复制Codex提示词</el-button>
<el-button type="success" :loading="postImportWorkflowDialog.submittingOpenAi" @click="continuePostImportWithOpenAi">
OpenAI继续
</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@@ -280,6 +467,17 @@ const domainSpiderMdDialogRef = ref();
const domainSeoCopyWorkbenchDialogRef = ref(); const domainSeoCopyWorkbenchDialogRef = ref();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const clipboard = navigator.clipboard;
const postImportWorkflowDialog = reactive<{
visible: boolean;
payload: SiteList.DomainUploadSummary | null;
submittingOpenAi: boolean;
}>({
visible: false,
payload: null,
submittingOpenAi: false
});
const batchAdd = () => { const batchAdd = () => {
void ensureDialogMounted("importDialog", () => { void ensureDialogMounted("importDialog", () => {
const params = { const params = {
@@ -444,6 +642,73 @@ const openPublicPath = (path?: string) => {
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener"); window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
}; };
const openPostImportWorkflowDialog = (payload: SiteList.DomainUploadSummary) => {
postImportWorkflowDialog.payload = payload;
postImportWorkflowDialog.visible = true;
};
const copyPostImportCodexPrompt = async () => {
const prompt = String(postImportWorkflowDialog.payload?.codex_continue_prompt ?? "").trim();
if (prompt === "") {
ElMessage.warning("当前没有可复制的 Codex 提示词");
return;
}
try {
await clipboard.writeText(prompt);
ElMessage.success("Codex 提示词已复制");
} catch {
ElMessage.error("复制失败,请手动复制");
}
};
const continuePostImportWithOpenAi = async () => {
const domainIds = (postImportWorkflowDialog.payload?.post_import_workflow?.openai?.domain_ids || [])
.map(item => Number(item || 0))
.filter(item => item > 0);
if (domainIds.length === 0) {
ElMessage.warning("当前批次没有可提交的站点 ID");
return;
}
try {
await ElMessageBox.confirm(
`确认把这批新导入站点提交到 AI 生成队列吗?\n共 ${domainIds.length} 个站点`,
"OpenAI继续",
{
confirmButtonText: "提交AI生成",
cancelButtonText: "取消",
type: "warning",
center: true,
draggable: true
}
);
} catch {
return;
}
postImportWorkflowDialog.submittingOpenAi = true;
try {
const res = await submitDomainSeoCopyAiGenerate({
d_ids: domainIds,
wait_for_ai_before_publish: 1
});
const intQueued = Number(res.data?.queued_count ?? domainIds.length);
const strJobId = String(res.data?.job?.job_id ?? "").trim();
ElMessage.success(`已提交 ${intQueued} 个站点进入 AI 生成队列`);
postImportWorkflowDialog.visible = false;
if (strJobId !== "") {
domainImportTaskConsoleDialogRef.value?.open("AI文案生成任务", strJobId, () => {
proTable.value?.getTableList();
});
return;
}
proTable.value?.getTableList();
} finally {
postImportWorkflowDialog.submittingOpenAi = false;
}
};
const showDomainImportSummary = async (payload: SiteList.DomainUploadSummary) => { const showDomainImportSummary = async (payload: SiteList.DomainUploadSummary) => {
const inserted = payload?.inserted_count ?? 0; const inserted = payload?.inserted_count ?? 0;
const probeProcessed = payload?.probe_processed_count ?? 0; const probeProcessed = payload?.probe_processed_count ?? 0;
@@ -455,8 +720,9 @@ const showDomainImportSummary = async (payload: SiteList.DomainUploadSummary) =>
const runRoot = payload?.run_root ?? ""; const runRoot = payload?.run_root ?? "";
const arrLines = [ const arrLines = [
`<p>新增站点:<strong>${inserted}</strong></p>`, `<p>本次新增站点:<strong>${inserted}</strong></p>`,
probeProcessed > 0 ? `<p>自动探测:<strong>${probeProcessed}</strong>,通过 <strong>${probePassed}</strong>,失败 <strong>${probeFailed}</strong></p>` : "", probeProcessed > 0 ? `<p>自动探测:<strong>${probeProcessed}</strong>,通过 <strong>${probePassed}</strong>,失败 <strong>${probeFailed}</strong></p>` : "",
`<p>建议先看探测摘要,确认页面链路正常后,再决定走 <strong>OpenAI继续</strong> 还是 <strong>Codex继续</strong>。</p>`,
runRoot ? `<p>运行目录:<code>${runRoot}</code></p>` : "", runRoot ? `<p>运行目录:<code>${runRoot}</code></p>` : "",
uploadedExcelPath ? `<p><a href="${BASE_URL}/${String(uploadedExcelPath).replace(/^\/+/, "")}" target="_blank" rel="noopener">打开上传原始 Excel</a></p>` : "", uploadedExcelPath ? `<p><a href="${BASE_URL}/${String(uploadedExcelPath).replace(/^\/+/, "")}" target="_blank" rel="noopener">打开上传原始 Excel</a></p>` : "",
summaryHtmlPath ? `<p><a href="${BASE_URL}/${String(summaryHtmlPath).replace(/^\/+/, "")}" target="_blank" rel="noopener">打开探测 HTML 摘要</a></p>` : "", summaryHtmlPath ? `<p><a href="${BASE_URL}/${String(summaryHtmlPath).replace(/^\/+/, "")}" target="_blank" rel="noopener">打开探测 HTML 摘要</a></p>` : "",
@@ -464,6 +730,9 @@ const showDomainImportSummary = async (payload: SiteList.DomainUploadSummary) =>
].filter(Boolean); ].filter(Boolean);
if (!arrLines.length) { if (!arrLines.length) {
if (payload?.post_import_workflow?.enabled) {
openPostImportWorkflowDialog(payload);
}
return; return;
} }
@@ -482,6 +751,10 @@ const showDomainImportSummary = async (payload: SiteList.DomainUploadSummary) =>
openPublicPath(summaryJsonPath); openPublicPath(summaryJsonPath);
} }
}).catch(() => undefined); }).catch(() => undefined);
if (payload?.post_import_workflow?.enabled) {
openPostImportWorkflowDialog(payload);
}
}; };
const initParam = reactive({ const initParam = reactive({
@@ -1530,4 +1803,62 @@ watch(
.tagTextColor { .tagTextColor {
border: none; border: none;
} }
.post-import-workflow {
display: flex;
flex-direction: column;
gap: 16px;
}
.post-import-workflow__metrics {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.post-import-workflow__section {
display: flex;
flex-direction: column;
gap: 10px;
}
.post-import-workflow__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.post-import-workflow__card {
border: 1px solid var(--el-border-color-light);
border-radius: 10px;
padding: 14px;
background: var(--el-fill-color-blank);
display: flex;
flex-direction: column;
gap: 10px;
}
.post-import-workflow__title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.post-import-workflow__desc,
.post-import-workflow__hint,
.post-import-workflow__path {
font-size: 13px;
line-height: 1.7;
color: var(--el-text-color-regular);
word-break: break-all;
}
.post-import-workflow__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.post-import-workflow__list {
margin: 0;
padding-left: 18px;
color: var(--el-text-color-regular);
line-height: 1.8;
}
@media (max-width: 900px) {
.post-import-workflow__grid {
grid-template-columns: 1fr;
}
}
</style> </style>

View File

@@ -22,7 +22,13 @@
<el-input v-model="e.value" :disabled="e.disabled" :placeholder="`请输入${e.label}`" clearable></el-input> <el-input v-model="e.value" :disabled="e.disabled" :placeholder="`请输入${e.label}`" clearable></el-input>
</template> </template>
<template v-if="e.type === 'text-tarea'"> <template v-if="e.type === 'text-tarea'">
<el-input v-model="e.value" :rows="5" type="textarea" :placeholder="`请输入${e.label}`" clearable></el-input> <el-input
v-model="e.value"
:rows="e.rows || 5"
type="textarea"
:placeholder="`请输入${e.label}`"
clearable
></el-input>
</template> </template>
<template v-if="e.type === 'textarea'"> <template v-if="e.type === 'textarea'">
<el-input <el-input
@@ -43,6 +49,15 @@
description="平时样式或脚本改动会自动重编只有当前端站群、CDN 或浏览器还在命中旧静态文件时,再手动提高这个版本号,强制全站切到新静态资源。" description="平时样式或脚本改动会自动重编只有当前端站群、CDN 或浏览器还在命中旧静态文件时,再手动提高这个版本号,强制全站切到新静态资源。"
show-icon show-icon
/> />
<el-alert
v-if="isSeoAiRuleConfig"
class="config-helper-alert"
type="warning"
:closable="false"
title="当前为 AI 文案最高规则配置"
description="这里修改后,会直接影响 OpenAI 实时生成和批处理/Codex 导出提示词。建议先改规则文案,再做小批量验证,不要一次性改动多条执行口径。"
show-icon
/>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="drawerVisible = false">取消</el-button> <el-button @click="drawerVisible = false">取消</el-button>
@@ -69,8 +84,11 @@ type Fields = {
required: boolean; required: boolean;
options?: Options[]; options?: Options[];
disabled?: boolean; disabled?: boolean;
rows?: number;
}; };
const isSeoAiRuleCode = (code?: string) => String(code || "").startsWith("SEO_COPY_AI_RULE_");
/** /**
* 解析系统配置字段的校验提示语。 * 解析系统配置字段的校验提示语。
*/ */
@@ -113,6 +131,9 @@ const formColumns = (): Fields[] => {
label: "配置值", label: "配置值",
prop: "sc_val", prop: "sc_val",
type: "text-tarea", type: "text-tarea",
get rows() {
return isSeoAiRuleCode(drawerProps.value.row?.sc_code) ? 16 : 5;
},
get value() { get value() {
return drawerProps.value.row!.sc_val; return drawerProps.value.row!.sc_val;
}, },
@@ -154,6 +175,7 @@ const drawerProps = ref<DrawerProps>({
row: {} row: {}
}); });
const isStaticFileVersion = computed(() => drawerProps.value.row?.sc_code === "STATIC_FILE_VERSION"); const isStaticFileVersion = computed(() => drawerProps.value.row?.sc_code === "STATIC_FILE_VERSION");
const isSeoAiRuleConfig = computed(() => isSeoAiRuleCode(drawerProps.value.row?.sc_code));
const acceptParams = (params: DrawerProps) => { const acceptParams = (params: DrawerProps) => {
drawerProps.value = { drawerProps.value = {

View File

@@ -1,12 +1,46 @@
<template> <template>
<div class="table-box"> <div class="table-box">
<el-alert
class="config-ai-rule-alert"
type="warning"
:closable="false"
title="SEO AI 文案最高规则"
description="GPT 模板当前已经支持在系统配置里直接编辑 AI 文案规则。建议优先通过下方快捷按钮进入这 3 条规则:最高指令、详情摘要规则、详情正文规则。"
show-icon
/>
<ProTable <ProTable
ref="proTable" ref="proTable"
highlight-current-row highlight-current-row
:columns="columns" :columns="columns"
:request-api="getTableList" :request-api="getTableList"
:data-callback="dataCallback" :data-callback="dataCallback"
:row-class-name="tableRowClassName"
> >
<template #tableHeader>
<div class="config-quick-filters">
<el-button
:type="activeQuickKey === '' ? 'primary' : 'default'"
plain
@click="applyQuickFilter('')"
>
全部配置
</el-button>
<el-button
:type="activeQuickKey === 'SEO_COPY_AI_RULE_' ? 'warning' : 'default'"
plain
@click="applyQuickFilter('SEO_COPY_AI_RULE_')"
>
SEO AI 规则
</el-button>
<el-button
:type="activeQuickKey === 'STATIC_FILE_VERSION' ? 'info' : 'default'"
plain
@click="applyQuickFilter('STATIC_FILE_VERSION')"
>
静态缓存兜底
</el-button>
</div>
</template>
<template #empty> <template #empty>
<el-empty description="暂无系统配置" /> <el-empty description="暂无系统配置" />
</template> </template>
@@ -19,6 +53,17 @@
<el-tag v-if="scope.row.sc_code === 'STATIC_FILE_VERSION'" size="small" type="warning" effect="plain"> <el-tag v-if="scope.row.sc_code === 'STATIC_FILE_VERSION'" size="small" type="warning" effect="plain">
缓存兜底 缓存兜底
</el-tag> </el-tag>
<el-tag v-if="isSeoAiRuleCode(scope.row.sc_code)" size="small" type="danger" effect="plain">
SEO AI
</el-tag>
</div>
</template>
<template #sc_description="scope">
<div class="config-description-cell">
<span>{{ scope.row.sc_description }}</span>
<el-tag v-if="isSeoAiRuleCode(scope.row.sc_code)" size="small" type="danger" effect="light">
最高规则
</el-tag>
</div> </div>
</template> </template>
</ProTable> </ProTable>
@@ -36,12 +81,16 @@ import { getConfigList, SaveApi } from "@/api/modules/system/config";
import Drawer from "./drawer.vue"; import Drawer from "./drawer.vue";
const proTable = ref<ProTableInstance>(); const proTable = ref<ProTableInstance>();
const activeQuickKey = ref("");
const initParam = reactive({ const initParam = reactive({
limit: 25, limit: 25,
page: 1 page: 1,
key: ""
}); });
const isSeoAiRuleCode = (code?: string) => String(code || "").startsWith("SEO_COPY_AI_RULE_");
const dataCallback = (data: any) => { const dataCallback = (data: any) => {
return { return {
items: data.items, items: data.items,
@@ -55,9 +104,23 @@ const getTableList = (params: any) => {
let newParams = JSON.parse(JSON.stringify(params)); let newParams = JSON.parse(JSON.stringify(params));
initParam.page = newParams.page; initParam.page = newParams.page;
initParam.limit = newParams.limit; initParam.limit = newParams.limit;
initParam.key = newParams.key ?? "";
return getConfigList(newParams); return getConfigList(newParams);
}; };
const applyQuickFilter = (keyword: string) => {
activeQuickKey.value = keyword;
if (!proTable.value) return;
proTable.value.searchParam.key = keyword;
proTable.value.search();
};
const tableRowClassName = ({ row }: { row: ConfigList.ResList }) => {
if (isSeoAiRuleCode(row?.sc_code)) return "danger-row";
if (row?.sc_code === "STATIC_FILE_VERSION") return "warning-row";
return "";
};
const columns = ref<ColumnProps[]>([ const columns = ref<ColumnProps[]>([
{ {
label: "配置编码 / ID", label: "配置编码 / ID",
@@ -112,10 +175,32 @@ const openDrawer = (row: Partial<ConfigList.ResList> = {}) => {
.el-table .success-row .el-table-fixed-column--left { .el-table .success-row .el-table-fixed-column--left {
background-color: var(--el-color-success-light-9); background-color: var(--el-color-success-light-9);
} }
.el-table .danger-row,
.el-table .danger-row .el-table-fixed-column--right,
.el-table .danger-row .el-table-fixed-column--left {
background-color: var(--el-color-danger-light-9);
}
.config-ai-rule-alert {
margin-bottom: 16px;
}
.config-quick-filters {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.config-code-cell { .config-code-cell {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.config-description-cell {
display: inline-flex;
align-items: center;
gap: 8px;
}
</style> </style>

View File

@@ -0,0 +1,254 @@
<template>
<div class="seo-ai-rules-page">
<el-alert
class="seo-ai-rules-page__alert"
type="warning"
:closable="false"
title="SEO AI 文案规则专属入口"
description="这里集中维护 GPT 模板当前 AI 文案最高规则。上半部分是实际会进入 AI 提示词的规则,下半部分“实验备注”只用于记录修改目的、观察指标和回看日期,不会喂给模型。"
show-icon
/>
<div class="seo-ai-rules-page__toolbar">
<el-button type="primary" plain @click="refreshRules">刷新规则</el-button>
<el-button type="danger" plain @click="resetAllRules">全部恢复默认</el-button>
<el-button plain @click="openSystemConfig">打开系统配置总表</el-button>
</div>
<el-row :gutter="16" v-loading="loading">
<el-col v-for="item in ruleItems" :key="item.sc_code" :xs="24" :sm="24" :md="12" :lg="8">
<el-card class="seo-ai-rules-page__card" shadow="hover">
<template #header>
<div class="seo-ai-rules-page__card-header">
<div>
<div class="seo-ai-rules-page__card-title">{{ resolveRuleTitle(item.sc_code) }}</div>
<div class="seo-ai-rules-page__card-code">{{ item.sc_code }}</div>
</div>
<div class="seo-ai-rules-page__card-actions">
<el-button type="warning" link @click="openNoteDrawer(item)">编辑备注</el-button>
<el-button type="danger" link @click="resetSingleRule(item)">恢复默认</el-button>
<el-button type="primary" link @click="openDrawer(item)">编辑</el-button>
</div>
</div>
</template>
<div class="seo-ai-rules-page__description">{{ item.sc_description }}</div>
<pre class="seo-ai-rules-page__value">{{ item.sc_val }}</pre>
<div class="seo-ai-rules-page__note-title">实验备注</div>
<pre class="seo-ai-rules-page__note">{{ item.note?.sc_val || "暂未填写实验备注" }}</pre>
</el-card>
</el-col>
</el-row>
<el-empty v-if="!loading && ruleItems.length === 0" description="当前还没有加载到 SEO AI 规则配置" />
<Drawer ref="drawerRef" />
</div>
</template>
<script setup lang="ts" name="SeoAiRulesConfig">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { ConfigList } from "@/api/interface/system/config";
import { getConfigList, SaveApi, resetSeoAiRulesApi } from "@/api/modules/system/config";
import Drawer from "./drawer.vue";
const router = useRouter();
const drawerRef = ref<InstanceType<typeof Drawer> | null>(null);
const loading = ref(false);
const ruleItems = ref<ConfigList.SeoAiRuleCard[]>([]);
const RULE_CODE = "SEO_COPY_AI_RULE_";
const NOTE_SUFFIX = "_NOTE";
const resolveRuleTitle = (code: string) => {
if (code === "SEO_COPY_AI_RULE_HIGHEST") return "最高指令";
if (code === "SEO_COPY_AI_RULE_DETAIL_SUMMARY") return "详情摘要规则";
if (code === "SEO_COPY_AI_RULE_DETAIL_BODY") return "详情正文规则";
if (code === "SEO_COPY_AI_RULE_MISSING_FIELDS") return "缺失字段处理规则";
if (code === "SEO_COPY_AI_RULE_MOVIE_STYLE") return "电影细分规则";
if (code === "SEO_COPY_AI_RULE_SERIES_STYLE") return "剧集细分规则";
if (code === "SEO_COPY_AI_RULE_VARIETY_STYLE") return "综艺细分规则";
if (code === "SEO_COPY_AI_RULE_ANIME_STYLE") return "动漫细分规则";
if (code === "SEO_COPY_AI_RULE_SHORT_DRAMA_STYLE") return "短剧细分规则";
return code;
};
const resolveNoteCode = (code: string) => `${code}${NOTE_SUFFIX}`;
const loadRules = async () => {
loading.value = true;
try {
const { data } = await getConfigList({
page: 1,
limit: 100,
key: RULE_CODE
});
const allItems = Array.isArray(data?.items) ? data.items : [];
const noteMap = new Map<string, ConfigList.ResList>();
allItems.forEach((item: ConfigList.ResList) => {
if (String(item.sc_code || "").endsWith(NOTE_SUFFIX)) {
noteMap.set(String(item.sc_code), item);
}
});
ruleItems.value = allItems
.filter((item: ConfigList.ResList) => !String(item.sc_code || "").endsWith(NOTE_SUFFIX))
.map((item: ConfigList.ResList) => ({
...item,
note: noteMap.get(resolveNoteCode(item.sc_code)) || null
}));
} finally {
loading.value = false;
}
};
const refreshRules = async () => {
await loadRules();
ElMessage.success("SEO AI 规则已刷新");
};
const resetAllRules = async () => {
await ElMessageBox.confirm("是否将全部 SEO AI 规则恢复为当前代码默认基线?该操作会覆盖后台现有自定义内容。", "恢复默认", {
type: "warning",
confirmButtonText: "恢复全部",
cancelButtonText: "取消"
});
await resetSeoAiRulesApi({});
await loadRules();
ElMessage.success("全部 SEO AI 规则已恢复默认");
};
const resetSingleRule = async (row: ConfigList.ResList) => {
await ElMessageBox.confirm(`是否将「${resolveRuleTitle(row.sc_code)}」恢复为默认规则?`, "恢复默认", {
type: "warning",
confirmButtonText: "恢复默认",
cancelButtonText: "取消"
});
await resetSeoAiRulesApi({ sc_code: row.sc_code });
await loadRules();
ElMessage.success(`${resolveRuleTitle(row.sc_code)}」已恢复默认`);
};
const openSystemConfig = () => {
router.push("/system/config/index");
};
const openDrawer = (row: Partial<ConfigList.ResList> = {}) => {
drawerRef.value?.acceptParams({
title: "编辑",
row: { ...row },
api: SaveApi,
getTableList: loadRules,
successMessage: "SEO AI 规则保存完成,已刷新专属页面"
} as any);
};
const openNoteDrawer = (row: ConfigList.SeoAiRuleCard) => {
const noteRow = row.note && row.note.sc_code
? { ...row.note }
: {
sc_code: resolveNoteCode(row.sc_code),
sc_val: "",
sc_description: `${resolveRuleTitle(row.sc_code)} 实验备注`
};
drawerRef.value?.acceptParams({
title: "编辑实验备注",
row: noteRow,
api: SaveApi,
getTableList: loadRules,
successMessage: `${resolveRuleTitle(row.sc_code)} 的实验备注已保存`
} as any);
};
void loadRules();
</script>
<style scoped lang="scss">
.seo-ai-rules-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.seo-ai-rules-page__alert {
margin-bottom: 4px;
}
.seo-ai-rules-page__toolbar {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.seo-ai-rules-page__card {
margin-bottom: 16px;
min-height: 100%;
}
.seo-ai-rules-page__card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.seo-ai-rules-page__card-actions {
display: inline-flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.seo-ai-rules-page__card-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.seo-ai-rules-page__card-code {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
word-break: break-all;
}
.seo-ai-rules-page__description {
margin-bottom: 12px;
color: var(--el-text-color-regular);
line-height: 1.6;
}
.seo-ai-rules-page__value {
margin: 0;
padding: 14px;
border-radius: 10px;
background: var(--el-fill-color-light);
color: var(--el-text-color-primary);
white-space: pre-wrap;
word-break: break-word;
line-height: 1.7;
font-size: 13px;
}
.seo-ai-rules-page__note-title {
margin-top: 14px;
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-regular);
}
.seo-ai-rules-page__note {
margin: 0;
padding: 12px;
border-radius: 10px;
background: var(--el-color-warning-light-9);
color: var(--el-text-color-primary);
white-space: pre-wrap;
word-break: break-word;
line-height: 1.7;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,496 @@
<template>
<div class="metadata-missing-page">
<el-card shadow="never" class="metadata-missing-page__hero">
<template #header>
<div class="metadata-missing-page__hero-header">
<div>
<div class="metadata-missing-page__title">{{ pageTitle }}</div>
<div class="metadata-missing-page__subtitle">{{ pageSubtitle }}</div>
</div>
<div class="metadata-missing-page__hero-actions">
<el-button :loading="loading" @click="loadCurrentPage">刷新当前页</el-button>
<el-button type="primary" plain :loading="workbenchRunning" @click="handleRunWorkbench">刷新工作台</el-button>
<el-button type="warning" plain :loading="taskPoolRunning" @click="handleRunTaskPool">刷新任务池</el-button>
</div>
</div>
</template>
<el-alert :type="heroAlertType" :closable="false" show-icon :title="heroAlertTitle" :description="heroAlertDescription" />
<div class="metadata-missing-page__nav">
<el-button :type="isMode('workbench') ? 'primary' : 'default'" @click="goMode('workbench')">工作台</el-button>
<el-button :type="isMode('task-pool') ? 'primary' : 'default'" @click="goMode('task-pool')">任务池</el-button>
<el-button :type="isMode('status') ? 'primary' : 'default'" @click="goMode('status')">状态总览</el-button>
<el-button :type="isMode('ops') ? 'primary' : 'default'" @click="goMode('ops')">运维总览</el-button>
</div>
</el-card>
<template v-if="currentMode === 'workbench'">
<div class="metadata-missing-page__stats">
<div class="metadata-stat-card">
<div class="metadata-stat-card__label">总视频数</div>
<div class="metadata-stat-card__value">{{ workbenchSummary.audit?.total_videos || 0 }}</div>
</div>
<div class="metadata-stat-card">
<div class="metadata-stat-card__label">缺字段视频数</div>
<div class="metadata-stat-card__value">{{ workbenchSummary.audit?.videos_with_any_missing_metadata || 0 }}</div>
</div>
<div class="metadata-stat-card">
<div class="metadata-stat-card__label">重采优先队列</div>
<div class="metadata-stat-card__value">{{ workbenchSummary.queue?.queue_size || 0 }}</div>
</div>
</div>
<el-card shadow="never">
<template #header><span>下一步动作</span></template>
<el-empty v-if="!(workbenchSummary.next_actions || []).length" description="当前还没有下一步动作建议" />
<div v-else class="metadata-action-list">
<div v-for="item in workbenchSummary.next_actions || []" :key="item.key || item.label" class="metadata-action-card">
<div class="metadata-action-card__title">{{ item.label || "-" }}</div>
<div class="metadata-action-card__desc">{{ item.summary || "-" }}</div>
</div>
</div>
</el-card>
<el-card shadow="never">
<template #header><span>缺失最严重字段</span></template>
<el-table :data="workbenchSummary.top_missing_fields || []" border>
<el-table-column prop="field" label="字段" min-width="180" />
<el-table-column prop="missing_count" label="缺失数" min-width="120" />
<el-table-column label="缺失率" min-width="120">
<template #default="scope">
{{ formatPercent(scope.row?.missing_ratio) }}
</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never">
<template #header>
<div class="metadata-card-header">
<span>Codex 派单提示词</span>
<el-button text type="primary" @click="copyPrompt(workbenchSummary.codex_dispatch_prompt)">复制提示词</el-button>
</div>
</template>
<pre class="metadata-code-block">{{ workbenchSummary.codex_dispatch_prompt || "当前还没有生成提示词" }}</pre>
</el-card>
</template>
<template v-else-if="currentMode === 'task-pool'">
<div class="metadata-missing-page__stats">
<div class="metadata-stat-card">
<div class="metadata-stat-card__label">批次数</div>
<div class="metadata-stat-card__value">{{ taskPoolSummary.batches?.length || 0 }}</div>
</div>
<div class="metadata-stat-card">
<div class="metadata-stat-card__label">缺字段视频数</div>
<div class="metadata-stat-card__value">{{ taskPoolSummary.videos_with_any_missing_metadata || 0 }}</div>
</div>
<div class="metadata-stat-card">
<div class="metadata-stat-card__label">重采优先队列</div>
<div class="metadata-stat-card__value">{{ taskPoolSummary.queue_size || 0 }}</div>
</div>
</div>
<el-alert
v-if="taskPoolSummary.operator_hint"
type="info"
:closable="false"
show-icon
:title="String(taskPoolSummary.operator_hint)"
/>
<el-card shadow="never">
<template #header><span>批次列表</span></template>
<el-table :data="taskPoolSummary.batches || []" border>
<el-table-column prop="batch_id" label="批次ID" min-width="180" />
<el-table-column prop="batch_label" label="批次名称" min-width="220" />
<el-table-column prop="focus_field" label="重点字段" min-width="140" />
<el-table-column prop="priority_level" label="优先级" min-width="100" />
<el-table-column prop="status_label" label="状态" min-width="120" />
<el-table-column prop="items_count" label="数量" min-width="100" />
<el-table-column label="操作" min-width="120" fixed="right">
<template #default="scope">
<el-button text type="primary" @click="copyPrompt(scope.row?.codex_prompt)">复制提示词</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<template v-else-if="currentMode === 'status'">
<div class="metadata-missing-page__stats">
<div class="metadata-stat-card">
<div class="metadata-stat-card__label">缺字段视频数</div>
<div class="metadata-stat-card__value">{{ statusSummary.workbench?.videos_with_any_missing_metadata || 0 }}</div>
</div>
<div class="metadata-stat-card">
<div class="metadata-stat-card__label">重采优先队列</div>
<div class="metadata-stat-card__value">{{ statusSummary.workbench?.queue_size || 0 }}</div>
</div>
<div class="metadata-stat-card">
<div class="metadata-stat-card__label">任务池批次数</div>
<div class="metadata-stat-card__value">{{ statusSummary.task_pool?.batch_count || 0 }}</div>
</div>
</div>
<el-card shadow="never">
<template #header><span>计划任务状态</span></template>
<el-descriptions :column="2" border>
<el-descriptions-item label="状态">{{ statusSummary.plan_task?.status_label || "-" }}</el-descriptions-item>
<el-descriptions-item label="启用">{{ statusSummary.plan_task?.enabled ? "是" : "否" }}</el-descriptions-item>
<el-descriptions-item label="频率">{{ statusSummary.plan_task?.limit_human || "-" }}</el-descriptions-item>
<el-descriptions-item label="任务编码">{{ statusSummary.plan_task?.pt_code || "-" }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card shadow="never">
<template #header><span>运维提示</span></template>
<el-empty v-if="!(statusSummary.operator_hint || []).length" description="当前还没有额外提示" />
<div v-else class="metadata-hint-list">
<div v-for="(item, index) in statusSummary.operator_hint || []" :key="index" class="metadata-hint-list__item">
{{ item }}
</div>
</div>
</el-card>
</template>
<template v-else>
<el-card shadow="never">
<template #header><span>运维动作</span></template>
<div class="metadata-action-list">
<div class="metadata-action-card">
<div class="metadata-action-card__title">推荐动作</div>
<div class="metadata-action-card__desc">{{ opsSummary.ops?.recommended_action || statusSummary.recommended_action || "-" }}</div>
</div>
</div>
</el-card>
<el-card shadow="never">
<template #header><span>常用命令</span></template>
<el-table :data="opsSummary.ops?.command_entries || []" border>
<el-table-column prop="label" label="用途" min-width="180" />
<el-table-column prop="command" label="命令 / 接口" min-width="420" show-overflow-tooltip />
</el-table>
</el-card>
<el-card shadow="never">
<template #header><span>产物路径</span></template>
<el-table :data="opsSummary.ops?.artifact_entries || []" border>
<el-table-column prop="label" label="名称" min-width="180" />
<el-table-column prop="path" label="路径" min-width="420" show-overflow-tooltip />
</el-table>
</el-card>
</template>
</div>
</template>
<script setup lang="ts" name="VideoMetadataMissingPage">
import { computed, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import {
getMetadataMissingTaskPoolOps,
getMetadataMissingTaskPoolStatus,
getMetadataMissingTaskPoolSummary,
getMetadataMissingWorkbenchSummary,
runMetadataMissingTaskPool,
runMetadataMissingWorkbench
} from "@/api/modules/video/list";
import type { VideoList } from "@/api/interface/video/list";
type PageMode = "workbench" | "task-pool" | "status" | "ops";
const route = useRoute();
const router = useRouter();
const loading = ref(false);
const workbenchRunning = ref(false);
const taskPoolRunning = ref(false);
const statusSummary = ref<VideoList.MetadataMissingTaskPoolStatusData>({});
const opsSummary = ref<VideoList.MetadataMissingTaskPoolOpsData>({});
const workbenchSummary = ref<VideoList.MetadataMissingWorkbenchSummaryData>({});
const taskPoolSummary = ref<VideoList.MetadataMissingTaskPoolData>({});
const currentMode = computed<PageMode>(() => {
const path = route.path;
if (path.endsWith("/task-pool/ops")) return "ops";
if (path.endsWith("/task-pool/status")) return "status";
if (path.endsWith("/task-pool")) return "task-pool";
return "workbench";
});
const pageTitle = computed(() => {
return {
workbench: "视频缺字段工作台",
"task-pool": "视频缺字段任务池",
status: "视频缺字段状态总览",
ops: "视频缺字段运维总览"
}[currentMode.value];
});
const pageSubtitle = computed(() => {
return {
workbench: "这里看缺字段规模、重点字段和 Codex 派单提示词。",
"task-pool": "这里看分批结果、批次状态和复制提示词入口。",
status: "这里看计划任务、缺字段规模和当前建议动作。",
ops: "这里看常用命令、推荐动作和运维产物路径。"
}[currentMode.value];
});
const heroAlertType = computed(() => {
if (currentMode.value === "status" || currentMode.value === "ops") {
const label = statusSummary.value.plan_task?.status_label || "missing";
if (label === "enabled") return "success";
if (label === "disabled") return "warning";
if (label === "db_blocked") return "error";
return "info";
}
if (currentMode.value === "task-pool") {
return (taskPoolSummary.value.batches?.length || 0) > 0 ? "success" : "warning";
}
return (workbenchSummary.value.audit?.videos_with_any_missing_metadata || 0) > 0 ? "warning" : "success";
});
const heroAlertTitle = computed(() => {
if (currentMode.value === "status" || currentMode.value === "ops") {
const label = statusSummary.value.plan_task?.status_label || "missing";
if (label === "enabled") return "计划任务已启用";
if (label === "disabled") return "计划任务已存在但未启用";
if (label === "db_blocked") return "当前环境暂时无法读取计划任务";
return "计划任务尚未补齐";
}
if (currentMode.value === "task-pool") return "这里已经完全迁到 Vue 后台,不再依赖 PHP 页面入口";
return "视频缺字段工作台已经迁到 Vue 后台首层页面";
});
const heroAlertDescription = computed(() => {
if (currentMode.value === "status" || currentMode.value === "ops") {
return statusSummary.value.recommended_action || "先刷新当前页,再看下一步动作。";
}
if (currentMode.value === "task-pool") {
return taskPoolSummary.value.operator_hint || "当前适合从这里继续看批次和复制提示词。";
}
return (workbenchSummary.value.next_actions || []).map(item => item.label).filter(Boolean).join(" / ") || "当前适合先看缺字段规模和重点字段。";
});
const isMode = (mode: PageMode) => currentMode.value === mode;
const goMode = (mode: PageMode) => {
const map: Record<PageMode, string> = {
workbench: "/video/metadata/missing/workbench",
"task-pool": "/video/metadata/missing/task-pool",
status: "/video/metadata/missing/task-pool/status",
ops: "/video/metadata/missing/task-pool/ops"
};
router.push(map[mode]);
};
const formatPercent = (value?: number) => `${(((value || 0) as number) * 100).toFixed(2)}%`;
const copyPrompt = async (text?: string) => {
const value = String(text || "").trim();
if (!value) {
ElMessage.warning("当前没有可复制的提示词");
return;
}
await navigator.clipboard.writeText(value);
ElMessage.success("提示词已复制");
};
const loadCurrentPage = async () => {
loading.value = true;
try {
if (currentMode.value === "workbench") {
const res = await getMetadataMissingWorkbenchSummary();
workbenchSummary.value = res.data || {};
return;
}
if (currentMode.value === "task-pool") {
const res = await getMetadataMissingTaskPoolSummary();
taskPoolSummary.value = res.data || {};
return;
}
if (currentMode.value === "status") {
const res = await getMetadataMissingTaskPoolStatus();
statusSummary.value = res.data || {};
return;
}
const [statusRes, opsRes] = await Promise.all([getMetadataMissingTaskPoolStatus(), getMetadataMissingTaskPoolOps()]);
statusSummary.value = statusRes.data || {};
opsSummary.value = opsRes.data || {};
} finally {
loading.value = false;
}
};
const handleRunWorkbench = async () => {
workbenchRunning.value = true;
try {
const res = await runMetadataMissingWorkbench();
ElMessage.success(res.msg || "工作台刷新已触发");
await loadCurrentPage();
} finally {
workbenchRunning.value = false;
}
};
const handleRunTaskPool = async () => {
taskPoolRunning.value = true;
try {
const res = await runMetadataMissingTaskPool();
ElMessage.success(res.msg || "任务池刷新已触发");
await loadCurrentPage();
} finally {
taskPoolRunning.value = false;
}
};
watch(
() => route.path,
() => {
loadCurrentPage();
}
);
onMounted(() => {
loadCurrentPage();
});
</script>
<style scoped lang="scss">
.metadata-missing-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.metadata-missing-page__hero {
border-radius: 20px;
background: linear-gradient(135deg, #fffdf4 0%, #ffffff 55%, #eef6ff 100%);
}
.metadata-missing-page__hero-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
}
.metadata-missing-page__title {
font-size: 24px;
font-weight: 800;
color: #111827;
line-height: 1.2;
}
.metadata-missing-page__subtitle {
margin-top: 8px;
color: #6b7280;
line-height: 1.7;
}
.metadata-missing-page__hero-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.metadata-missing-page__nav {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 16px;
}
.metadata-missing-page__stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.metadata-stat-card {
padding: 18px;
border-radius: 16px;
background: #fff;
border: 1px solid #e5e7eb;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
}
.metadata-stat-card__label {
font-size: 13px;
color: #6b7280;
}
.metadata-stat-card__value {
margin-top: 10px;
font-size: 30px;
font-weight: 800;
line-height: 1;
color: #111827;
}
.metadata-action-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.metadata-action-card {
padding: 16px;
border-radius: 14px;
background: #f8fafc;
border: 1px solid #e5e7eb;
}
.metadata-action-card__title {
font-size: 16px;
font-weight: 700;
color: #111827;
}
.metadata-action-card__desc {
margin-top: 8px;
line-height: 1.8;
color: #475569;
}
.metadata-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.metadata-code-block {
margin: 0;
padding: 16px;
border-radius: 14px;
background: #0f172a;
color: #e2e8f0;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.7;
}
.metadata-hint-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.metadata-hint-list__item {
padding: 14px 16px;
border-radius: 14px;
background: #f8fafc;
border: 1px solid #e5e7eb;
line-height: 1.8;
}
@media (max-width: 960px) {
.metadata-missing-page__stats,
.metadata-action-list {
grid-template-columns: 1fr;
}
}
</style>