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