镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 13:47:47 +08:00
feat(文件选择器): 添加重试和返回功能并优化同步状态显示
- 在文件选择器面板中添加重试失败任务和返回功能 - 为配置面板添加同步重试按钮和返回按钮 - 优化同步状态显示逻辑,仅在必要时显示 - 移除调试日志并添加同步支持状态检查 - 更新多语言翻译添加"返回"和"重新同步"文本
这个提交包含在:
父节点
fc682a7a09
当前提交
b245c81a42
@ -268,11 +268,6 @@ export class QuarkAdapter extends BasePlatformAdapter {
|
||||
return { success: true, method: 'none' };
|
||||
}
|
||||
|
||||
logger.info('[DIAG-SYNC-QUARK] syncAfterRename start', {
|
||||
renameCount: renameInfoById.size,
|
||||
hasOldName: renameByOldName.size,
|
||||
});
|
||||
|
||||
// Always patch visible DOM for immediate feedback
|
||||
const patchedById = this.applyRenameMappingToDomById(renameInfoById);
|
||||
const patchedByOldName = this.applyRenameMappingToDomByOldName(renameByOldName);
|
||||
@ -283,17 +278,6 @@ export class QuarkAdapter extends BasePlatformAdapter {
|
||||
// Best effort: trigger Quark's own re-render so it can rebuild native filename DOM (icon/wrap/etc.)
|
||||
const rerender = await this.tryTriggerNativeFileListRerender(renameInfoById);
|
||||
|
||||
logger.info('[DIAG-SYNC-QUARK] syncAfterRename patch result', {
|
||||
rerender,
|
||||
patchedById,
|
||||
patchedByOldName,
|
||||
patchedCount,
|
||||
dom: {
|
||||
hasDataFileId: document.querySelectorAll('[data-file-id]').length,
|
||||
hasFileNameClass: document.querySelectorAll('.file-name').length,
|
||||
},
|
||||
});
|
||||
|
||||
if (rerender.triggered) {
|
||||
return { success: true, method: 'ui-refresh', message: rerender.message };
|
||||
}
|
||||
|
||||
@ -76,6 +76,12 @@ export class ConfigPanel extends LitElement {
|
||||
@property({ type: String })
|
||||
syncMessage: string | null = null;
|
||||
|
||||
/**
|
||||
* Whether sync is supported by the current adapter (optional)
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
syncSupported = false;
|
||||
|
||||
/**
|
||||
* Current selected rule type
|
||||
*/
|
||||
@ -235,6 +241,7 @@ export class ConfigPanel extends LitElement {
|
||||
: this.syncStatus === 'failed'
|
||||
? I18nService.t('sync_failed')
|
||||
: '';
|
||||
const showSyncStatus = Boolean(syncText) || Boolean(this.syncMessage);
|
||||
|
||||
return html`
|
||||
<div class="execution-view">
|
||||
@ -270,10 +277,17 @@ export class ConfigPanel extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.finished
|
||||
${this.finished && showSyncStatus
|
||||
? html`
|
||||
<div class="sync-status ${this.syncStatus}">
|
||||
${syncText}${this.syncMessage ? `:${this.syncMessage}` : ''}
|
||||
${this.syncSupported && this.syncStatus === 'failed'
|
||||
? html`
|
||||
<button class="sync-retry" @click=${this.handleSync}>
|
||||
${I18nService.t('sync_retry')}
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
@ -295,13 +309,15 @@ export class ConfigPanel extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const failed = this.progress?.failed ?? 0;
|
||||
|
||||
return html`
|
||||
<div class="execution-actions">
|
||||
<button class="button button-primary" ?disabled=${this.syncStatus === 'syncing'} @click=${this.handleSync}>
|
||||
${I18nService.t('sync_list')}
|
||||
<button class="button button-primary" ?disabled=${failed <= 0} @click=${this.handleRetry}>
|
||||
${I18nService.t('retry')}
|
||||
</button>
|
||||
<button class="button button-default" @click=${this.handleClose}>
|
||||
${I18nService.t('close')}
|
||||
<button class="button button-default" @click=${this.handleBack}>
|
||||
${I18nService.t('back')}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@ -335,9 +351,18 @@ export class ConfigPanel extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private handleClose(): void {
|
||||
private handleRetry(): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('close', {
|
||||
new CustomEvent('retry', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleBack(): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('back', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
@ -749,6 +774,20 @@ export class ConfigPanel extends LitElement {
|
||||
border-color: #91caff;
|
||||
}
|
||||
|
||||
.sync-retry {
|
||||
margin-left: 8px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #1677ff;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sync-retry:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.execution-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
@ -592,7 +592,6 @@ export class FileSelectorPanel extends LitElement {
|
||||
const result = await this.adapter.syncAfterRename(renames);
|
||||
this.syncStatus = result.success ? 'success' : 'failed';
|
||||
this.syncMessage = result.success ? null : result.message || null;
|
||||
logger.info('[DIAG-SYNC] syncAfterRename result', result);
|
||||
} catch (error) {
|
||||
const errorObj = error instanceof Error ? error : new Error(String(error));
|
||||
this.syncStatus = 'failed';
|
||||
@ -641,6 +640,95 @@ export class FileSelectorPanel extends LitElement {
|
||||
void this.syncAfterRename();
|
||||
}
|
||||
|
||||
private async handleRetryFailed(): Promise<void> {
|
||||
if (this.executing || !this.executionFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
const failedIds = new Set(this.executionItems.filter((item) => item.done === false).map((item) => item.file.id));
|
||||
if (failedIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.executing = true;
|
||||
this.executorState = ExecutorState.RUNNING;
|
||||
this.syncStatus = 'idle';
|
||||
this.syncMessage = null;
|
||||
|
||||
// Reset only failed items back to pending
|
||||
this.executionItems = this.executionItems.map((item) => {
|
||||
if (!failedIds.has(item.file.id)) return item;
|
||||
return { ...item, done: undefined, error: undefined };
|
||||
});
|
||||
|
||||
const tasks = this.executionItems
|
||||
.map((item, index) => ({ item, index }))
|
||||
.filter(({ item }) => failedIds.has(item.file.id))
|
||||
.map(({ item, index }) => ({
|
||||
file: item.file,
|
||||
newName: item.newName,
|
||||
index,
|
||||
}));
|
||||
|
||||
const retryFiles = tasks.map((task) => task.file);
|
||||
if (retryFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.progress = {
|
||||
completed: 0,
|
||||
total: retryFiles.length,
|
||||
currentFile: '',
|
||||
success: 0,
|
||||
failed: 0,
|
||||
};
|
||||
|
||||
const executor = new BatchExecutor(retryFiles, this.ruleConfig, this.adapter, {
|
||||
requestInterval: this.adapter.getConfig().requestInterval,
|
||||
tasks,
|
||||
onProgress: (progress) => {
|
||||
this.handleProgress(progress);
|
||||
},
|
||||
});
|
||||
this.executor = executor;
|
||||
|
||||
const results = await executor.execute();
|
||||
this.executionResults = results;
|
||||
this.executorState = executor.getState();
|
||||
|
||||
this.applyExecutionResults(results);
|
||||
this.updateSummaryProgress();
|
||||
void this.syncAfterRename();
|
||||
} catch (error) {
|
||||
const errorObj = error instanceof Error ? error : new Error(String(error));
|
||||
this.error = errorObj.message;
|
||||
logger.error('[FileSelectorPanel] Retry failed:', errorObj);
|
||||
} finally {
|
||||
this.executing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private handleBack(): void {
|
||||
this.resetExecutionState();
|
||||
}
|
||||
|
||||
private updateSummaryProgress(): void {
|
||||
if (!this.executionItems.length) return;
|
||||
|
||||
const success = this.executionItems.filter((item) => item.done === true).length;
|
||||
const failed = this.executionItems.filter((item) => item.done === false).length;
|
||||
const completed = success + failed;
|
||||
|
||||
this.progress = {
|
||||
completed,
|
||||
total: this.executionItems.length,
|
||||
currentFile: '',
|
||||
success,
|
||||
failed,
|
||||
};
|
||||
}
|
||||
|
||||
private resetExecutionState(): void {
|
||||
this.executor = null;
|
||||
this.executorState = ExecutorState.IDLE;
|
||||
@ -712,12 +800,14 @@ export class FileSelectorPanel extends LitElement {
|
||||
.paused=${this.executorState === ExecutorState.PAUSED}
|
||||
.syncStatus=${this.syncStatus}
|
||||
.syncMessage=${this.syncMessage}
|
||||
.syncSupported=${!!this.adapter.syncAfterRename}
|
||||
@config-change=${this.handleConfigChange}
|
||||
@execute=${this.handleExecute}
|
||||
@pause=${this.handlePause}
|
||||
@cancel=${this.handleCancel}
|
||||
@sync=${this.handleSync}
|
||||
@close=${this.handleClose}
|
||||
@retry=${this.handleRetryFailed}
|
||||
@back=${this.handleBack}
|
||||
></config-panel>
|
||||
|
||||
<file-list-panel
|
||||
|
||||
@ -12,6 +12,8 @@ export interface BatchExecutorOptions {
|
||||
requestInterval: number;
|
||||
/** 是否跳过无需变更的任务(默认不跳过,保持与平台行为一致) */
|
||||
skipUnchanged?: boolean;
|
||||
/** 自定义任务列表(可选,用于重试等场景,优先级高于 rule 生成的任务) */
|
||||
tasks?: Task[];
|
||||
/** 进度回调 */
|
||||
onProgress?: (progress: ProgressEvent) => void;
|
||||
/** 完成回调 */
|
||||
@ -105,7 +107,10 @@ export class BatchExecutor {
|
||||
|
||||
try {
|
||||
// 准备所有重命名任务
|
||||
const tasks = this.prepareTasks();
|
||||
const preparedTasks = this.options.tasks ?? this.prepareTasks();
|
||||
const tasks = this.options.skipUnchanged
|
||||
? preparedTasks.filter((task) => task.newName !== task.file.name)
|
||||
: preparedTasks;
|
||||
this.tasks = tasks;
|
||||
|
||||
if (tasks.length === 0) {
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"loading_files": "Loading files...",
|
||||
"error_title": "Error",
|
||||
"retry": "Retry",
|
||||
"back": "Back",
|
||||
"close": "Close",
|
||||
"search_placeholder": "Search files",
|
||||
"clear_search": "Clear search",
|
||||
@ -87,6 +88,7 @@
|
||||
"syncing_list": "Syncing...",
|
||||
"sync_success": "List synced",
|
||||
"sync_failed": "Sync failed, please refresh the page",
|
||||
"sync_retry": "Resync",
|
||||
"sync_not_supported": "Auto sync is not supported on this platform. Please refresh the page.",
|
||||
"preview_empty": "No files to preview",
|
||||
"preview_loading": "Generating preview...",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"loading_files": "正在加载文件...",
|
||||
"error_title": "错误",
|
||||
"retry": "重试",
|
||||
"back": "返回",
|
||||
"close": "关闭",
|
||||
"search_placeholder": "搜索文件名",
|
||||
"clear_search": "清除搜索",
|
||||
@ -87,6 +88,7 @@
|
||||
"syncing_list": "同步中...",
|
||||
"sync_success": "已同步列表",
|
||||
"sync_failed": "同步失败,可能需要手动刷新页面",
|
||||
"sync_retry": "重新同步",
|
||||
"sync_not_supported": "当前平台暂不支持自动同步,请手动刷新页面",
|
||||
"preview_empty": "暂无文件可预览",
|
||||
"preview_loading": "生成预览中...",
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"loading_files": "正在載入檔案...",
|
||||
"error_title": "錯誤",
|
||||
"retry": "重試",
|
||||
"back": "返回",
|
||||
"close": "關閉",
|
||||
"search_placeholder": "搜尋檔案名稱",
|
||||
"clear_search": "清除搜尋",
|
||||
@ -87,6 +88,7 @@
|
||||
"syncing_list": "同步中...",
|
||||
"sync_success": "已同步列表",
|
||||
"sync_failed": "同步失敗,可能需要手動重新整理頁面",
|
||||
"sync_retry": "重新同步",
|
||||
"sync_not_supported": "目前平台暫不支援自動同步,請手動重新整理頁面",
|
||||
"preview_empty": "暫無檔案可預覽",
|
||||
"preview_loading": "產生預覽中...",
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
* 1. Edit version in package.json
|
||||
* 2. Run: npm run sync:version (or automatically via prebuild/predev)
|
||||
*
|
||||
* Generated at: 2025-12-29T06:17:04.003Z
|
||||
* Generated at: 2025-12-29T07:41:42.086Z
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@ -55,5 +55,7 @@ export interface FileSelectorEvents {
|
||||
'type-filter': { type: FileType | 'all' };
|
||||
'config-change': RuleConfig;
|
||||
'execute': void;
|
||||
'retry': void;
|
||||
'back': void;
|
||||
'panel-close': void;
|
||||
}
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户