Compare commits
5 Commits
845d68110b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8884f9c416 | ||
|
|
e801249e21 | ||
|
|
31aecb388a | ||
|
|
992ae11b21 | ||
|
|
2d1834422b |
@@ -2,6 +2,7 @@ export const loginApiUrl = "/guest/login";
|
||||
|
||||
export const getSystemConfigListApiUrl = "/system/config/list";
|
||||
export const getSystemConfigSaveApiUrl = "/system/config/save";
|
||||
export const getSystemConfigSeoAiRulesResetApiUrl = "/system/config/seo-ai-rules/reset";
|
||||
|
||||
export const getSystemPlanListApiUrl = "/system/plan/list";
|
||||
export const getSystemPlanSaveApiUrl = "/system/plan/save";
|
||||
|
||||
@@ -264,6 +264,7 @@ export namespace SiteList {
|
||||
export interface DomainSeoCopyHistoryRollbackData {
|
||||
host?: string;
|
||||
rollback_from_job_id?: string;
|
||||
job?: DomainImportAsyncJobStatusData;
|
||||
written_pages?: Array<{
|
||||
scene?: string;
|
||||
page_key?: string;
|
||||
@@ -924,7 +925,15 @@ export namespace SiteList {
|
||||
skipped_template_missing_count?: number;
|
||||
skipped_duplicate_in_file_count?: 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 }>;
|
||||
run_root?: string;
|
||||
uploaded_excel_path?: string;
|
||||
@@ -934,6 +943,67 @@ export namespace SiteList {
|
||||
probe_failed_count?: number;
|
||||
summary_json_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 {
|
||||
@@ -1449,6 +1519,7 @@ export namespace SiteList {
|
||||
run_id?: string;
|
||||
generated_at?: string;
|
||||
status?: string;
|
||||
mode?: string;
|
||||
run_root?: string;
|
||||
domain_count?: number;
|
||||
aggregate_count?: number;
|
||||
@@ -1461,8 +1532,13 @@ export namespace SiteList {
|
||||
gitee_push_attempted?: number;
|
||||
gitee_push_status?: string;
|
||||
gitee_push_error?: string;
|
||||
github_pushed?: number;
|
||||
github_push_attempted?: number;
|
||||
github_push_status?: string;
|
||||
github_push_error?: string;
|
||||
files?: DomainSpiderMdFileItem[];
|
||||
gitee_files?: DomainSpiderMdFileItem[];
|
||||
github_files?: DomainSpiderMdFileItem[];
|
||||
}
|
||||
|
||||
export interface DomainSpiderMdSummaryData {
|
||||
@@ -1495,6 +1571,15 @@ export namespace SiteList {
|
||||
root?: 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;
|
||||
runs?: DomainSpiderMdRunSummaryItem[];
|
||||
}
|
||||
@@ -1814,6 +1899,7 @@ export namespace SiteList {
|
||||
generated_at?: string;
|
||||
mode?: string;
|
||||
note?: string;
|
||||
hero_summary?: string;
|
||||
real_external_metrics?: boolean;
|
||||
host_count?: number;
|
||||
ready_count?: number;
|
||||
@@ -1879,6 +1965,7 @@ export namespace SiteList {
|
||||
generated_at?: string;
|
||||
mode?: string;
|
||||
note?: string;
|
||||
hero_summary?: string;
|
||||
real_external_metrics?: boolean;
|
||||
host_count?: number;
|
||||
ready_count?: number;
|
||||
|
||||
@@ -9,6 +9,9 @@ export namespace ConfigList {
|
||||
sc_val: string;
|
||||
sc_description: string;
|
||||
}
|
||||
export interface resetSeoAiRuleParams {
|
||||
sc_code?: string;
|
||||
}
|
||||
export interface delParams {
|
||||
sc_id: string;
|
||||
}
|
||||
@@ -18,5 +21,8 @@ export namespace ConfigList {
|
||||
sc_val: string;
|
||||
sc_description: string;
|
||||
}
|
||||
export interface SeoAiRuleCard extends ResList {
|
||||
note?: Partial<ResList> | null;
|
||||
}
|
||||
export interface ResData extends ResultData {}
|
||||
}
|
||||
|
||||
@@ -78,4 +78,125 @@ export namespace VideoList {
|
||||
tag_list?: any;
|
||||
}
|
||||
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>>;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ResPage, ResultData } from "@/api/interface/index";
|
||||
import { ConfigList } from "@/api/interface/system/config";
|
||||
import { getSystemConfigListApiUrl, getSystemConfigSaveApiUrl } from "@/api/config";
|
||||
import { getSystemConfigListApiUrl, getSystemConfigSaveApiUrl, getSystemConfigSeoAiRulesResetApiUrl } from "@/api/config";
|
||||
import http from "@/api";
|
||||
|
||||
|
||||
@@ -12,3 +12,7 @@ export const getConfigList = (params: ConfigList.ReqParams) => {
|
||||
export const SaveApi = (params: ConfigList.saveParams) => {
|
||||
return http.post<ResPage<ResultData>>(getSystemConfigSaveApiUrl, params);
|
||||
};
|
||||
|
||||
export const resetSeoAiRulesApi = (params: ConfigList.resetSeoAiRuleParams) => {
|
||||
return http.post<ResPage<ResultData>>(getSystemConfigSeoAiRulesResetApiUrl, params);
|
||||
};
|
||||
|
||||
@@ -19,3 +19,27 @@ export const batchData = (params: VideoList.delParams) => {
|
||||
export const batchVipData = (params: VideoList.delParams) => {
|
||||
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`, {});
|
||||
};
|
||||
|
||||
@@ -134,6 +134,21 @@
|
||||
"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",
|
||||
"name": "endpointList",
|
||||
@@ -458,6 +473,66 @@
|
||||
"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",
|
||||
|
||||
54
src/utils/adminArtifact.ts
Normal file
54
src/utils/adminArtifact.ts
Normal 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("打开产物失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
@@ -6,3 +6,101 @@
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,60 @@
|
||||
<template>
|
||||
<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">
|
||||
<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>
|
||||
@@ -13,14 +68,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="home">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import BootstrapOpsOverview from "@/views/site/bootstrapOpsOverview.vue";
|
||||
import BootstrapEnvOverview from "@/views/site/bootstrapEnvOverview.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 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 BootstrapFocusPayload = BootstrapFocusMode | BootstrapFocusMode[];
|
||||
type BootstrapComparePreset =
|
||||
@@ -77,6 +143,69 @@ const openBootstrapCenter = (payload: BootstrapAdminOpenPayload = "overview") =>
|
||||
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 normalizedFocus = typeof payload === "string" || Array.isArray(payload) ? payload : payload.focusMode ?? "all";
|
||||
const comparePreset = typeof payload === "string" || Array.isArray(payload) ? undefined : payload.comparePreset;
|
||||
@@ -93,6 +222,10 @@ const goSitePage = (payload: BootstrapDialogPayload = "all") => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadMetadataConsole();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -55,15 +55,14 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const queue = ref<SiteList.DomainExternalSeoAizhanKeywordAttentionQueueData>({});
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadQueue = async () => {
|
||||
|
||||
@@ -69,10 +69,10 @@ import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/site/list";
|
||||
import { getDomainExternalSeoAizhanKeywordProbeSummary } from "@/api/modules/site/list";
|
||||
import { openAdminArtifact } from "@/utils/adminArtifact";
|
||||
import DomainExternalSeoAizhanKeywordAttentionDialog from "./domainExternalSeoAizhanKeywordAttentionDialog.vue";
|
||||
import DomainExternalSeoAizhanKeywordTrendDialog from "./domainExternalSeoAizhanKeywordTrendDialog.vue";
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL as string;
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoAizhanKeywordProbeData>({});
|
||||
@@ -80,8 +80,7 @@ const attentionDialogRef = ref();
|
||||
const trendDialogRef = ref();
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const alertTitle = computed(() => {
|
||||
|
||||
@@ -59,8 +59,8 @@ import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoAizhanKeywordTrendData>({});
|
||||
@@ -74,8 +74,7 @@ const alertType = computed(() => {
|
||||
});
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadSummary = async () => {
|
||||
|
||||
@@ -65,15 +65,14 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoBaiduFeedbackSummaryData>({});
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadSummary = async () => {
|
||||
|
||||
@@ -61,6 +61,7 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/site/list";
|
||||
import { getDomainExternalSeoBaiduProviderPlan } from "@/api/modules/site/list";
|
||||
import { openAdminArtifact } from "@/utils/adminArtifact";
|
||||
import DomainExternalSeoBaiduPushDialog from "./domainExternalSeoBaiduPushDialog.vue";
|
||||
import DomainExternalSeoBaiduFeedbackDialog from "./domainExternalSeoBaiduFeedbackDialog.vue";
|
||||
import DomainExternalSeoSiteQueryDialog from "./domainExternalSeoSiteQueryDialog.vue";
|
||||
@@ -68,7 +69,6 @@ import DomainExternalSeoBaiduPushTaskDialog from "./domainExternalSeoBaiduPushTa
|
||||
import DomainExternalSeoBaiduPushAttentionDialog from "./domainExternalSeoBaiduPushAttentionDialog.vue";
|
||||
import DomainExternalSeoBaiduPushTrendDialog from "./domainExternalSeoBaiduPushTrendDialog.vue";
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL as string;
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoBaiduProviderPlanData>({});
|
||||
@@ -80,8 +80,7 @@ const attentionDialogRef = ref();
|
||||
const trendDialogRef = ref();
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const openPushSummaryDialog = () => {
|
||||
|
||||
@@ -68,15 +68,14 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const queue = ref<SiteList.DomainExternalSeoBaiduPushAttentionQueueData>({});
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadQueue = async () => {
|
||||
|
||||
@@ -70,11 +70,11 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/site/list";
|
||||
import { getDomainExternalSeoBaiduPushSummary } from "@/api/modules/site/list";
|
||||
import { openAdminArtifact } from "@/utils/adminArtifact";
|
||||
import DomainExternalSeoBaiduPushTaskDialog from "./domainExternalSeoBaiduPushTaskDialog.vue";
|
||||
import DomainExternalSeoBaiduPushAttentionDialog from "./domainExternalSeoBaiduPushAttentionDialog.vue";
|
||||
import DomainExternalSeoBaiduPushTrendDialog from "./domainExternalSeoBaiduPushTrendDialog.vue";
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL as string;
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoBaiduPushSummaryData>({});
|
||||
@@ -83,8 +83,7 @@ const attentionDialogRef = ref();
|
||||
const trendDialogRef = ref();
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadSummary = async () => {
|
||||
|
||||
@@ -66,8 +66,8 @@ import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoBaiduPushTrendData>({});
|
||||
@@ -80,8 +80,7 @@ const alertType = computed(() => {
|
||||
});
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadSummary = async () => {
|
||||
|
||||
@@ -43,16 +43,15 @@ import { ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const rows = ref<SiteList.DomainExternalSeoFailedQueueItem[]>([]);
|
||||
const queue = ref<SiteList.DomainExternalSeoFailedQueueData>({});
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const showCommand = async (title: string, command?: string) => {
|
||||
|
||||
@@ -39,17 +39,14 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const rows = ref<SiteList.DomainExternalSeoManualImportRunItem[]>([]);
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadRuns = async () => {
|
||||
|
||||
@@ -370,6 +370,7 @@
|
||||
import { ref } from "vue";
|
||||
import { ElMessage, UploadRawFile } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/site/list";
|
||||
import { openAdminArtifact } from "@/utils/adminArtifact";
|
||||
import {
|
||||
getDomainExternalSeoManualImportTemplate,
|
||||
getDomainExternalSeoCnOverview,
|
||||
@@ -606,7 +607,7 @@ const downloadManualTemplate = async () => {
|
||||
ElMessage.warning("模板生成成功,但没有可打开的文件路径");
|
||||
return;
|
||||
}
|
||||
window.open(`${import.meta.env.VITE_API_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
await openAdminArtifact(path);
|
||||
} catch (_error) {
|
||||
ElMessage.error("生成站外手工导入模板失败");
|
||||
}
|
||||
@@ -620,7 +621,7 @@ const downloadSearchConsolePropertyMapTemplate = async () => {
|
||||
ElMessage.warning("映射模板生成成功,但没有可打开的文件路径");
|
||||
return;
|
||||
}
|
||||
window.open(`${import.meta.env.VITE_API_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
await openAdminArtifact(path);
|
||||
} catch (_error) {
|
||||
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} 行`);
|
||||
const summaryHtmlPath = res.data.summary_html_path;
|
||||
if (summaryHtmlPath) {
|
||||
window.open(`${import.meta.env.VITE_API_URL}/${String(summaryHtmlPath).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
await openAdminArtifact(summaryHtmlPath);
|
||||
}
|
||||
await loadSummary();
|
||||
openRealSummaryDialog();
|
||||
@@ -697,7 +698,7 @@ const uploadSearchConsolePropertyMap = async (param: any) => {
|
||||
ElMessage.success(`Search Console 映射上传完成:${res.data.mapped_hosts_count || 0} 个 host`);
|
||||
const summaryHtmlPath = res.data.summary_html_path;
|
||||
if (summaryHtmlPath) {
|
||||
window.open(`${import.meta.env.VITE_API_URL}/${String(summaryHtmlPath).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
await openAdminArtifact(summaryHtmlPath);
|
||||
}
|
||||
await loadSummary();
|
||||
openRealSummaryDialog();
|
||||
|
||||
@@ -52,6 +52,7 @@ import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/site/list";
|
||||
import { getDomainExternalSeoRealSummary } from "@/api/modules/site/list";
|
||||
import { openAdminArtifact } from "@/utils/adminArtifact";
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
@@ -90,8 +91,7 @@ const loadSummary = async () => {
|
||||
};
|
||||
|
||||
const openSummaryFile = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${import.meta.env.VITE_API_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const statusLabel = (status?: string) => {
|
||||
|
||||
@@ -145,6 +145,7 @@ import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/site/list";
|
||||
import { getDomainExternalSeoRealSummary, getDomainExternalSeoSnapshotRuns, getDomainExternalSeoSummary, getDomainExternalSeoTrendSummary } from "@/api/modules/site/list";
|
||||
import { openAdminArtifact } from "@/utils/adminArtifact";
|
||||
import DomainExternalSeoSnapshotRunDialog from "./domainExternalSeoSnapshotRunDialog.vue";
|
||||
|
||||
const visible = ref(false);
|
||||
@@ -215,8 +216,7 @@ const formatDateTime = (value?: string) => {
|
||||
};
|
||||
|
||||
const openSummaryFile = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${import.meta.env.VITE_API_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const openSnapshotRunsDialog = () => {
|
||||
|
||||
@@ -71,15 +71,14 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoSearchConsoleFetchData>({});
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadSummary = async () => {
|
||||
|
||||
@@ -66,17 +66,16 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/site/list";
|
||||
import { getDomainExternalSeoSearchConsoleProviderPlan } from "@/api/modules/site/list";
|
||||
import { openAdminArtifact } from "@/utils/adminArtifact";
|
||||
import DomainExternalSeoSearchConsoleFetchDialog from "./domainExternalSeoSearchConsoleFetchDialog.vue";
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL as string;
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoSearchConsoleProviderPlanData>({});
|
||||
const fetchDialogRef = ref();
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const openFetchDialog = () => {
|
||||
|
||||
@@ -62,15 +62,14 @@ import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoSiteQueryProbeData>({});
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const alertTitle = computed(() => {
|
||||
|
||||
@@ -130,16 +130,15 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const rows = ref<SiteList.DomainExternalSeoSnapshotRunItem[]>([]);
|
||||
const summary = ref<SiteList.DomainExternalSeoSnapshotRunData | null>(null);
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadRuns = async () => {
|
||||
|
||||
@@ -137,16 +137,15 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const rows = ref<SiteList.DomainExternalSeoSnapshotValidateRunItem[]>([]);
|
||||
const summary = ref<SiteList.DomainExternalSeoSnapshotValidateRunData | null>(null);
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const loadRuns = async () => {
|
||||
|
||||
@@ -81,13 +81,13 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/site/list";
|
||||
import { getDomainExternalSeoSummary } from "@/api/modules/site/list";
|
||||
import { openAdminArtifact } from "@/utils/adminArtifact";
|
||||
import DomainExternalSeoFailedQueueDialog from "./domainExternalSeoFailedQueueDialog.vue";
|
||||
import DomainExternalSeoTrendDialog from "./domainExternalSeoTrendDialog.vue";
|
||||
import DomainExternalSeoRealSummaryDialog from "./domainExternalSeoRealSummaryDialog.vue";
|
||||
import DomainExternalSeoProviderDialog from "./domainExternalSeoProviderDialog.vue";
|
||||
import DomainExternalSeoSearchConsolePlanDialog from "./domainExternalSeoSearchConsolePlanDialog.vue";
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL as string;
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const rows = ref<SiteList.DomainExternalSeoHostItem[]>([]);
|
||||
@@ -99,10 +99,7 @@ const providerDialogRef = ref();
|
||||
const searchConsolePlanDialogRef = ref();
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const trendLabel = () => {
|
||||
|
||||
@@ -72,15 +72,14 @@ import { ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { SiteList } from "@/api/interface/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 loading = ref(false);
|
||||
const summary = ref<SiteList.DomainExternalSeoTrendData>({});
|
||||
|
||||
const openPublicPath = (path?: string) => {
|
||||
if (!path) return;
|
||||
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
|
||||
void openAdminArtifact(path);
|
||||
};
|
||||
|
||||
const trendLabel = () => {
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<el-button type="warning" plain @click="runForceGenerate">强制重生成</el-button>
|
||||
<el-button plain :disabled="!lastJobId" @click="reopenLastTask">继续查看任务</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>
|
||||
|
||||
<el-alert
|
||||
:title="giteeTitle"
|
||||
:description="giteeDescription"
|
||||
:type="summary.gitee?.configured ? 'success' : 'warning'"
|
||||
:title="remoteTitle"
|
||||
:description="remoteDescription"
|
||||
:type="remoteConfigured ? 'success' : 'warning'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
@@ -20,10 +20,15 @@
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<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="GitHub 根目录">{{ summary.github?.root || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="仓库">
|
||||
{{ summary.gitee?.owner && summary.gitee?.repo ? `${summary.gitee.owner}/${summary.gitee.repo}` : "-" }}
|
||||
</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="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?.detail_limit_per_host || 0 }} / {{ summary.runtime_config?.forge_limit_per_host || 0 }}
|
||||
@@ -35,6 +40,9 @@
|
||||
<el-descriptions-item label="配置来源">
|
||||
Gitee开关 {{ summary.gitee?.sources?.enabled || "-" }} / 根目录 {{ summary.gitee?.sources?.root || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="GitHub来源">
|
||||
GitHub开关 {{ summary.github?.sources?.enabled || "-" }} / 根目录 {{ summary.github?.sources?.root || "-" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-alert
|
||||
@@ -48,7 +56,7 @@
|
||||
|
||||
<el-alert
|
||||
title="推荐配置方式:蜘蛛池MD 相关配置统一走后台系统配置"
|
||||
description="可后台维护:SPIDER_MD_MAX_LINKS_PER_FILE、各类链接上限、Gitee 开关/owner/repo/branch/root/token;token 在运行摘要中会自动打码,不直接回显。"
|
||||
description="可后台维护:SPIDER_MD_MAX_LINKS_PER_FILE、各类链接上限、Gitee / GitHub 开关、owner、repo、branch、root、token;token 在运行摘要中会自动打码,不直接回显。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
@@ -60,24 +68,52 @@
|
||||
<template #default="scope">
|
||||
<div class="spider-md-dialog__expand">
|
||||
<el-empty
|
||||
v-if="!(scope.row.gitee_files || []).length"
|
||||
:description="scope.row.gitee_push_status === 'failed' || scope.row.gitee_push_status === 'partial_failed'
|
||||
? `Gitee 推送失败:${scope.row.gitee_push_error || '请查看任务日志'}`
|
||||
: '当前这轮还没有可复制的 Gitee 链接'"
|
||||
v-if="!hasRemoteFiles(scope.row)"
|
||||
:description="emptyRemoteDescription(scope.row)"
|
||||
/>
|
||||
<template v-else>
|
||||
<div class="spider-md-dialog__expand-toolbar">
|
||||
<el-button size="small" type="primary" plain @click="copyAllHtmlLinks(scope.row.gitee_files || [])">批量复制页面链接</el-button>
|
||||
<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__provider-title">Gitee 链接</div>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
@click="copyAllMdLinks(remoteMdFiles(scope.row, 'gitee'), 'gitee')"
|
||||
>
|
||||
一键复制 Gitee MD
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="remoteMdFiles(scope.row, 'gitee')" 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, "gitee") }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</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>
|
||||
<el-table :data="scope.row.gitee_files || []" border size="small">
|
||||
<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="操作" width="140" fixed="right">
|
||||
<template #default="fileScope">
|
||||
<el-button link type="primary" @click="copyText(fileScope.row.html_url)">复制页面链接</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -97,21 +133,20 @@
|
||||
聚合 {{ scope.row.aggregate_count || 0 }} / 详情 {{ scope.row.detail_count || 0 }} / 泛链 {{ scope.row.forge_count || 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="推送状态" min-width="110">
|
||||
<el-table-column label="推送状态" min-width="180">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
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')"
|
||||
>
|
||||
{{
|
||||
scope.row.gitee_push_status === 'success'
|
||||
? "已推送"
|
||||
: scope.row.gitee_push_status === 'failed'
|
||||
? "推送失败"
|
||||
: scope.row.gitee_push_status === 'partial_failed'
|
||||
? "部分失败"
|
||||
: "仅本地"
|
||||
}}
|
||||
Gitee:{{ statusLabel(remoteStatus(scope.row, "gitee")) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
size="small"
|
||||
style="margin-left:8px;"
|
||||
: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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -120,21 +155,34 @@
|
||||
<el-tag size="small" type="success">本地成功</el-tag>
|
||||
<el-tag
|
||||
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;"
|
||||
>
|
||||
{{
|
||||
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>
|
||||
</template>
|
||||
</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>
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -165,20 +213,34 @@ const formatDateTime = (value?: string) => {
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||
};
|
||||
|
||||
const giteeTitle = computed(() => {
|
||||
if (summary.value.gitee?.configured) {
|
||||
return "当前已配置 Gitee 自动推送,生成后会自动返回可复制链接";
|
||||
}
|
||||
if (summary.value.gitee?.enabled) {
|
||||
return "Gitee 已启用但未配完整,当前只能保留本地文件";
|
||||
}
|
||||
return "当前未启用 Gitee 推送,生成后仅保留本地文件";
|
||||
type RemoteProvider = "gitee" | "github";
|
||||
|
||||
const remoteConfigured = computed(() => {
|
||||
return Boolean(summary.value.gitee?.configured || summary.value.github?.configured);
|
||||
});
|
||||
|
||||
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 repo = gitee.owner && gitee.repo ? `${gitee.owner}/${gitee.repo}` : "未配置仓库";
|
||||
return `仓库:${repo};分支:${gitee.branch || "-"};根目录:${gitee.root || "-"}`;
|
||||
const github = summary.value.github || {};
|
||||
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(() => {
|
||||
@@ -194,19 +256,76 @@ const latestRunDescription = computed(() => {
|
||||
const fileCount = Number(row.file_count || 0);
|
||||
const totalLinks = Number(row.total_links || 0);
|
||||
const runRoot = String(row.run_root || "").trim() || "-";
|
||||
const pushStatus = String(row.gitee_push_status || "").trim();
|
||||
const pushError = String(row.gitee_push_error || "").trim();
|
||||
let pushed = "当前仅保留本地";
|
||||
if (pushStatus === "success") {
|
||||
pushed = "已推送到 Gitee";
|
||||
} else if (pushStatus === "failed") {
|
||||
pushed = "本地生成成功,Gitee 推送失败";
|
||||
} else if (pushStatus === "partial_failed") {
|
||||
pushed = "本地生成成功,Gitee 部分推送后失败";
|
||||
}
|
||||
return `目录:${runRoot};文件数:${fileCount};链接数:${totalLinks};状态:${pushed}${pushError ? `;错误:${pushError}` : ""}`;
|
||||
const githubStatus = String(row.github_push_status || "").trim();
|
||||
const githubError = String(row.github_push_error || "").trim();
|
||||
const giteeStatus = String(row.gitee_push_status || "").trim();
|
||||
const giteeError = String(row.gitee_push_error || "").trim();
|
||||
const statusParts: string[] = [];
|
||||
const errorParts: string[] = [];
|
||||
|
||||
statusParts.push(`Gitee ${statusLabel(giteeStatus)}`);
|
||||
statusParts.push(`GitHub ${statusLabel(githubStatus)}`);
|
||||
if (giteeError) errorParts.push(`Gitee:${giteeError}`);
|
||||
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 res = await getDomainSpiderMdSummary({ limit: 10 });
|
||||
summary.value = res.data || {};
|
||||
@@ -227,15 +346,31 @@ const copyText = async (text?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const copyAllHtmlLinks = async (items: Array<{ html_url?: string }>) => {
|
||||
const lines = (items || []).map(item => String(item?.html_url || "").trim()).filter(Boolean);
|
||||
const copyAllMdLinks = async (items: Array<{ download_url?: string; html_url?: string }>, provider: RemoteProvider) => {
|
||||
const lines = (items || [])
|
||||
.map(item => getRemoteLink(item, provider))
|
||||
.filter(Boolean);
|
||||
if (!lines.length) {
|
||||
ElMessage.warning("当前没有可复制的页面链接");
|
||||
ElMessage.warning("当前没有可复制的MD链接");
|
||||
return;
|
||||
}
|
||||
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 res = await runDomainSpiderMdGenerate({});
|
||||
const jobId = String(res.data?.job?.job_id || "").trim();
|
||||
@@ -315,7 +450,19 @@ defineExpose({
|
||||
|
||||
.spider-md-dialog__expand-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
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>
|
||||
|
||||
@@ -113,6 +113,193 @@
|
||||
<BootstrapEnvDialog v-if="dialogMount.bootstrapEnvDialog" ref="bootstrapEnvDialogRef" />
|
||||
<DomainSupplyDialog v-if="dialogMount.domainSupplyDialog" ref="domainSupplyDialogRef" />
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -280,6 +467,17 @@ const domainSpiderMdDialogRef = ref();
|
||||
const domainSeoCopyWorkbenchDialogRef = ref();
|
||||
const route = useRoute();
|
||||
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 = () => {
|
||||
void ensureDialogMounted("importDialog", () => {
|
||||
const params = {
|
||||
@@ -444,6 +642,73 @@ const openPublicPath = (path?: string) => {
|
||||
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 inserted = payload?.inserted_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 arrLines = [
|
||||
`<p>新增站点:<strong>${inserted}</strong></p>`,
|
||||
probeProcessed > 0 ? `<p>自动探测:<strong>${probeProcessed}</strong>,通过 <strong>${probePassed}</strong>,失败 <strong>${probeFailed}</strong></p>` : "",
|
||||
`<p>本次新增站点:<strong>${inserted}</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>` : "",
|
||||
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>` : "",
|
||||
@@ -464,6 +730,9 @@ const showDomainImportSummary = async (payload: SiteList.DomainUploadSummary) =>
|
||||
].filter(Boolean);
|
||||
|
||||
if (!arrLines.length) {
|
||||
if (payload?.post_import_workflow?.enabled) {
|
||||
openPostImportWorkflowDialog(payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -482,6 +751,10 @@ const showDomainImportSummary = async (payload: SiteList.DomainUploadSummary) =>
|
||||
openPublicPath(summaryJsonPath);
|
||||
}
|
||||
}).catch(() => undefined);
|
||||
|
||||
if (payload?.post_import_workflow?.enabled) {
|
||||
openPostImportWorkflowDialog(payload);
|
||||
}
|
||||
};
|
||||
|
||||
const initParam = reactive({
|
||||
@@ -1530,4 +1803,62 @@ watch(
|
||||
.tagTextColor {
|
||||
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>
|
||||
|
||||
@@ -22,7 +22,13 @@
|
||||
<el-input v-model="e.value" :disabled="e.disabled" :placeholder="`请输入${e.label}`" clearable></el-input>
|
||||
</template>
|
||||
<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 v-if="e.type === 'textarea'">
|
||||
<el-input
|
||||
@@ -43,6 +49,15 @@
|
||||
description="平时样式或脚本改动会自动重编;只有当前端站群、CDN 或浏览器还在命中旧静态文件时,再手动提高这个版本号,强制全站切到新静态资源。"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert
|
||||
v-if="isSeoAiRuleConfig"
|
||||
class="config-helper-alert"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
title="当前为 AI 文案最高规则配置"
|
||||
description="这里修改后,会直接影响 OpenAI 实时生成和批处理/Codex 导出提示词。建议先改规则文案,再做小批量验证,不要一次性改动多条执行口径。"
|
||||
show-icon
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="drawerVisible = false">取消</el-button>
|
||||
@@ -69,8 +84,11 @@ type Fields = {
|
||||
required: boolean;
|
||||
options?: Options[];
|
||||
disabled?: boolean;
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
const isSeoAiRuleCode = (code?: string) => String(code || "").startsWith("SEO_COPY_AI_RULE_");
|
||||
|
||||
/**
|
||||
* 解析系统配置字段的校验提示语。
|
||||
*/
|
||||
@@ -113,6 +131,9 @@ const formColumns = (): Fields[] => {
|
||||
label: "配置值",
|
||||
prop: "sc_val",
|
||||
type: "text-tarea",
|
||||
get rows() {
|
||||
return isSeoAiRuleCode(drawerProps.value.row?.sc_code) ? 16 : 5;
|
||||
},
|
||||
get value() {
|
||||
return drawerProps.value.row!.sc_val;
|
||||
},
|
||||
@@ -154,6 +175,7 @@ const drawerProps = ref<DrawerProps>({
|
||||
row: {}
|
||||
});
|
||||
const isStaticFileVersion = computed(() => drawerProps.value.row?.sc_code === "STATIC_FILE_VERSION");
|
||||
const isSeoAiRuleConfig = computed(() => isSeoAiRuleCode(drawerProps.value.row?.sc_code));
|
||||
|
||||
const acceptParams = (params: DrawerProps) => {
|
||||
drawerProps.value = {
|
||||
|
||||
@@ -1,12 +1,46 @@
|
||||
<template>
|
||||
<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
|
||||
ref="proTable"
|
||||
highlight-current-row
|
||||
:columns="columns"
|
||||
:request-api="getTableList"
|
||||
: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>
|
||||
<el-empty description="暂无系统配置" />
|
||||
</template>
|
||||
@@ -19,6 +53,17 @@
|
||||
<el-tag v-if="scope.row.sc_code === 'STATIC_FILE_VERSION'" size="small" type="warning" effect="plain">
|
||||
缓存兜底
|
||||
</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>
|
||||
</template>
|
||||
</ProTable>
|
||||
@@ -36,12 +81,16 @@ import { getConfigList, SaveApi } from "@/api/modules/system/config";
|
||||
|
||||
import Drawer from "./drawer.vue";
|
||||
const proTable = ref<ProTableInstance>();
|
||||
const activeQuickKey = ref("");
|
||||
|
||||
const initParam = reactive({
|
||||
limit: 25,
|
||||
page: 1
|
||||
page: 1,
|
||||
key: ""
|
||||
});
|
||||
|
||||
const isSeoAiRuleCode = (code?: string) => String(code || "").startsWith("SEO_COPY_AI_RULE_");
|
||||
|
||||
const dataCallback = (data: any) => {
|
||||
return {
|
||||
items: data.items,
|
||||
@@ -55,9 +104,23 @@ const getTableList = (params: any) => {
|
||||
let newParams = JSON.parse(JSON.stringify(params));
|
||||
initParam.page = newParams.page;
|
||||
initParam.limit = newParams.limit;
|
||||
initParam.key = newParams.key ?? "";
|
||||
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[]>([
|
||||
{
|
||||
label: "配置编码 / ID",
|
||||
@@ -112,10 +175,32 @@ const openDrawer = (row: Partial<ConfigList.ResList> = {}) => {
|
||||
.el-table .success-row .el-table-fixed-column--left {
|
||||
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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.config-description-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
254
src/views/system/config/seoAiRules.vue
Normal file
254
src/views/system/config/seoAiRules.vue
Normal 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>
|
||||
496
src/views/video/metadataMissing/index.vue
Normal file
496
src/views/video/metadataMissing/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user