mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
feat(wiki): per-raw progress bar in raw-material card (RFC-012 M2 v2 UI)
This commit is contained in:
parent
f285db22d0
commit
2a2f862257
@ -55,6 +55,18 @@ public class WikiRawMaterialEntity {
|
|||||||
/** 错误信息 */
|
/** 错误信息 */
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RFC-012 M2 v2 UI:当前处理阶段(null 未开始 / "route" / "phase-b" / "done")。
|
||||||
|
* 供前端决定是否显示进度条以及显示"准备中"还是具体进度。
|
||||||
|
*/
|
||||||
|
private String progressPhase;
|
||||||
|
|
||||||
|
/** RFC-012 M2 v2 UI:本次处理计划的总页数(route 阶段确定后写入)。 */
|
||||||
|
private Integer progressTotal;
|
||||||
|
|
||||||
|
/** RFC-012 M2 v2 UI:已完成的页数(每个 phase B 页成功后 +1)。 */
|
||||||
|
private Integer progressDone;
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
|||||||
@ -22,9 +22,11 @@ import vip.mate.wiki.model.WikiRawMaterialEntity;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +52,20 @@ public class WikiProcessingService {
|
|||||||
/** 并行 chunk / 材料处理执行器(JDK 21 虚拟线程);Listener 跨包需要引用,故 public */
|
/** 并行 chunk / 材料处理执行器(JDK 21 虚拟线程);Listener 跨包需要引用,故 public */
|
||||||
public static final ExecutorService WIKI_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
|
public static final ExecutorService WIKI_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RFC-012 M2 v2 UI v2:单 raw 的进度计数器,多个并行 chunk 的 {@code processChunkTwoPhase}
|
||||||
|
* 共享同一份 atomic 计数,避免 6 个 chunk 各写各的 progress 字段时互相覆盖(导致 UI 永远 preparing)。
|
||||||
|
* <p>
|
||||||
|
* 生命周期:{@code processRawMaterial} 入口 put,try/finally 出口 remove。
|
||||||
|
*/
|
||||||
|
private static final class ProgressCounter {
|
||||||
|
final AtomicInteger total = new AtomicInteger(0);
|
||||||
|
final AtomicInteger done = new AtomicInteger(0);
|
||||||
|
final AtomicBoolean phaseBStarted = new AtomicBoolean(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<Long, ProgressCounter> progressCounters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理单个原始材料
|
* 处理单个原始材料
|
||||||
*/
|
*/
|
||||||
@ -93,6 +109,10 @@ public class WikiProcessingService {
|
|||||||
|
|
||||||
kbService.updateStatus(kb.getId(), "processing");
|
kbService.updateStatus(kb.getId(), "processing");
|
||||||
|
|
||||||
|
// RFC-012 M2 v2 UI v2:为本次 raw 处理创建共享进度计数器(多 chunk 共享,避免 race)
|
||||||
|
progressCounters.put(rawId, new ProgressCounter());
|
||||||
|
rawService.updateProgress(rawId, "route", 0, 0); // UI 立即看到 indeterminate 滑条
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Phase 1: 获取文本内容
|
// Phase 1: 获取文本内容
|
||||||
String textContent = rawService.getTextContent(raw);
|
String textContent = rawService.getTextContent(raw);
|
||||||
@ -152,6 +172,12 @@ public class WikiProcessingService {
|
|||||||
log.error("[Wiki] Processing failed for raw={}: {}", rawId, e.getMessage(), e);
|
log.error("[Wiki] Processing failed for raw={}: {}", rawId, e.getMessage(), e);
|
||||||
rawService.updateProcessingStatus(rawId, "failed", e.getMessage());
|
rawService.updateProcessingStatus(rawId, "failed", e.getMessage());
|
||||||
kbService.updateStatus(kb.getId(), "active");
|
kbService.updateStatus(kb.getId(), "active");
|
||||||
|
} finally {
|
||||||
|
// RFC-012 M2 v2 UI v2:写入最终进度并清理共享计数器
|
||||||
|
ProgressCounter pc = progressCounters.remove(rawId);
|
||||||
|
if (pc != null) {
|
||||||
|
rawService.updateProgress(rawId, "done", pc.done.get(), pc.total.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,6 +397,10 @@ public class WikiProcessingService {
|
|||||||
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
||||||
String rawTitle = raw.getTitle();
|
String rawTitle = raw.getTitle();
|
||||||
|
|
||||||
|
// RFC-012 M2 v2 UI v2:取共享进度计数器(processRawMaterial 入口已 put)。
|
||||||
|
// 多 chunk 并行时所有 chunk 共享同一份 atomic 计数,避免互相覆盖把 UI 拉回 preparing。
|
||||||
|
ProgressCounter pc = progressCounters.get(rawId);
|
||||||
|
|
||||||
// ─── 阶段 A:路由 ───
|
// ─── 阶段 A:路由 ───
|
||||||
String routeSystem = PromptLoader.loadPrompt("wiki/route-system");
|
String routeSystem = PromptLoader.loadPrompt("wiki/route-system");
|
||||||
String routeUserTemplate = PromptLoader.loadPrompt("wiki/route-user");
|
String routeUserTemplate = PromptLoader.loadPrompt("wiki/route-user");
|
||||||
@ -414,9 +444,19 @@ public class WikiProcessingService {
|
|||||||
if (!slug.isBlank()) updateSlugs.add(slug);
|
if (!slug.isBlank()) updateSlugs.add(slug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
int totalPlanned = createMetas.size() + updateSlugs.size();
|
||||||
log.info("[Wiki] Route phase: kbId={}, rawId={}, planned create={}, planned update={}",
|
log.info("[Wiki] Route phase: kbId={}, rawId={}, planned create={}, planned update={}",
|
||||||
kbId, rawId, createMetas.size(), updateSlugs.size());
|
kbId, rawId, createMetas.size(), updateSlugs.size());
|
||||||
|
|
||||||
|
// RFC-012 M2 v2 UI v2:把本 chunk 的计划数累加到共享 total;切换到 phase-b(仅首次切换需 log)
|
||||||
|
if (pc != null) {
|
||||||
|
pc.total.addAndGet(totalPlanned);
|
||||||
|
if (pc.phaseBStarted.compareAndSet(false, true)) {
|
||||||
|
log.info("[Wiki] Progress: switching to phase-b for raw={}", rawId);
|
||||||
|
}
|
||||||
|
rawService.updateProgress(rawId, "phase-b", pc.done.get(), pc.total.get());
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 阶段 B-1:逐页 create(每页一次单独 LLM call,输入/输出都是单页规模) ───
|
// ─── 阶段 B-1:逐页 create(每页一次单独 LLM call,输入/输出都是单页规模) ───
|
||||||
for (JsonNode meta : createMetas) {
|
for (JsonNode meta : createMetas) {
|
||||||
try {
|
try {
|
||||||
@ -427,6 +467,11 @@ public class WikiProcessingService {
|
|||||||
log.warn("[Wiki] Phase B create page slug='{}' failed: {}",
|
log.warn("[Wiki] Phase B create page slug='{}' failed: {}",
|
||||||
meta.path("slug").asText(""), e.getMessage());
|
meta.path("slug").asText(""), e.getMessage());
|
||||||
}
|
}
|
||||||
|
// 无论成功失败都推进 done 计数,避免失败页卡死 UI 进度
|
||||||
|
if (pc != null) {
|
||||||
|
int d = pc.done.incrementAndGet();
|
||||||
|
rawService.updateProgress(rawId, "phase-b", d, pc.total.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 阶段 B-2:逐页 merge(每页一次单独 LLM call) ───
|
// ─── 阶段 B-2:逐页 merge(每页一次单独 LLM call) ───
|
||||||
@ -438,7 +483,12 @@ public class WikiProcessingService {
|
|||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
log.warn("[Wiki] Phase B merge page slug='{}' failed: {}", slug, e.getMessage());
|
log.warn("[Wiki] Phase B merge page slug='{}' failed: {}", slug, e.getMessage());
|
||||||
}
|
}
|
||||||
|
if (pc != null) {
|
||||||
|
int d = pc.done.incrementAndGet();
|
||||||
|
rawService.updateProgress(rawId, "phase-b", d, pc.total.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 单 chunk 完成时不写"done"——多 chunk 还在跑;最终"done"由 processRawMaterial 的 finally 写入
|
||||||
|
|
||||||
log.info("[Wiki] Two-phase digest applied: kbId={}, rawId={}, created={}, updated={}",
|
log.info("[Wiki] Two-phase digest applied: kbId={}, rawId={}, created={}, updated={}",
|
||||||
kbId, rawId, created, updated);
|
kbId, rawId, created, updated);
|
||||||
|
|||||||
@ -165,10 +165,35 @@ public class WikiRawMaterialService {
|
|||||||
}
|
}
|
||||||
entity.setProcessingStatus("processing");
|
entity.setProcessingStatus("processing");
|
||||||
entity.setErrorMessage(null);
|
entity.setErrorMessage(null);
|
||||||
|
// RFC-012 M2 v2 UI:新一轮处理开始,清掉上次遗留的进度显示
|
||||||
|
entity.setProgressPhase(null);
|
||||||
|
entity.setProgressTotal(0);
|
||||||
|
entity.setProgressDone(0);
|
||||||
rawMapper.updateById(entity);
|
rawMapper.updateById(entity);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RFC-012 M2 v2 UI:更新 wiki 两阶段消化的进度字段。
|
||||||
|
* <p>
|
||||||
|
* 在 {@code WikiProcessingService.processChunkTwoPhase} 的四个节点被调用:
|
||||||
|
* <ul>
|
||||||
|
* <li>方法开头 → {@code phase="route"}, done=0, total=0(进度条显示 indeterminate)</li>
|
||||||
|
* <li>route 返回后 → {@code phase="phase-b"}, done=0, total=N+M(切换到 determinate)</li>
|
||||||
|
* <li>每页 create/merge 成功 → done +1</li>
|
||||||
|
* <li>方法结束 → {@code phase="done"}(UI 会随 status 变成 completed 自动隐藏进度条)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void updateProgress(Long id, String phase, int done, int total) {
|
||||||
|
WikiRawMaterialEntity entity = rawMapper.selectById(id);
|
||||||
|
if (entity == null) return;
|
||||||
|
entity.setProgressPhase(phase);
|
||||||
|
entity.setProgressDone(done);
|
||||||
|
entity.setProgressTotal(total);
|
||||||
|
rawMapper.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void updateProcessingStatus(Long id, String status, String errorMessage) {
|
public void updateProcessingStatus(Long id, String status, String errorMessage) {
|
||||||
WikiRawMaterialEntity entity = rawMapper.selectById(id);
|
WikiRawMaterialEntity entity = rawMapper.selectById(id);
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
-- V8: wiki raw material two-phase digest progress fields, for UI progress bar
|
||||||
|
-- RFC-012 M2 v2 UI follow-up: expose per-raw progress (current phase + pages done / total planned)
|
||||||
|
-- so the frontend can render a determinate progress bar instead of an opaque "处理中" badge.
|
||||||
|
ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS progress_phase VARCHAR(32) DEFAULT NULL;
|
||||||
|
ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS progress_total INT DEFAULT 0;
|
||||||
|
ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS progress_done INT DEFAULT 0;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
-- V8: wiki raw material two-phase digest progress fields, for UI progress bar
|
||||||
|
-- RFC-012 M2 v2 UI follow-up: expose per-raw progress (current phase + pages done / total planned)
|
||||||
|
-- so the frontend can render a determinate progress bar instead of an opaque "处理中" badge.
|
||||||
|
ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS progress_phase VARCHAR(32) DEFAULT NULL;
|
||||||
|
ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS progress_total INT DEFAULT 0;
|
||||||
|
ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS progress_done INT DEFAULT 0;
|
||||||
@ -1196,6 +1196,9 @@ export default {
|
|||||||
partial: 'PARTIAL',
|
partial: 'PARTIAL',
|
||||||
failed: 'FAILED',
|
failed: 'FAILED',
|
||||||
},
|
},
|
||||||
|
progress: {
|
||||||
|
preparing: 'Preparing…',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
cronJobs: {
|
cronJobs: {
|
||||||
kicker: 'Automation',
|
kicker: 'Automation',
|
||||||
|
|||||||
@ -1206,6 +1206,9 @@ export default {
|
|||||||
partial: '部分完成',
|
partial: '部分完成',
|
||||||
failed: '失败',
|
failed: '失败',
|
||||||
},
|
},
|
||||||
|
progress: {
|
||||||
|
preparing: '准备中…',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
cronJobs: {
|
cronJobs: {
|
||||||
kicker: '自动执行',
|
kicker: '自动执行',
|
||||||
|
|||||||
@ -26,6 +26,10 @@ export interface WikiRawMaterial {
|
|||||||
lastProcessedAt: string | null
|
lastProcessedAt: string | null
|
||||||
errorMessage: string | null
|
errorMessage: string | null
|
||||||
createTime: string
|
createTime: string
|
||||||
|
// RFC-012 M2 v2 UI:两阶段消化进度字段(后端在 route 后写 total,每页完成后 +1 done)
|
||||||
|
progressPhase: string | null
|
||||||
|
progressTotal: number
|
||||||
|
progressDone: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WikiPage {
|
export interface WikiPage {
|
||||||
|
|||||||
@ -56,35 +56,56 @@
|
|||||||
{{ t('wiki.noRawMaterials') }}
|
{{ t('wiki.noRawMaterials') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-for="raw in store.rawMaterials" :key="raw.id" class="raw-item">
|
<div v-for="raw in store.rawMaterials" :key="raw.id" class="raw-item">
|
||||||
<div class="raw-item-info">
|
<div class="raw-item-row">
|
||||||
<span class="raw-item-title">{{ raw.title }}</span>
|
<div class="raw-item-info">
|
||||||
<span class="raw-item-type">{{ raw.sourceType }}</span>
|
<span class="raw-item-title">{{ raw.title }}</span>
|
||||||
|
<span class="raw-item-type">{{ raw.sourceType }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="raw-item-meta">
|
||||||
|
<span class="status-badge" :class="raw.processingStatus">
|
||||||
|
{{ t(`wiki.status.${raw.processingStatus}`) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="raw.errorMessage && (raw.processingStatus === 'failed' || raw.processingStatus === 'partial')"
|
||||||
|
class="error-hint" :title="raw.errorMessage"
|
||||||
|
>
|
||||||
|
{{ raw.errorMessage }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="raw-item-actions">
|
||||||
|
<button
|
||||||
|
v-if="raw.processingStatus === 'failed' || raw.processingStatus === 'completed'"
|
||||||
|
class="btn-icon" :title="t('wiki.reprocess')"
|
||||||
|
@click="reprocess(raw.id)"
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="23 4 23 10 17 10"/>
|
||||||
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon btn-icon-danger" :title="t('common.delete')" @click="deleteRaw(raw.id)">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="raw-item-meta">
|
<div v-if="raw.processingStatus === 'processing'" class="raw-progress">
|
||||||
<span class="status-badge" :class="raw.processingStatus">
|
<div class="raw-progress-track">
|
||||||
{{ t(`wiki.status.${raw.processingStatus}`) }}
|
<div
|
||||||
|
class="raw-progress-fill"
|
||||||
|
:class="{ indeterminate: !raw.progressTotal }"
|
||||||
|
:style="raw.progressTotal
|
||||||
|
? { width: Math.min(100, Math.round((raw.progressDone / raw.progressTotal) * 100)) + '%' }
|
||||||
|
: {}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="raw-progress-label">
|
||||||
|
{{ raw.progressTotal
|
||||||
|
? `${raw.progressDone} / ${raw.progressTotal}`
|
||||||
|
: t('wiki.progress.preparing') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="raw.errorMessage" class="error-hint" :title="raw.errorMessage">
|
|
||||||
{{ raw.errorMessage }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="raw-item-actions">
|
|
||||||
<button
|
|
||||||
v-if="raw.processingStatus === 'failed' || raw.processingStatus === 'completed'"
|
|
||||||
class="btn-icon" :title="t('wiki.reprocess')"
|
|
||||||
@click="reprocess(raw.id)"
|
|
||||||
>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="23 4 23 10 17 10"/>
|
|
||||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="btn-icon btn-icon-danger" :title="t('common.delete')" @click="deleteRaw(raw.id)">
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="3 6 5 6 21 6"/>
|
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -122,7 +143,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useWikiStore } from '@/stores/useWikiStore'
|
import { useWikiStore } from '@/stores/useWikiStore'
|
||||||
import { wikiApi } from '@/api/index'
|
import { wikiApi } from '@/api/index'
|
||||||
@ -131,6 +152,33 @@ const { t } = useI18n()
|
|||||||
const store = useWikiStore()
|
const store = useWikiStore()
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
// RFC-012 M2 v2 UI:当列表中存在 processing 的材料时,每 3s 轮询一次刷新进度
|
||||||
|
// 处理完毕(无 processing 项)自动停止;组件卸载时也会清理 timer。
|
||||||
|
let pollTimer: number | null = null
|
||||||
|
const hasProcessing = computed(() =>
|
||||||
|
store.rawMaterials.some(r => r.processingStatus === 'processing')
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
hasProcessing,
|
||||||
|
(active) => {
|
||||||
|
if (active && pollTimer == null) {
|
||||||
|
pollTimer = window.setInterval(() => {
|
||||||
|
if (store.currentKB) store.fetchRawMaterials(store.currentKB.id)
|
||||||
|
}, 3000)
|
||||||
|
} else if (!active && pollTimer != null) {
|
||||||
|
clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (pollTimer != null) {
|
||||||
|
clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const showAddText = ref(false)
|
const showAddText = ref(false)
|
||||||
const textTitle = ref('')
|
const textTitle = ref('')
|
||||||
const textContent = ref('')
|
const textContent = ref('')
|
||||||
@ -250,8 +298,9 @@ async function handleScanDir() {
|
|||||||
.raw-list-title { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--mc-text-tertiary); margin-bottom: 4px; }
|
.raw-list-title { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--mc-text-tertiary); margin-bottom: 4px; }
|
||||||
.empty-hint { text-align: center; padding: 24px 0; font-size: 14px; color: var(--mc-text-tertiary); }
|
.empty-hint { text-align: center; padding: 24px 0; font-size: 14px; color: var(--mc-text-tertiary); }
|
||||||
|
|
||||||
.raw-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: linear-gradient(180deg, var(--mc-bg-elevated), var(--mc-bg-muted)); border: 1px solid var(--mc-border-light); border-radius: 14px; font-size: 13px; transition: border-color 0.15s, transform 0.15s; }
|
.raw-item { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; background: linear-gradient(180deg, var(--mc-bg-elevated), var(--mc-bg-muted)); border: 1px solid var(--mc-border-light); border-radius: 14px; font-size: 13px; transition: border-color 0.15s, transform 0.15s; }
|
||||||
.raw-item:hover { border-color: var(--mc-border); transform: translateY(-1px); }
|
.raw-item:hover { border-color: var(--mc-border); transform: translateY(-1px); }
|
||||||
|
.raw-item-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
|
|
||||||
.raw-item-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
|
.raw-item-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
|
||||||
.raw-item-title { font-weight: 500; color: var(--mc-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.raw-item-title { font-weight: 500; color: var(--mc-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
@ -260,6 +309,23 @@ async function handleScanDir() {
|
|||||||
.raw-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
.raw-item-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
||||||
.error-hint { font-size: 11px; color: var(--mc-danger); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.error-hint { font-size: 11px; color: var(--mc-danger); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* Two-phase digest progress bar (RFC-012 M2 v2 UI) */
|
||||||
|
.raw-progress { display: flex; align-items: center; gap: 10px; padding-top: 2px; }
|
||||||
|
.raw-progress-track { flex: 1; height: 4px; background: var(--mc-bg-sunken); border-radius: 9999px; overflow: hidden; position: relative; }
|
||||||
|
.raw-progress-fill { height: 100%; background: var(--mc-primary); border-radius: 9999px; transition: width 0.3s ease; }
|
||||||
|
.raw-progress-fill.indeterminate {
|
||||||
|
width: 30%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
animation: raw-progress-slide 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes raw-progress-slide {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
50% { transform: translateX(170%); }
|
||||||
|
100% { transform: translateX(330%); }
|
||||||
|
}
|
||||||
|
.raw-progress-label { font-size: 11px; color: var(--mc-text-tertiary); font-variant-numeric: tabular-nums; flex-shrink: 0; min-width: 56px; text-align: right; }
|
||||||
|
|
||||||
/* Icon button */
|
/* Icon button */
|
||||||
.btn-icon { width: 30px; height: 30px; border: 1px solid var(--mc-border-light); background: var(--mc-bg-elevated); cursor: pointer; border-radius: 8px; color: var(--mc-text-secondary); transition: all 0.15s; display: flex; align-items: center; justify-content: center; }
|
.btn-icon { width: 30px; height: 30px; border: 1px solid var(--mc-border-light); background: var(--mc-bg-elevated); cursor: pointer; border-radius: 8px; color: var(--mc-text-secondary); transition: all 0.15s; display: flex; align-items: center; justify-content: center; }
|
||||||
.btn-icon:hover { background: var(--mc-bg-sunken); color: var(--mc-primary); border-color: var(--mc-border); }
|
.btn-icon:hover { background: var(--mc-bg-sunken); color: var(--mc-primary); border-color: var(--mc-border); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user