fix: remove Identity processor option and deprecated trace_chunks
- Removed Identity checkbox from processor selection in context menu - Removed trace_chunks from PostgreSQL section (deprecated) - Added relationship_chunks to PostgreSQL section
This commit is contained in:
@@ -90,7 +90,11 @@
|
||||
<div class="ms-fm-badge-row" :class="ingestedBadgeClass(f)">{{ ingestedBadgeIcon(f) }} {{ ingestedBadgeLabel(f) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="getProgress(f.file_uuid)?.overall_progress < 100" class="ms-fm-progress">
|
||||
<div v-if="getPipelineStats(f.file_uuid) && getPipelineStats(f.file_uuid).overall_progress < 1.0" class="ms-fm-progress">
|
||||
<div class="ms-fm-progress-bar" :style="{ width: (getPipelineStats(f.file_uuid).overall_progress * 100) + '%' }"></div>
|
||||
<div class="ms-fm-progress-text">{{ Math.round(getPipelineStats(f.file_uuid).overall_progress * 100) }}%</div>
|
||||
</div>
|
||||
<div v-else-if="getProgress(f.file_uuid) && getProgress(f.file_uuid).overall_progress < 100" class="ms-fm-progress">
|
||||
<div class="ms-fm-progress-bar" :style="{ width: getProgress(f.file_uuid)?.overall_progress + '%' }"></div>
|
||||
<div class="ms-fm-progress-text">{{ getProgress(f.file_uuid)?.overall_progress || 0 }}%</div>
|
||||
</div>
|
||||
@@ -108,7 +112,7 @@
|
||||
<div class="ms-ctx-filename">{{ ctxMenu.file?.file_name }}</div>
|
||||
<div class="ms-ctx-info">{{ formatSize(ctxMenu.file?.file_size || 0) }} · {{ formatDate(ctxMenu.file?.modified_time || '') }}</div>
|
||||
<div class="ms-ctx-info">狀態:<span :class="ctxMenu.file?.status === 'completed' ? 'ms-ctx-done' : ctxMenu.file?.status === 'registered' ? 'ms-ctx-pending' : 'ms-ctx-none'">{{ statusLabel(ctxMenu.file) }}</span></div>
|
||||
<div v-if="ctxMenu.file?.isRegistered && ctxMenu.file?.file_uuid" class="ms-ctx-info">UUID:{{ ctxMenu.file.file_uuid?.slice(0, 12) }}...</div>
|
||||
<div v-if="ctxMenu.file?.isRegistered && ctxMenu.file?.file_uuid" class="ms-ctx-info">UUID:{{ ctxMenu.file.file_uuid }}</div>
|
||||
</div>
|
||||
<hr class="ms-ctx-divider">
|
||||
<button v-if="!ctxMenu.file?.isRegistered" class="ms-ctx-item" @click="ctxRegister(ctxMenu.file)">📥 註冊此檔案</button>
|
||||
@@ -124,13 +128,115 @@
|
||||
<div class="ms-ctx-procs-title">處理器選擇</div>
|
||||
<label class="ms-fm-check-label" :class="procStatusClass('asr')"><input type="checkbox" v-model="procAsr"> ASR <span class="ms-proc-count">{{ procCountLabel('asr', 'segment') }}</span></label>
|
||||
<label class="ms-fm-check-label" :class="procStatusClass('asrx')"><input type="checkbox" v-model="procAsrx"> ASRX <span class="ms-proc-count">{{ procCountLabel('asrx', 'segment') }}</span></label>
|
||||
<label class="ms-fm-check-label" :class="procStatusClass('yolo')"><input type="checkbox" v-model="procYolo"> YOLO <span class="ms-proc-count">{{ procCountLabel('yolo', 'frame') }}</span></label>
|
||||
<label class="ms-fm-check-label" :class="procStatusClass('face')"><input type="checkbox" v-model="procFace"> Face <span class="ms-proc-count">{{ procCountLabel('face', 'frame') }}</span></label>
|
||||
<label class="ms-fm-check-label" :class="procStatusClass('ocr')"><input type="checkbox" v-model="procOcr"> OCR <span class="ms-proc-count">{{ procCountLabel('ocr', 'frame') }}</span></label>
|
||||
<label class="ms-fm-check-label" :class="procStatusClass('pose')"><input type="checkbox" v-model="procPose"> Pose <span class="ms-proc-count">{{ procCountLabel('pose', 'frame') }}</span></label>
|
||||
<label class="ms-fm-check-label" :class="procStatusClass('appearance')"><input type="checkbox" v-model="procAppearance"> Appearance <span class="ms-proc-count">{{ procCountLabel('appearance', 'frame') }}</span></label>
|
||||
<label class="ms-fm-check-label" :class="procStatusClass('cut')"><input type="checkbox" v-model="procCut"> CUT <span class="ms-proc-count">{{ procCountLabel('cut', 'frame') }}</span></label>
|
||||
</div>
|
||||
<div v-if="ctxMenu.file?.file_uuid" class="ms-ctx-progress">
|
||||
<div v-if="statsLoading" class="ms-ctx-loading">
|
||||
<div class="ms-ctx-spinner"></div>
|
||||
<span class="ms-ctx-loading-text">載入中...</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="ms-ctx-overall-progress" v-if="getPipelineStats(ctxMenu.file.file_uuid)">
|
||||
<div class="ms-ctx-progress-title">整體進度</div>
|
||||
<div class="ms-ctx-progress-bar-wrap">
|
||||
<div class="ms-ctx-progress-bar" :style="{ width: formatPipelineProgress(getPipelineStats(ctxMenu.file.file_uuid).overall_progress) }"></div>
|
||||
<span class="ms-ctx-progress-text">{{ formatPipelineProgress(getPipelineStats(ctxMenu.file.file_uuid).overall_progress) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="getPipelineStats(ctxMenu.file.file_uuid)" class="ms-ctx-stages">
|
||||
<div v-for="stage in getPipelineStats(ctxMenu.file.file_uuid).stages" :key="stage.name" class="ms-ctx-stage">
|
||||
<div class="ms-ctx-stage-header">
|
||||
<span class="ms-ctx-stage-icon" :style="{ color: getStageColor(stage.status) }">{{ getStageIcon(stage.status) }}</span>
|
||||
<span class="ms-ctx-stage-name">{{ formatStageName(stage.name) }}</span>
|
||||
<span class="ms-ctx-stage-weight">{{ Math.round(stage.weight * 100) }}%</span>
|
||||
<span class="ms-ctx-stage-progress">{{ formatPipelineProgress(stage.progress) }}</span>
|
||||
</div>
|
||||
<div class="ms-ctx-stage-bar-wrap">
|
||||
<div class="ms-ctx-stage-bar" :style="{ width: formatPipelineProgress(stage.progress), backgroundColor: getStageColor(stage.status) }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="getFileStats(ctxMenu.file.file_uuid)" class="ms-ctx-details">
|
||||
<div class="ms-ctx-phase" v-if="getFileStats(ctxMenu.file.file_uuid).json?.processors">
|
||||
<div class="ms-ctx-phase-title">Rule1: 處理器輸出</div>
|
||||
<div v-for="(info, proc) in getFileStats(ctxMenu.file.file_uuid).json.processors" :key="proc" class="ms-ctx-proc-item">
|
||||
<span class="ms-ctx-proc-name">{{ proc.toUpperCase() }}</span>
|
||||
<span class="ms-ctx-proc-count">{{ formatProcCount(proc, info) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-ctx-phase" v-if="getFileStats(ctxMenu.file.file_uuid).postgresql">
|
||||
<div class="ms-ctx-phase-title">Rule1: PostgreSQL 入庫</div>
|
||||
<div class="ms-ctx-stat-item">
|
||||
<span class="ms-ctx-proc-name">Sentence Chunks</span>
|
||||
<span class="ms-ctx-proc-count">{{ getFileStats(ctxMenu.file.file_uuid).postgresql.sentence_chunks }}</span>
|
||||
</div>
|
||||
<div class="ms-ctx-stat-item">
|
||||
<span class="ms-ctx-proc-name">Relationship Chunks</span>
|
||||
<span class="ms-ctx-proc-count">{{ getFileStats(ctxMenu.file.file_uuid).postgresql.relationship_chunks }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-ctx-phase" v-if="getFileStats(ctxMenu.file.file_uuid).qdrant">
|
||||
<div class="ms-ctx-phase-title">Rule1: Qdrant 入庫</div>
|
||||
<div class="ms-ctx-stat-item">
|
||||
<span class="ms-ctx-proc-name">Text Chunks</span>
|
||||
<span class="ms-ctx-proc-count">{{ getFileStats(ctxMenu.file.file_uuid).qdrant.text_chunks }}</span>
|
||||
</div>
|
||||
<div class="ms-ctx-stat-item">
|
||||
<span class="ms-ctx-proc-name">Face Points</span>
|
||||
<span class="ms-ctx-proc-count">{{ getFileStats(ctxMenu.file.file_uuid).qdrant.faces }}</span>
|
||||
</div>
|
||||
<div class="ms-ctx-stat-item">
|
||||
<span class="ms-ctx-proc-name">Speakers</span>
|
||||
<span class="ms-ctx-proc-count">{{ getFileStats(ctxMenu.file.file_uuid).qdrant.speakers }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-ctx-phase" v-if="getFileStats(ctxMenu.file.file_uuid).tkg?.nodes">
|
||||
<div class="ms-ctx-phase-title">Rule2: TKG Nodes</div>
|
||||
<div v-for="(count, type) in getFileStats(ctxMenu.file.file_uuid).tkg.nodes" :key="type" class="ms-ctx-proc-item">
|
||||
<span class="ms-ctx-proc-name">{{ formatNodeName(type) }}</span>
|
||||
<span class="ms-ctx-proc-count">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-ctx-phase" v-if="getFileStats(ctxMenu.file.file_uuid).tkg?.edges">
|
||||
<div class="ms-ctx-phase-title">Rule2: TKG Edges</div>
|
||||
<div v-for="(count, type) in getFileStats(ctxMenu.file.file_uuid).tkg.edges" :key="type" class="ms-ctx-proc-item">
|
||||
<span class="ms-ctx-proc-name">{{ formatEdgeName(type) }}</span>
|
||||
<span class="ms-ctx-proc-count">{{ count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-ctx-phase" v-if="getFileStats(ctxMenu.file.file_uuid).identity_agent">
|
||||
<div class="ms-ctx-phase-title">Identity Agent</div>
|
||||
<div class="ms-ctx-stat-item">
|
||||
<span class="ms-ctx-proc-name">Identities Created</span>
|
||||
<span class="ms-ctx-proc-count">{{ getFileStats(ctxMenu.file.file_uuid).identity_agent.identities_created }}</span>
|
||||
</div>
|
||||
<div class="ms-ctx-stat-item">
|
||||
<span class="ms-ctx-proc-name">TMDB Matches</span>
|
||||
<span class="ms-ctx-proc-count">{{ getFileStats(ctxMenu.file.file_uuid).identity_agent.tmdb_matches }}</span>
|
||||
</div>
|
||||
<div class="ms-ctx-stat-item">
|
||||
<span class="ms-ctx-proc-name">Speaker Bindings</span>
|
||||
<span class="ms-ctx-proc-count">{{ getFileStats(ctxMenu.file.file_uuid).identity_agent.speaker_bindings }}</span>
|
||||
</div>
|
||||
<div class="ms-ctx-stat-item">
|
||||
<span class="ms-ctx-proc-name">Confirmations</span>
|
||||
<span class="ms-ctx-proc-count">{{ getFileStats(ctxMenu.file.file_uuid).identity_agent.confirmations }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +247,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { apiCall } from '@/api'
|
||||
import { ensureFiles, filesCache, filesLoaded, thumbnailsCache, loadThumbnail, invalidateFiles, processorCountsCache, loadProcessorCounts, getProcessorCounts, ProcessorOutputInfo, pollProgress, stopPolling, getProgress, getFileStatus, refreshFileStatus, refreshAllFilesStatus, fileProgressCache, fileStatusCache } from '@/store'
|
||||
import { ensureFiles, filesCache, filesLoaded, thumbnailsCache, loadThumbnail, invalidateFiles, processorCountsCache, loadProcessorCounts, getProcessorCounts, ProcessorOutputInfo, pollProgress, stopPolling, getProgress, getFileStatus, refreshFileStatus, refreshAllFilesStatus, fileProgressCache, fileStatusCache, pollingProgressSet, getFileProcessingStatus, ProcessingStatus, ProcessingLevel, loadPipelineStats, getPipelineStats, PipelineStats, loadFileStats, getFileStats, FileStats, pollPipelineProgress, startStatsAutoRefresh, stopStatsAutoRefresh, JsonProcessorInfo, pipelineStatsCache } from '@/store'
|
||||
import VideoPlayer from '../components/VideoPlayer.vue'
|
||||
|
||||
const files = computed(() => filesCache.value)
|
||||
@@ -235,8 +341,8 @@ const completedCount = computed(() => files.value.filter((f: any) => f.status ==
|
||||
const ingestedCount = computed(() => files.value.filter((f: any) => f.ingested).length)
|
||||
const notIngestedCount = computed(() => files.value.filter((f: any) => f.isRegistered && !f.ingested).length)
|
||||
|
||||
onMounted(() => { loadFiles(); document.addEventListener('click', docClickClose) })
|
||||
onUnmounted(() => { document.removeEventListener('click', docClickClose) })
|
||||
onMounted(() => { loadFiles(); document.addEventListener('click', docClickClose); startStatsAutoRefresh() })
|
||||
onUnmounted(() => { document.removeEventListener('click', docClickClose); stopStatsAutoRefresh() })
|
||||
|
||||
function docClickClose(e: MouseEvent) {
|
||||
if (e.target instanceof Element && e.target.closest('.ms-ctx-menu')) return
|
||||
@@ -251,19 +357,24 @@ async function loadFiles() {
|
||||
for (const f of files.value.slice(0, 6)) {
|
||||
if ((isPhoto(f) || isVideo(f)) && f.file_uuid) loadThumbnail(f.file_uuid)
|
||||
}
|
||||
const registeredFiles = files.value.filter((f: any) => f.isRegistered && f.file_uuid && f.status !== 'completed')
|
||||
const registeredFiles = files.value.filter((f: any) => f.isRegistered && f.file_uuid)
|
||||
for (const f of registeredFiles) {
|
||||
try {
|
||||
const progress: any = await apiCall('get_progress', { fileUuid: f.file_uuid })
|
||||
if (progress && progress.overall_progress < 100 && progress.processors?.some((p: any) => p.status === 'running')) {
|
||||
// 强制重新加载,忽略缓存
|
||||
const pipeline = await loadPipelineStats(f.file_uuid, true)
|
||||
console.log('[loadFiles] pipeline result', f.file_uuid, pipeline)
|
||||
|
||||
if (pipeline && pipeline.overall_progress < 1.0 && pipeline.stages?.some((s: any) => s.status === 'running')) {
|
||||
f.status = 'processing'
|
||||
fileProgressCache.value[f.file_uuid] = {
|
||||
file_uuid: f.file_uuid,
|
||||
overall_progress: progress.overall_progress,
|
||||
processors: progress.processors,
|
||||
}
|
||||
pollProgress(f.file_uuid)
|
||||
fileStatusCache.value[f.file_uuid] = 'processing'
|
||||
pollPipelineProgress(f.file_uuid)
|
||||
} else if (pipeline && pipeline.overall_progress >= 1.0) {
|
||||
f.status = 'completed'
|
||||
fileStatusCache.value[f.file_uuid] = 'completed'
|
||||
}
|
||||
|
||||
// 同时加载文件统计(包含降级逻辑)
|
||||
await loadFileStats(f.file_uuid, true)
|
||||
} catch (e) {
|
||||
console.error('check progress failed:', f.file_uuid, e)
|
||||
}
|
||||
@@ -326,7 +437,6 @@ const unregistering = ref(false)
|
||||
const actionMsg = ref('')
|
||||
const procAsr = ref(true)
|
||||
const procAsrx = ref(true)
|
||||
const procYolo = ref(true)
|
||||
const procFace = ref(true)
|
||||
const procOcr = ref(true)
|
||||
const procPose = ref(true)
|
||||
@@ -334,12 +444,72 @@ const procAppearance = ref(true)
|
||||
const procCut = ref(true)
|
||||
|
||||
const ctxMenu = ref<{ show: boolean; x: number; y: number; file: any }>({ show: false, x: 0, y: 0, file: null })
|
||||
const statsLoading = ref(false)
|
||||
|
||||
function formatProcCount(proc: string, info: JsonProcessorInfo): string {
|
||||
if (info.segment_count != null) return `${info.segment_count} segments`
|
||||
if (info.frame_count != null) return `${info.frame_count} frames`
|
||||
if (info.chunk_count != null) return `${info.chunk_count} chunks`
|
||||
return info.status
|
||||
}
|
||||
|
||||
function formatNodeName(type: string): string {
|
||||
const names: Record<string, string> = {
|
||||
character: 'Character', object: 'Object', scene: 'Scene',
|
||||
action: 'Action', location: 'Location', event: 'Event',
|
||||
dialogue: 'Dialogue', emotion: 'Emotion', relation: 'Relation'
|
||||
}
|
||||
return names[type] || type
|
||||
}
|
||||
|
||||
function formatEdgeName(type: string): string {
|
||||
const names: Record<string, string> = {
|
||||
appear_in: 'Appear_In', interact_with: 'Interact_With',
|
||||
located_at: 'Located_At', perform_action: 'Perform_Action',
|
||||
cause_effect: 'Cause_Effect', temporal_link: 'Temporal_Link',
|
||||
speak_to: 'Speak_To', feel: 'Feel'
|
||||
}
|
||||
return names[type] || type
|
||||
}
|
||||
|
||||
function formatPipelineProgress(progress: number): string {
|
||||
return `${Math.round(progress * 100)}%`
|
||||
}
|
||||
|
||||
function getStageIcon(status: string): string {
|
||||
switch (status) {
|
||||
case 'completed': return '✓'
|
||||
case 'running': return '⟳'
|
||||
case 'failed': return '✗'
|
||||
default: return '○'
|
||||
}
|
||||
}
|
||||
|
||||
function formatStageName(name: string): string {
|
||||
const names: Record<string, string> = {
|
||||
processors: '處理器',
|
||||
rule1_ingestion: 'Rule1 入庫',
|
||||
face_tracing: 'Face Tracing',
|
||||
tkg_nodes: 'TKG Nodes',
|
||||
tkg_edges: 'TKG Edges',
|
||||
rule2_ingestion: 'Rule2 入庫'
|
||||
}
|
||||
return names[name] || name
|
||||
}
|
||||
|
||||
function getStageColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'completed': return '#34a853'
|
||||
case 'running': return '#4285f4'
|
||||
case 'failed': return '#ea4335'
|
||||
default: return '#dadce0'
|
||||
}
|
||||
}
|
||||
|
||||
function selectedProcessors(): string[] {
|
||||
const procs: string[] = []
|
||||
if (procAsr.value) procs.push('asr')
|
||||
if (procAsrx.value) procs.push('asrx')
|
||||
if (procYolo.value) procs.push('yolo')
|
||||
if (procFace.value) procs.push('face')
|
||||
if (procOcr.value) procs.push('ocr')
|
||||
if (procPose.value) procs.push('pose')
|
||||
@@ -369,6 +539,12 @@ function procCountLabel(proc: string, type: 'frame' | 'segment' | 'chunk'): stri
|
||||
|
||||
function statusLabel(f: any): string {
|
||||
if (!f) return ''
|
||||
// 优先使用缓存的状态
|
||||
const cachedStatus = getFileStatus(f.file_uuid)
|
||||
if (cachedStatus === 'completed') return '已完成'
|
||||
if (cachedStatus === 'processing') return '處理中'
|
||||
|
||||
// 否则使用文件对象的状态
|
||||
if (f.status === 'completed') return '已完成'
|
||||
if (f.status === 'processing') return '處理中'
|
||||
if (f.status === 'failed') return '處理失敗'
|
||||
@@ -426,12 +602,23 @@ async function openContextMenu(e: MouseEvent, f: any) {
|
||||
e.stopPropagation()
|
||||
ctxMenu.value = {
|
||||
show: true,
|
||||
x: Math.min(e.clientX, window.innerWidth - 280),
|
||||
y: Math.min(e.clientY, window.innerHeight - 320),
|
||||
x: Math.min(e.clientX, window.innerWidth - 360),
|
||||
y: Math.min(e.clientY, window.innerHeight * 0.8),
|
||||
file: f
|
||||
}
|
||||
if (f.isRegistered && f.file_uuid) {
|
||||
await loadProcessorCounts(f.file_uuid, true)
|
||||
statsLoading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
loadPipelineStats(f.file_uuid, true),
|
||||
loadFileStats(f.file_uuid, true),
|
||||
loadProcessorCounts(f.file_uuid, true)
|
||||
])
|
||||
} catch (e) {
|
||||
console.error('Failed to load stats:', e)
|
||||
} finally {
|
||||
statsLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,8 +629,6 @@ function closeContextMenu() {
|
||||
async function ctxRegister(f: any) {
|
||||
closeContextMenu()
|
||||
if (registering.value) return
|
||||
const procs = selectedProcessors()
|
||||
if (!procs.length) { actionMsg.value = '請至少選擇一個處理器'; return }
|
||||
registering.value = true
|
||||
actionMsg.value = '註冊中...'
|
||||
const filePath = f.file_path || f.relative_path || f.file_name
|
||||
@@ -452,12 +637,9 @@ async function ctxRegister(f: any) {
|
||||
const result: any = await apiCall('register_file', { filePath })
|
||||
if (result.success) {
|
||||
actionMsg.value = `註冊成功:${f.file_name}`
|
||||
if (result.file_uuid) {
|
||||
try {
|
||||
const pResult: any = await apiCall('process_file', { fileUuid: result.file_uuid, processors: procs })
|
||||
if (pResult.success) actionMsg.value += ' → 處理已觸發'
|
||||
} catch (e: any) { console.error('[process]', e) }
|
||||
}
|
||||
f.isRegistered = true
|
||||
f.file_uuid = result.file_uuid
|
||||
f.status = 'registered'
|
||||
} else {
|
||||
actionMsg.value = `註冊失敗:${result.message}`
|
||||
}
|
||||
@@ -484,7 +666,7 @@ async function ctxProcess(f: any) {
|
||||
actionMsg.value = `處理已觸發:${f.file_name}`
|
||||
f.status = 'processing'
|
||||
fileStatusCache.value[f.file_uuid] = 'processing'
|
||||
pollProgress(f.file_uuid)
|
||||
pollPipelineProgress(f.file_uuid)
|
||||
} else {
|
||||
actionMsg.value = `處理失敗:${result.message}`
|
||||
}
|
||||
@@ -600,7 +782,9 @@ function resetFilters() {
|
||||
.ms-fm-action-msg { margin-left: 12px; color: #1e8e3e; font-weight: 600; }
|
||||
.ms-fm-btn-danger { background: #d93025; color: #fff; border-color: #d93025; }
|
||||
.ms-fm-btn-danger:hover:not(:disabled) { background: #c5221f; }
|
||||
.ms-ctx-menu { position: fixed !important; z-index: 99999 !important; background: #fff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,.15); padding: 6px; min-width: 220px; max-width: 320px; font-size: 13px; color: #222; }
|
||||
.ms-ctx-menu { position: fixed !important; z-index: 99999 !important; background: #fff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,.15); padding: 6px; min-width: 280px; max-width: 360px; max-height: 80vh; overflow-y: auto; font-size: 13px; color: #222; }
|
||||
.ms-ctx-menu::-webkit-scrollbar { width: 4px; }
|
||||
.ms-ctx-menu::-webkit-scrollbar-thumb { background: #dadce0; border-radius: 2px; }
|
||||
.ms-ctx-header { padding: 8px 10px 4px; }
|
||||
.ms-ctx-filename { font-weight: 600; font-size: 13px; color: #202124; word-break: break-all; }
|
||||
.ms-ctx-info { font-size: 11px; color: #7a7f87; margin-top: 2px; }
|
||||
@@ -614,6 +798,14 @@ function resetFilters() {
|
||||
.ms-ctx-danger:hover { background: #fce8e6 !important; }
|
||||
.ms-ctx-procs { padding: 6px 10px 8px; display: flex; flex-wrap: wrap; gap: 6px 10px; }
|
||||
.ms-ctx-procs-title { width: 100%; font-size: 11px; color: #7a7f87; font-weight: 600; margin-bottom: 2px; }
|
||||
.ms-ctx-progress { padding: 8px 12px; background: #f8f9fa; border-radius: 8px; margin-top: 8px; }
|
||||
.ms-ctx-progress-title { font-size: 11px; color: #5f6368; font-weight: 600; margin-bottom: 6px; }
|
||||
.ms-ctx-progress-bar-wrap { position: relative; height: 20px; background: #e8eaed; border-radius: 4px; overflow: hidden; }
|
||||
.ms-ctx-progress-bar { height: 100%; background: linear-gradient(90deg, #1a73e8, #4285f4); border-radius: 4px; transition: width 0.3s; }
|
||||
.ms-ctx-progress-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 11px; font-weight: 600; color: #202124; }
|
||||
.ms-ctx-proc-status { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; font-size: 11px; }
|
||||
.ms-ctx-proc-name { font-weight: 600; color: #1a73e8; }
|
||||
.ms-ctx-proc-detail { color: #5f6368; }
|
||||
.ms-proc-done { color: #059669; }
|
||||
.ms-proc-done .ms-proc-count { color: #059669; font-weight: 600; }
|
||||
.ms-proc-count { margin-left: 4px; font-size: 11px; color: #8a919c; }
|
||||
@@ -654,4 +846,34 @@ function resetFilters() {
|
||||
.ms-fm-axis-bar { font-size: 12px; color: #5f6368; margin: 2px 0 12px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.ms-fm-axis-item strong { color: #202124; }
|
||||
.ms-fm-axis-sep { color: #dadce0; }
|
||||
|
||||
/* 加载状态 */
|
||||
.ms-ctx-loading { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 32px 0; }
|
||||
.ms-ctx-spinner { width: 24px; height: 24px; border: 3px solid #e8eaed; border-top-color: #4285f4; border-radius: 50%; animation: spin 0.8s linear infinite; }
|
||||
.ms-ctx-loading-text { font-size: 13px; color: #5f6368; }
|
||||
|
||||
/* 进度条和阶段样式 */
|
||||
.ms-ctx-overall-progress { margin-bottom: 16px; }
|
||||
.ms-ctx-progress-bar-wrap { position: relative; height: 20px; background: #e8eaed; border-radius: 10px; overflow: hidden; }
|
||||
.ms-ctx-progress-bar { height: 100%; background: linear-gradient(90deg, #4285f4, #34a853); transition: width 0.3s ease; }
|
||||
.ms-ctx-progress-text { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); font-size: 12px; font-weight: 600; color: #333; }
|
||||
|
||||
.ms-ctx-stages { margin-bottom: 16px; }
|
||||
.ms-ctx-stage { margin-bottom: 12px; }
|
||||
.ms-ctx-stage-header { display: flex; align-items: center; gap: 6px; font-size: 13px; margin-bottom: 4px; }
|
||||
.ms-ctx-stage-icon { width: 16px; text-align: center; font-size: 12px; font-weight: 600; }
|
||||
.ms-ctx-stage-name { flex: 1; font-weight: 500; color: #333; }
|
||||
.ms-ctx-stage-weight { color: #5f6368; font-size: 12px; }
|
||||
.ms-ctx-stage-progress { color: #5f6368; font-size: 12px; font-weight: 600; }
|
||||
.ms-ctx-stage-bar-wrap { height: 6px; background: #e8eaed; border-radius: 3px; overflow: hidden; }
|
||||
.ms-ctx-stage-bar { height: 100%; transition: width 0.3s ease; }
|
||||
|
||||
/* 详细数据样式 */
|
||||
.ms-ctx-details { border-top: 1px solid #eee; padding-top: 12px; }
|
||||
.ms-ctx-phase { margin-bottom: 16px; }
|
||||
.ms-ctx-phase:last-child { margin-bottom: 0; }
|
||||
.ms-ctx-phase-title { font-size: 11px; color: #5f6368; font-weight: 600; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.ms-ctx-proc-item, .ms-ctx-stat-item { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; }
|
||||
.ms-ctx-proc-name, .ms-ctx-stat-name { color: #333; }
|
||||
.ms-ctx-proc-count, .ms-ctx-stat-count { color: #666; font-weight: 500; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user