feat: extract shared constants and utilities to utils/exceptions.js
Phase 1 of exception center redesign - all shared constants, utility functions, and styling helpers extracted from ExceptionPage.jsx into a single source of truth for use by new components and hooks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
export const EXCEPTION_FILTERS = [
|
||||
{ id: 'all', name: '全部异常' },
|
||||
{ id: 'missing_tags', name: '元数据缺失' },
|
||||
{ id: 'duplicates', name: '文件重复' },
|
||||
{ id: 'match_failed', name: '匹配失败' },
|
||||
{ id: 'low_score', name: '匹配分过低' },
|
||||
{ id: 'convert_failed', name: '转码失败' },
|
||||
{ id: 'organize_failed', name: '入库失败' }
|
||||
];
|
||||
|
||||
export const RESOLUTION_FILTERS = [
|
||||
{ id: 'open', name: '开放中' },
|
||||
{ id: 'resolved', name: '已解决' },
|
||||
{ id: 'ignored', name: '已忽略' },
|
||||
{ id: 'all', name: '全部' }
|
||||
];
|
||||
|
||||
export const ACTION_LABELS = {
|
||||
retry_match: '一键匹配',
|
||||
select_match_candidate: '确认候选',
|
||||
edit_metadata: '保存草稿',
|
||||
save_and_organize: '加入音乐库',
|
||||
keep_existing: '忽略并删除新版',
|
||||
replace_existing: '覆盖替换旧版',
|
||||
keep_both_with_rename: '重命名新版并保留双版本',
|
||||
retry_preprocess: '重跑预处理',
|
||||
move_to_review_trash: '移入 Review Trash',
|
||||
retry_organize: '重试入库',
|
||||
edit_target_path: '编辑目标路径',
|
||||
ignore_exception: '永久忽略',
|
||||
delete_file: '删除文件'
|
||||
};
|
||||
|
||||
export const PROVIDER_MODES = [
|
||||
{ id: 'all', label: '多源并行', providers: [] },
|
||||
{ id: 'authoritative', label: '权威优先', providers: ['acoustid', 'musicbrainz'] },
|
||||
{ id: 'netease', label: '网易云', providers: ['netease'] },
|
||||
{ id: 'qq', label: 'QQ 音乐', providers: ['qq'] },
|
||||
{ id: 'spotify', label: 'Spotify', providers: ['spotify'] }
|
||||
];
|
||||
|
||||
export const BULK_ACTIONS = {
|
||||
match_failed: ['retry_match', 'ignore_exception'],
|
||||
low_score: ['retry_match', 'ignore_exception'],
|
||||
organize_failed: ['ignore_exception', 'retry_organize'],
|
||||
missing_tags: ['retry_match', 'ignore_exception'],
|
||||
duplicates: ['ignore_exception'],
|
||||
convert_failed: ['ignore_exception']
|
||||
};
|
||||
|
||||
export const ITEMS_PER_PAGE = 8;
|
||||
export const METADATA_QUEUE_TYPES = ['missing_tags', 'match_failed', 'low_score'];
|
||||
export const METADATA_QUEUE_PAGE_SIZE = 100;
|
||||
export const METADATA_FIELDS = ['title', 'artist', 'album', 'album_artist', 'track_number', 'disc_number', 'year', 'lyrics'];
|
||||
export const REQUIRED_FIELDS = ['title', 'artist', 'album_artist'];
|
||||
|
||||
export const WIZARD_STEPS = [
|
||||
{ id: 'select', label: '选择歌曲' },
|
||||
{ id: 'listen', label: '试听确认' },
|
||||
{ id: 'match', label: '推荐匹配' },
|
||||
{ id: 'edit', label: '手动编辑' },
|
||||
{ id: 'confirm', label: '入库确认' }
|
||||
];
|
||||
|
||||
// --- Utility functions ---
|
||||
|
||||
export function compareTimestampDesc(a, b) {
|
||||
return new Date(b || 0).getTime() - new Date(a || 0).getTime();
|
||||
}
|
||||
|
||||
export function formatTimestamp(value) {
|
||||
if (!value) return '--';
|
||||
try {
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
||||
}).format(new Date(value));
|
||||
} catch { return String(value); }
|
||||
}
|
||||
|
||||
export function formatSeconds(value) {
|
||||
if (!Number.isFinite(value) || value <= 0) return '--:--';
|
||||
const total = Math.floor(value);
|
||||
const minutes = Math.floor(total / 60);
|
||||
const seconds = total % 60;
|
||||
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function formatConfidence(value) {
|
||||
if (value == null) return '--';
|
||||
return `${Number(value).toFixed(1)} 分`;
|
||||
}
|
||||
|
||||
export function formatMetadataValue(value) {
|
||||
if (value === null || value === undefined || value === '') return '--';
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function providerLabel(provider) {
|
||||
const labels = { acoustid: 'AcoustID', musicbrainz: 'MusicBrainz', netease: '网易云', qq: 'QQ 音乐', spotify: 'Spotify' };
|
||||
const key = String(provider || '').toLowerCase();
|
||||
return labels[key] || provider || '推荐候选';
|
||||
}
|
||||
|
||||
export function isTerminalRepairStatus(status) {
|
||||
return status === 'completed' || status === 'failed';
|
||||
}
|
||||
|
||||
export function normalizeActionParams(action, params) {
|
||||
if (action === 'retry_match') {
|
||||
return { provider_mode: params.provider_mode || 'all', providers: params.providers || [] };
|
||||
}
|
||||
if (action === 'save_and_organize' || action === 'edit_metadata') {
|
||||
return { metadata_patch: { ...(params.metadata_patch || {}) } };
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export function buildDefaultParams(action, detailRecord) {
|
||||
if (action === 'retry_match') {
|
||||
return { provider_mode: 'all', providers: [] };
|
||||
}
|
||||
if (action === 'select_match_candidate') {
|
||||
return { candidate_index: 0 };
|
||||
}
|
||||
if (action === 'edit_metadata' || action === 'save_and_organize') {
|
||||
const metadata =
|
||||
detailRecord?.effective_metadata ||
|
||||
detailRecord?.matched_metadata_json ||
|
||||
detailRecord?.original_tags_json ||
|
||||
{};
|
||||
return {
|
||||
metadata_patch: {
|
||||
title: metadata.title || '',
|
||||
artist: metadata.artist || '',
|
||||
album: metadata.album || '',
|
||||
album_artist: metadata.album_artist || '',
|
||||
track_number: metadata.track_number ?? null,
|
||||
disc_number: metadata.disc_number ?? null,
|
||||
year: metadata.year ?? null,
|
||||
lyrics: metadata.lyrics || ''
|
||||
}
|
||||
};
|
||||
}
|
||||
if (action === 'retry_organize' || action === 'edit_target_path') {
|
||||
return { target_relative_path: detailRecord?.library_relative_path || '' };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function isMetadataWorkflowException(exceptionType) {
|
||||
return ['missing_tags', 'match_failed', 'low_score'].includes(exceptionType);
|
||||
}
|
||||
|
||||
export function inferDefaultAction(detailRecord) {
|
||||
if (!detailRecord) return '';
|
||||
const actions = detailRecord.available_actions || [];
|
||||
if (actions.includes('save_and_organize')) return 'save_and_organize';
|
||||
if (actions.includes('retry_match')) return 'retry_match';
|
||||
if (actions.includes('retry_organize')) return 'retry_organize';
|
||||
return actions[0] || '';
|
||||
}
|
||||
|
||||
export function getMissingRequiredFields(metadata) {
|
||||
return REQUIRED_FIELDS
|
||||
.filter((field) => !String(metadata?.[field] || '').trim())
|
||||
.map((field) => {
|
||||
const labels = { title: '标题', artist: '艺术家', album_artist: '专辑艺术家' };
|
||||
return labels[field] || field;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Styling helpers ---
|
||||
|
||||
export function chipClass(active, secondary = false) {
|
||||
if (secondary) {
|
||||
return `rounded-full border px-3 py-1.5 text-xs transition ${
|
||||
active
|
||||
? 'border-slate-400 bg-slate-200 text-slate-900'
|
||||
: 'border-slate-700 bg-slate-900 text-slate-400 hover:border-slate-500'
|
||||
}`;
|
||||
}
|
||||
return `rounded-full border px-3 py-1.5 text-xs transition ${
|
||||
active
|
||||
? 'border-cyan-400/50 bg-cyan-500/15 text-cyan-100'
|
||||
: 'border-slate-700 bg-slate-900 text-slate-400 hover:border-slate-500'
|
||||
}`;
|
||||
}
|
||||
|
||||
export function inputClass() {
|
||||
return 'w-full rounded-xl border border-slate-700 bg-slate-900 px-3 py-2 text-sm text-slate-100 outline-none transition placeholder:text-slate-500 focus:border-cyan-400/60';
|
||||
}
|
||||
|
||||
export function actionButtonClass(enabled = true) {
|
||||
return `inline-flex items-center gap-1.5 rounded-xl px-3 py-2 text-xs font-medium transition ${
|
||||
enabled
|
||||
? 'border border-cyan-400/40 bg-cyan-500/15 text-cyan-100 hover:bg-cyan-500/20'
|
||||
: 'cursor-not-allowed border border-slate-800 bg-slate-900 text-slate-600'
|
||||
}`;
|
||||
}
|
||||
|
||||
export function riskClass(riskLevel) {
|
||||
return {
|
||||
low: 'bg-emerald-500/10 text-emerald-200',
|
||||
medium: 'bg-amber-500/10 text-amber-200',
|
||||
high: 'bg-rose-500/10 text-rose-200'
|
||||
}[riskLevel || 'low'];
|
||||
}
|
||||
|
||||
export function getFilterCount(summary, filterId) {
|
||||
if (!summary) return '-';
|
||||
if (filterId === 'all') return summary.total ?? '-';
|
||||
return summary.counts_by_type?.[filterId] ?? '-';
|
||||
}
|
||||
|
||||
export function getMetadataQueueCounts(queue) {
|
||||
return METADATA_QUEUE_TYPES.reduce(
|
||||
(counts, type) => ({
|
||||
...counts,
|
||||
[type]: queue.filter((item) => item.exception_type === type).length
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldRefreshExceptionListFully(repairTask) {
|
||||
const action = repairTask?.repair_plan_json?.action;
|
||||
const itemCount = (repairTask?.repair_plan_json?.items || []).length;
|
||||
if (!action) return true;
|
||||
if (itemCount !== 1) return true;
|
||||
return !['retry_match', 'select_match_candidate', 'edit_metadata'].includes(action);
|
||||
}
|
||||
Reference in New Issue
Block a user