Template
Add one-click Navidrome ingestion workflow
This commit is contained in:
+15
-2
@@ -11,7 +11,7 @@
|
||||
@select="handleSelect"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="tab in tabs"
|
||||
v-for="tab in visibleTabs"
|
||||
:key="tab.key"
|
||||
:index="tab.key"
|
||||
>
|
||||
@@ -42,6 +42,7 @@
|
||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
const IngestTab = defineAsyncComponent(() => import('./components/IngestTab.vue'));
|
||||
const AggregateTab = defineAsyncComponent(() => import('./components/AggregateTab.vue'));
|
||||
const ConvertTab = defineAsyncComponent(() => import('./components/ConvertTab.vue'));
|
||||
const DedupTab = defineAsyncComponent(() => import('./components/DedupTab.vue'));
|
||||
@@ -51,6 +52,7 @@ const MergeTab = defineAsyncComponent(() => import('./components/MergeTab.vue'))
|
||||
const SettingsTab = defineAsyncComponent(() => import('./components/SettingsTab.vue'));
|
||||
|
||||
type TabKey =
|
||||
| 'ingest'
|
||||
| 'aggregate'
|
||||
| 'convert'
|
||||
| 'dedup'
|
||||
@@ -69,6 +71,14 @@ interface TabDefinition {
|
||||
}
|
||||
|
||||
const tabs: TabDefinition[] = [
|
||||
{
|
||||
key: 'ingest',
|
||||
menuLabel: '一键导入',
|
||||
badge: 'INGEST',
|
||||
title: '一键导入',
|
||||
subtitle: '自动扫描、校验、转码、去重、整理入库 —— 一步到位。',
|
||||
component: IngestTab
|
||||
},
|
||||
{
|
||||
key: 'aggregate',
|
||||
menuLabel: '01 音频文件汇聚',
|
||||
@@ -132,7 +142,10 @@ const tabMap = tabs.reduce((acc, tab) => {
|
||||
return acc;
|
||||
}, {} as Record<TabKey, TabDefinition>);
|
||||
|
||||
const activeKey = ref<TabKey>('aggregate');
|
||||
const activeKey = ref<TabKey>('ingest');
|
||||
|
||||
// 用户可见的导航菜单:仅保留一键导入 + 配置(旧版六步标签隐藏但保留功能)
|
||||
const visibleTabs = computed(() => tabs.filter(t => t.key === 'ingest' || t.key === 'settings'));
|
||||
|
||||
const currentTab = computed(() => tabMap[activeKey.value] || tabs[0]);
|
||||
const currentComponent = computed(() => currentTab.value.component);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import request from './request';
|
||||
|
||||
export interface IngestResponse {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface IngestStatusResponse {
|
||||
taskId: string;
|
||||
running: boolean;
|
||||
completed: boolean;
|
||||
total: number;
|
||||
processed: number;
|
||||
ingestedFiles: number;
|
||||
duplicateFiles: number;
|
||||
missingMetadataFiles: number;
|
||||
unreadableFiles: number;
|
||||
conversionFailedFiles: number;
|
||||
otherRejectedFiles: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动一键导入任务
|
||||
*/
|
||||
export function startIngest(): Promise<IngestResponse> {
|
||||
return request.post('/api/ingest/start', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前/最近一次导入任务状态
|
||||
*/
|
||||
export function getIngestCurrentStatus(): Promise<IngestStatusResponse> {
|
||||
return request.get('/api/ingest/status');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导入任务状态
|
||||
*/
|
||||
export function getIngestStatus(taskId: string): Promise<IngestStatusResponse> {
|
||||
return request.get(`/api/ingest/status/${taskId}`);
|
||||
}
|
||||
@@ -20,6 +20,13 @@ export interface ProgressMessage {
|
||||
tracksMerged?: number;
|
||||
upgradedFiles?: number;
|
||||
skippedFiles?: number;
|
||||
// ingest 任务字段
|
||||
ingestedFiles?: number;
|
||||
duplicateFiles?: number;
|
||||
missingMetadataFiles?: number;
|
||||
unreadableFiles?: number;
|
||||
conversionFailedFiles?: number;
|
||||
otherRejectedFiles?: number;
|
||||
}
|
||||
|
||||
export function getProgress(taskId: string): Promise<ProgressMessage | null> {
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div class="ingest-container">
|
||||
<el-row :gutter="24">
|
||||
<!-- 任务控制卡片 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="10">
|
||||
<el-card class="config-card" shadow="hover">
|
||||
<template #header>
|
||||
<task-card-header :icon="Upload" title="一键导入" />
|
||||
</template>
|
||||
|
||||
<div class="ingest-description">
|
||||
<p>自动扫描 Input 目录中的音频文件,校验元数据、繁简转换、转码为 FLAC、去重后整理入 Library 目录。</p>
|
||||
</div>
|
||||
|
||||
<el-form label-width="100px" label-position="left">
|
||||
<el-form-item label="输入目录">
|
||||
<el-input :model-value="config.inputDir" disabled>
|
||||
<template #prefix>
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="库目录">
|
||||
<el-input :model-value="config.libraryDir" disabled>
|
||||
<template #prefix>
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="拒绝目录">
|
||||
<el-input :model-value="config.rejectedDir" disabled>
|
||||
<template #prefix>
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!canStart"
|
||||
@click="startIngest"
|
||||
size="large"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-icon v-if="!submitting"><VideoPlay /></el-icon>
|
||||
<span style="margin-left: 4px">{{ submitting ? '导入中...' : '开始一键导入' }}</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
:disabled="!config.basePath"
|
||||
@click="loadConfig"
|
||||
size="default"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
<span style="margin-left: 4px">刷新配置</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 配置提示 -->
|
||||
<el-alert
|
||||
v-if="!config.basePath"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #title>
|
||||
请先在<strong>配置</strong>页面设置工作根目录
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 任务进度卡片 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="14">
|
||||
<el-card class="progress-card" shadow="hover">
|
||||
<template #header>
|
||||
<task-card-header :icon="DataLine" title="导入进度" :status="connectionStatus" />
|
||||
</template>
|
||||
|
||||
<div v-if="!progress.taskId" class="empty-state">
|
||||
<el-icon class="empty-icon"><Upload /></el-icon>
|
||||
<p class="empty-text">准备就绪,点击「开始一键导入」...</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="progress-content">
|
||||
<!-- 统计信息 -->
|
||||
<task-stats-grid :items="progressStats" />
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-section">
|
||||
<el-progress
|
||||
:percentage="percentage"
|
||||
:status="progress.completed ? 'success' : progress.message?.includes('失败') ? 'exception' : undefined"
|
||||
:stroke-width="12"
|
||||
:show-text="true"
|
||||
:format="formatProgress"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 当前文件 -->
|
||||
<div v-if="progress.currentFile" class="current-file-section">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span class="file-text">{{ progress.currentFile }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<div v-if="progress.message" class="message-section">
|
||||
<el-alert
|
||||
:type="progress.completed ? (progress.failed > 0 ? 'warning' : 'success') : 'info'"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #title>
|
||||
<span>{{ progress.message }}</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Upload, VideoPlay, Refresh, DataLine, FolderOpened, Document } from '@element-plus/icons-vue';
|
||||
import { getConfig } from '../api/config';
|
||||
import type { ConfigResponse } from '../api/config';
|
||||
import { startIngest as apiStartIngest, getIngestStatus } from '../api/ingest';
|
||||
import type { IngestStatusResponse } from '../api/ingest';
|
||||
import { useWebSocket } from '../composables/useWebSocket';
|
||||
import type { ProgressMessage } from '../composables/useWebSocket';
|
||||
import TaskCardHeader from './common/TaskCardHeader.vue';
|
||||
import TaskStatsGrid from './common/TaskStatsGrid.vue';
|
||||
import type { StatItem } from './common/TaskStatsGrid.vue';
|
||||
|
||||
const submitting = ref(false);
|
||||
const taskId = ref<string | null>(null);
|
||||
|
||||
// 配置
|
||||
const config = reactive<{ basePath: string; inputDir: string; libraryDir: string; rejectedDir: string }>({
|
||||
basePath: '',
|
||||
inputDir: '',
|
||||
libraryDir: '',
|
||||
rejectedDir: ''
|
||||
});
|
||||
|
||||
// 进度
|
||||
const progress = reactive<ProgressMessage>({
|
||||
taskId: '',
|
||||
type: '',
|
||||
total: 0,
|
||||
processed: 0,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
currentFile: '',
|
||||
message: '',
|
||||
completed: false
|
||||
});
|
||||
|
||||
// 连接状态
|
||||
const connectionStatus = ref<'connected' | 'connecting' | 'completed' | null>(null);
|
||||
|
||||
const canStart = computed(() => !!config.basePath && !submitting.value);
|
||||
|
||||
const percentage = computed(() => {
|
||||
if (!progress.total || !progress.processed) return 0;
|
||||
if (progress.completed) return 100;
|
||||
return Math.round((progress.processed / progress.total) * 100);
|
||||
});
|
||||
|
||||
const progressStats = computed((): StatItem[] => {
|
||||
if (!progress.taskId) return [];
|
||||
return [
|
||||
{ label: '已处理', value: progress.processed ?? 0 },
|
||||
{ label: '已入库', value: progress.ingestedFiles ?? 0, tone: 'success' },
|
||||
{ label: '重复跳过', value: progress.duplicateFiles ?? 0, tone: 'warning' },
|
||||
{ label: '缺元数据', value: progress.missingMetadataFiles ?? 0, tone: 'failed' },
|
||||
{ label: '不可读', value: progress.unreadableFiles ?? 0, tone: 'failed' },
|
||||
{ label: '转码失败', value: progress.conversionFailedFiles ?? 0, tone: 'failed' },
|
||||
{ label: '其他拒绝', value: progress.otherRejectedFiles ?? 0, tone: 'failed' },
|
||||
];
|
||||
});
|
||||
|
||||
function formatProgress() {
|
||||
if (progress.completed) return '完成';
|
||||
return `${progress.processed ?? 0} / ${progress.total ?? 0}`;
|
||||
}
|
||||
|
||||
// WebSocket 消息处理
|
||||
function handleProgressMessage(msg: ProgressMessage) {
|
||||
Object.assign(progress, msg);
|
||||
connectionStatus.value = msg.completed ? 'completed' : 'connected';
|
||||
|
||||
if (msg.completed) {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket
|
||||
const { connect: wsConnect, disconnect: wsDisconnect } = useWebSocket(taskId, handleProgressMessage);
|
||||
|
||||
// 轮询兜底
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
pollTimer = setInterval(async () => {
|
||||
if (!taskId.value) return;
|
||||
try {
|
||||
const resp = await getIngestStatus(taskId.value);
|
||||
if (resp) {
|
||||
handleProgressMessage({
|
||||
taskId: resp.taskId,
|
||||
type: 'ingest',
|
||||
total: resp.total,
|
||||
processed: resp.processed,
|
||||
success: resp.ingestedFiles,
|
||||
failed: resp.otherRejectedFiles,
|
||||
currentFile: '',
|
||||
message: resp.message,
|
||||
completed: resp.completed,
|
||||
ingestedFiles: resp.ingestedFiles,
|
||||
duplicateFiles: resp.duplicateFiles,
|
||||
missingMetadataFiles: resp.missingMetadataFiles,
|
||||
unreadableFiles: resp.unreadableFiles,
|
||||
conversionFailedFiles: resp.conversionFailedFiles,
|
||||
otherRejectedFiles: resp.otherRejectedFiles,
|
||||
});
|
||||
if (resp.completed) {
|
||||
stopPolling();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略轮询错误
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer !== null) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 开始导入
|
||||
async function startIngest() {
|
||||
if (submitting.value) return;
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const resp = await apiStartIngest();
|
||||
taskId.value = resp.taskId;
|
||||
|
||||
// 重置进度
|
||||
Object.assign(progress, {
|
||||
taskId: resp.taskId, type: 'ingest', total: 0, processed: 0,
|
||||
success: 0, failed: 0, currentFile: '', message: '开始导入任务...',
|
||||
completed: false, ingestedFiles: 0, duplicateFiles: 0,
|
||||
missingMetadataFiles: 0, unreadableFiles: 0,
|
||||
conversionFailedFiles: 0, otherRejectedFiles: 0
|
||||
});
|
||||
|
||||
// 连接 WebSocket
|
||||
wsConnect();
|
||||
// 开启轮询兜底
|
||||
startPolling();
|
||||
} catch (e: any) {
|
||||
submitting.value = false;
|
||||
const msg = e?.response?.data?.message || e?.message || '启动任务失败';
|
||||
ElMessage.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面挂载恢复:查询是否存在正在运行或最近完成的任务
|
||||
async function resumeFromCurrentTask() {
|
||||
try {
|
||||
const { getIngestCurrentStatus } = await import('../api/ingest');
|
||||
let resp: IngestStatusResponse | null = null;
|
||||
try {
|
||||
resp = await getIngestCurrentStatus();
|
||||
} catch {
|
||||
// 没有当前任务或接口不可用
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resp || (!resp.taskId && !resp.running)) return;
|
||||
|
||||
taskId.value = resp.taskId;
|
||||
submitting.value = resp.running;
|
||||
|
||||
// 组装进度消息
|
||||
handleProgressMessage({
|
||||
taskId: resp.taskId,
|
||||
type: 'ingest',
|
||||
total: resp.total,
|
||||
processed: resp.processed,
|
||||
success: resp.ingestedFiles,
|
||||
failed: resp.otherRejectedFiles,
|
||||
currentFile: '',
|
||||
message: resp.message || (resp.running ? '恢复轮询...' : '已完成'),
|
||||
completed: resp.completed,
|
||||
ingestedFiles: resp.ingestedFiles,
|
||||
duplicateFiles: resp.duplicateFiles,
|
||||
missingMetadataFiles: resp.missingMetadataFiles,
|
||||
unreadableFiles: resp.unreadableFiles,
|
||||
conversionFailedFiles: resp.conversionFailedFiles,
|
||||
otherRejectedFiles: resp.otherRejectedFiles,
|
||||
});
|
||||
|
||||
if (resp.running) {
|
||||
wsConnect();
|
||||
startPolling();
|
||||
}
|
||||
} catch {
|
||||
// 恢复失败不阻塞页面
|
||||
}
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const cfg: ConfigResponse | null = await getConfig();
|
||||
if (cfg) {
|
||||
config.basePath = cfg.basePath || '';
|
||||
config.inputDir = cfg.inputDir || '';
|
||||
config.libraryDir = cfg.libraryDir || '';
|
||||
config.rejectedDir = cfg.rejectedDir || '';
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('加载配置失败');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
resumeFromCurrentTask();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (wsDisconnect) wsDisconnect();
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '../styles/panel-shared.css';
|
||||
|
||||
.ingest-description {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -29,7 +29,22 @@
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4 class="preview-title">派生目录预览</h4>
|
||||
<h4 class="preview-title">派生目录预览(简化模型)</h4>
|
||||
<el-descriptions :column="1" size="small" border>
|
||||
<el-descriptions-item label="Input">
|
||||
<span class="path-text">{{ preview.input || '未配置' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Library">
|
||||
<span class="path-text">{{ preview.library || '未配置' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Rejected">
|
||||
<span class="path-text">{{ preview.rejected || '未配置' }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4 class="preview-title">派生目录预览(旧版,向后兼容)</h4>
|
||||
<el-descriptions :column="1" size="small" border>
|
||||
<el-descriptions-item label="Input (SRC_ACC_DIR)">
|
||||
<span class="path-text">{{ preview.input || '未配置' }}</span>
|
||||
@@ -91,6 +106,8 @@ const preview = computed(() => {
|
||||
return {
|
||||
input: '',
|
||||
aggregated: '',
|
||||
library: '',
|
||||
rejected: '',
|
||||
formatIssues: '',
|
||||
duplicates: '',
|
||||
zhOutput: '',
|
||||
@@ -101,6 +118,8 @@ const preview = computed(() => {
|
||||
return {
|
||||
input: `${root}/Input`,
|
||||
aggregated: `${root}/Staging_Aggregated`,
|
||||
library: `${root}/Library`,
|
||||
rejected: `${root}/Rejected`,
|
||||
formatIssues: `${root}/Staging_Format_Issues`,
|
||||
duplicates: `${root}/Staging_Duplicates`,
|
||||
zhOutput: `${root}/Staging_T2S_Output`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref } from 'vue';
|
||||
import { ref, type Ref, unref } from 'vue';
|
||||
import SockJS from 'sockjs-client';
|
||||
import Stomp from 'stompjs';
|
||||
|
||||
@@ -22,15 +22,31 @@ export interface ProgressMessage {
|
||||
tracksMerged?: number;
|
||||
upgradedFiles?: number;
|
||||
skippedFiles?: number;
|
||||
// ingest 任务字段
|
||||
ingestedFiles?: number;
|
||||
duplicateFiles?: number;
|
||||
missingMetadataFiles?: number;
|
||||
unreadableFiles?: number;
|
||||
conversionFailedFiles?: number;
|
||||
otherRejectedFiles?: number;
|
||||
}
|
||||
|
||||
export function useWebSocket(taskId: string | null, onMessage: (msg: ProgressMessage) => void) {
|
||||
/**
|
||||
* WebSocket 连接组合式函数。
|
||||
*
|
||||
* @param taskIdRef 响应式任务 ID(Ref<string | null>),每次 connect 时取 .value
|
||||
* @param onMessage 收到进度消息时的回调
|
||||
*/
|
||||
export function useWebSocket(taskIdRef: Ref<string | null>, onMessage: (msg: ProgressMessage) => void) {
|
||||
const connected = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
let stompClient: Stomp.Client | null = null;
|
||||
// 记住订阅了哪个 taskId,以便断线重连时仍使用正确值
|
||||
let currentSubscribedId: string | null = null;
|
||||
|
||||
const connect = () => {
|
||||
if (!taskId) {
|
||||
const rawId = unref(taskIdRef);
|
||||
if (!rawId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,6 +55,8 @@ export function useWebSocket(taskId: string | null, onMessage: (msg: ProgressMes
|
||||
disconnect();
|
||||
}
|
||||
|
||||
currentSubscribedId = rawId;
|
||||
|
||||
try {
|
||||
const wsBase =
|
||||
import.meta.env.VITE_WS_BASE_URL !== undefined && import.meta.env.VITE_WS_BASE_URL !== ''
|
||||
@@ -46,35 +64,37 @@ export function useWebSocket(taskId: string | null, onMessage: (msg: ProgressMes
|
||||
: (typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8080');
|
||||
const socket = new SockJS(`${wsBase}/ws`);
|
||||
stompClient = Stomp.over(socket);
|
||||
|
||||
|
||||
// 禁用调试日志
|
||||
stompClient.debug = () => {};
|
||||
|
||||
stompClient.connect(
|
||||
{},
|
||||
() => {
|
||||
// 连接成功
|
||||
connected.value = true;
|
||||
error.value = null;
|
||||
|
||||
// 订阅任务进度
|
||||
if (stompClient) {
|
||||
stompClient.subscribe(`/topic/progress/${taskId}`, (message) => {
|
||||
try {
|
||||
const progress: ProgressMessage = JSON.parse(message.body);
|
||||
onMessage(progress);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse progress message:', e);
|
||||
|
||||
// 订阅当前 taskId 的进度主题
|
||||
const topic = `/topic/progress/${currentSubscribedId}`;
|
||||
stompClient!.subscribe(topic, (frame) => {
|
||||
try {
|
||||
const body = JSON.parse(frame.body) as ProgressMessage;
|
||||
onMessage(body);
|
||||
|
||||
// 任务完成时自动断开
|
||||
if (body.completed) {
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析 WebSocket 消息失败:', e);
|
||||
}
|
||||
});
|
||||
},
|
||||
(errorFrame) => {
|
||||
// 连接失败
|
||||
const errorMsg = errorFrame.headers?.['message'] || errorFrame.toString() || 'WebSocket 连接错误';
|
||||
error.value = errorMsg;
|
||||
(err: unknown) => {
|
||||
connected.value = false;
|
||||
console.error('WebSocket 连接失败:', errorMsg, errorFrame);
|
||||
const errMsg = err instanceof Error ? err.message : 'WebSocket 连接失败';
|
||||
error.value = errMsg;
|
||||
console.error('WebSocket 连接失败:', err);
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -93,7 +113,6 @@ export function useWebSocket(taskId: string | null, onMessage: (msg: ProgressMes
|
||||
connected.value = false;
|
||||
});
|
||||
} else {
|
||||
// 如果连接未成功,直接清理
|
||||
connected.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -102,6 +121,7 @@ export function useWebSocket(taskId: string | null, onMessage: (msg: ProgressMes
|
||||
}
|
||||
stompClient = null;
|
||||
}
|
||||
currentSubscribedId = null;
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user