diff --git a/.github/workflows/dev-daily-fixed.yml b/.github/workflows/dev-daily-fixed.yml index 7666825..9093f60 100644 --- a/.github/workflows/dev-daily-fixed.yml +++ b/.github/workflows/dev-daily-fixed.yml @@ -81,6 +81,7 @@ jobs: run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build @@ -131,6 +132,7 @@ jobs: run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build @@ -178,6 +180,7 @@ jobs: run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build @@ -225,6 +228,7 @@ jobs: run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build diff --git a/.github/workflows/preview-nightly-main.yml b/.github/workflows/preview-nightly-main.yml index 13e04aa..e9486bb 100644 --- a/.github/workflows/preview-nightly-main.yml +++ b/.github/workflows/preview-nightly-main.yml @@ -107,6 +107,7 @@ jobs: run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build @@ -160,6 +161,7 @@ jobs: run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build @@ -211,6 +213,7 @@ jobs: run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build @@ -262,6 +265,7 @@ jobs: run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f0b6ac9..ed89fb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,7 @@ jobs: npm version $VERSION --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build @@ -95,6 +96,7 @@ jobs: npm version $VERSION --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build @@ -145,6 +147,7 @@ jobs: npm version $VERSION --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build @@ -195,6 +198,7 @@ jobs: npm version $VERSION --no-git-tag-version --allow-same-version - name: Build Frontend & Type Check + shell: bash run: | npx tsc npx vite build diff --git a/electron/preload-env.ts b/electron/preload-env.ts index 3476a0b..514b5e6 100644 --- a/electron/preload-env.ts +++ b/electron/preload-env.ts @@ -2,7 +2,7 @@ import { join, dirname } from 'path' /** * 强制将本地资源目录添加到 PATH 最前端,确保优先加载本地 DLL - * 解决系统中存在冲突版本的 DLL 导致的应用崩溃问题 + * 解决系统中存在冲突版本的数据服务导致的应用崩溃问题 */ function enforceLocalDllPriority() { const isDev = !!process.env.VITE_DEV_SERVER_URL @@ -35,5 +35,5 @@ function enforceLocalDllPriority() { try { enforceLocalDllPriority() } catch (e) { - console.error('[WeFlow] Failed to enforce local DLL priority:', e) + console.error('[WeFlow] Failed to enforce local service priority:', e) } diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index f4c63fa..bfeaf4a 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -469,7 +469,7 @@ class ChatService { if (this.monitorSetup) return this.monitorSetup = true - // 使用 C++ DLL 内部的文件监控 (ReadDirectoryChangesW) + // 使用 C++数据服务内部的文件监控 (ReadDirectoryChangesW) // 这种方式更高效,且不占用 JS 线程,并能直接监听 session/message 目录变更 wcdbService.setMonitor((type, json) => { this.handleSessionStatsMonitorChange(type, json) @@ -5117,7 +5117,7 @@ class ChatService { } } - //手动查找 media_*.db 文件(当 WCDB DLL 不支持 listMediaDbs 时的 fallback) + //手动查找 media_*.db 文件(当 WCDB数据服务不支持 listMediaDbs 时的 fallback) private async findMediaDbsManually(): Promise { try { const dbPath = this.configService.get('dbPath') @@ -5676,7 +5676,7 @@ class ChatService { if (!result.success || !result.contact) return null const contact = result.contact as Record let alias = String(contact.alias || contact.Alias || '') - // DLL 有时不返回 alias 字段,补一条直接 SQL 查询兜底 + //数据服务有时不返回 alias 字段,补一条直接 SQL 查询兜底 if (!alias) { try { const aliasResult = await wcdbService.getContactAliasMap([username]) diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index 76176cc..95800b6 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -1423,7 +1423,7 @@ class ExportService { } return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates) } catch (e) { - console.error('getGroupNicknamesForRoom dll error:', e) + console.error('getGroupNicknamesForRoom service error:', e) return new Map() } } diff --git a/electron/services/groupAnalyticsService.ts b/electron/services/groupAnalyticsService.ts index a244d16..b07745a 100644 --- a/electron/services/groupAnalyticsService.ts +++ b/electron/services/groupAnalyticsService.ts @@ -275,7 +275,7 @@ class GroupAnalyticsService { } return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates) } catch (e) { - console.error('getGroupNicknamesForRoom dll error:', e) + console.error('getGroupNicknamesForRoom service error:', e) return new Map() } } diff --git a/electron/services/keyService.ts b/electron/services/keyService.ts index 25b0aa1..4b25c88 100644 --- a/electron/services/keyService.ts +++ b/electron/services/keyService.ts @@ -684,10 +684,7 @@ export class KeyService { return { success: false, error: '获取密钥超时', logs } } - // --- Image Key (通过 DLL 从缓存目录获取 code,用前端 wxid 计算密钥) --- - private cleanWxid(wxid: string): string { - // 截断到第二个下划线: wxid_g4pshorcc0r529_da6c → wxid_g4pshorcc0r529 const first = wxid.indexOf('_') if (first === -1) return wxid const second = wxid.indexOf('_', first + 1) diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index d7fe76e..3a23acb 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -1062,14 +1062,14 @@ class SnsService { } /** - * 补全 DLL 返回的评论中缺失的 refNickname - * DLL 返回的 refCommentId 是被回复评论的 cmtid + * 补全数据服务返回的评论中缺失的 refNickname + *数据服务返回的 refCommentId 是被回复评论的 cmtid * 评论按 cmtid 从小到大排列,cmtid 从 1 开始递增 */ private fixCommentRefs(comments: any[]): any[] { if (!comments || comments.length === 0) return [] - // DLL 现在返回完整的评论数据(含 emojis、refNickname) + //数据服务现在返回完整的评论数据(含 emojis、refNickname) // 此处做最终的格式化和兜底补全 const idToNickname = new Map() comments.forEach((c, idx) => { @@ -1140,14 +1140,14 @@ class SnsService { } : undefined })) - // DLL 已返回完整评论数据(含 emojis、refNickname) - // 如果 DLL 评论缺少表情包信息,回退到从 rawXml 重新解析 + //数据服务已返回完整评论数据(含 emojis、refNickname) + // 如果数据服务评论缺少表情包信息,回退到从 rawXml 重新解析 const dllComments: any[] = post.comments || [] const hasEmojisInDll = dllComments.some((c: any) => c.emojis && c.emojis.length > 0) let finalComments: any[] if (dllComments.length > 0 && (hasEmojisInDll || !rawXml)) { - // DLL 数据完整,直接使用 + //数据服务数据完整,直接使用 finalComments = this.fixCommentRefs(dllComments) } else if (rawXml) { // 回退:从 rawXml 重新解析(兼容旧版 DLL) diff --git a/electron/services/voiceTranscribeService.ts b/electron/services/voiceTranscribeService.ts index 952bac9..5cc7804 100644 --- a/electron/services/voiceTranscribeService.ts +++ b/electron/services/voiceTranscribeService.ts @@ -76,7 +76,7 @@ export class VoiceTranscribeService { console.warn(`[VoiceTranscribe] 未找到 ${platformPkg} 目录,可能导致语音引擎加载失败`) } } else if (process.platform === 'win32') { - // Windows: 把 sherpa-onnx DLL 所在目录加到 PATH,否则 native module 找不到依赖 + // Windows: 把 sherpa-onnx 所在目录加到 PATH,否则 native module 找不到依赖 const existing = env['PATH'] || '' const merged = [...candidates, ...existing.split(';').filter(Boolean)] env['PATH'] = Array.from(new Set(merged)).join(';') diff --git a/electron/services/wcdbCore.ts b/electron/services/wcdbCore.ts index f183cfe..8c389c2 100644 --- a/electron/services/wcdbCore.ts +++ b/electron/services/wcdbCore.ts @@ -2,7 +2,7 @@ import { join, dirname, basename } from 'path' import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs' import { tmpdir } from 'os' -// DLL 初始化错误信息,用于帮助用户诊断问题 +//数据服务初始化错误信息,用于帮助用户诊断问题 let lastDllInitError: string | null = null export function getLastDllInitError(): string | null { @@ -157,7 +157,7 @@ export class WcdbCore { return false } - // 从 DLL 获取动态管道名(含 PID) + // 从数据服务获取动态管道名(含 PID) let pipePath = '\\\\.\\pipe\\weflow_monitor' if (this.wcdbGetMonitorPipeName) { try { @@ -638,8 +638,8 @@ export class WcdbCore { this.writeLog(`[bootstrap] initialize platform=${process.platform} dllPath=${dllPath} resourcesPath=${this.resourcesPath || ''} userDataPath=${this.userDataPath || ''}`, true) if (!existsSync(dllPath)) { - console.error('WCDB DLL 不存在:', dllPath) - this.writeLog(`[bootstrap] initialize failed: dll not found path=${dllPath}`, true) + console.error('WCDB数据服务不存在:', dllPath) + this.writeLog(`[bootstrap] initialize failed:数据服务not found path=${dllPath}`, true) return false } @@ -694,7 +694,7 @@ export class WcdbCore { // 尝试多个可能的资源路径 const resourcePaths = [ - dllDir, // DLL 所在目录 + dllDir, //数据服务所在目录 dirname(dllDir), // 上级目录 process.resourcesPath, // 打包后 Contents/Resources process.resourcesPath ? join(process.resourcesPath as string, 'resources') : null, // Contents/Resources/resources @@ -1280,7 +1280,7 @@ export class WcdbCore { } /** - * 打印 DLL 内部日志(仅在出错时调用) + * 打印数据服务内部日志(仅在出错时调用) */ private async printLogs(force = false): Promise { try { @@ -1603,7 +1603,7 @@ export class WcdbCore { const outPtr = [null as any] const result = this.wcdbGetSessions(this.handle, outPtr) - // DLL 调用后再次让出控制权 + //数据服务调用后再次让出控制权 await new Promise(resolve => setImmediate(resolve)) if (result !== 0 || !outPtr[0]) { @@ -1958,7 +1958,7 @@ export class WcdbCore { const outPtr = [null as any] const result = this.wcdbGetDisplayNames(this.handle, JSON.stringify(usernames), outPtr) - // DLL 调用后再次让出控制权 + //数据服务调用后再次让出控制权 await new Promise(resolve => setImmediate(resolve)) if (result !== 0 || !outPtr[0]) { @@ -2043,7 +2043,7 @@ export class WcdbCore { const outPtr = [null as any] const result = this.wcdbGetAvatarUrls(handle, JSON.stringify(toFetch), outPtr) - // DLL 调用后再次让出控制权 + //数据服务调用后再次让出控制权 await new Promise(resolve => setImmediate(resolve)) if (result !== 0 || !outPtr[0]) { @@ -2143,7 +2143,7 @@ export class WcdbCore { return { success: false, error: 'WCDB 未连接' } } if (!this.wcdbGetGroupNicknames) { - return { success: false, error: '当前 DLL 版本不支持获取群昵称接口' } + return { success: false, error: '当前数据服务版本不支持获取群昵称接口' } } try { const outPtr = [null as any] @@ -2986,7 +2986,7 @@ export class WcdbCore { async getVoiceData(sessionId: string, createTime: number, candidates: string[], localId: number = 0, svrId: string | number = 0): Promise<{ success: boolean; hex?: string; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbGetVoiceData) return { success: false, error: '当前 DLL 版本不支持获取语音数据' } + if (!this.wcdbGetVoiceData) return { success: false, error: '当前数据服务版本不支持获取语音数据' } try { const outPtr = [null as any] const result = this.wcdbGetVoiceData(this.handle, sessionId, createTime, localId, BigInt(svrId || 0), JSON.stringify(candidates), outPtr) @@ -3400,7 +3400,7 @@ export class WcdbCore { async searchMessages(keyword: string, sessionId?: string, limit?: number, offset?: number, beginTimestamp?: number, endTimestamp?: number): Promise<{ success: boolean; messages?: any[]; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbSearchMessages) return { success: false, error: '当前 DLL 版本不支持搜索消息' } + if (!this.wcdbSearchMessages) return { success: false, error: '当前数据服务版本不支持搜索消息' } try { const handle = this.handle await new Promise(resolve => setImmediate(resolve)) @@ -3430,7 +3430,7 @@ export class WcdbCore { async getSnsTimeline(limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number): Promise<{ success: boolean; timeline?: any[]; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbGetSnsTimeline) return { success: false, error: '当前 DLL 版本不支持获取朋友圈' } + if (!this.wcdbGetSnsTimeline) return { success: false, error: '当前数据服务版本不支持获取朋友圈' } try { const outPtr = [null as any] const usernamesJson = usernames && usernames.length > 0 ? JSON.stringify(usernames) : '' @@ -3522,7 +3522,7 @@ export class WcdbCore { async installMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbInstallMessageAntiRevokeTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' } + if (!this.wcdbInstallMessageAntiRevokeTrigger) return { success: false, error: '当前数据服务版本不支持此功能' } const normalizedSessionId = String(sessionId || '').trim() if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' } try { @@ -3547,7 +3547,7 @@ export class WcdbCore { async uninstallMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbUninstallMessageAntiRevokeTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' } + if (!this.wcdbUninstallMessageAntiRevokeTrigger) return { success: false, error: '当前数据服务版本不支持此功能' } const normalizedSessionId = String(sessionId || '').trim() if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' } try { @@ -3569,7 +3569,7 @@ export class WcdbCore { async checkMessageAntiRevokeTrigger(sessionId: string): Promise<{ success: boolean; installed?: boolean; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbCheckMessageAntiRevokeTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' } + if (!this.wcdbCheckMessageAntiRevokeTrigger) return { success: false, error: '当前数据服务版本不支持此功能' } const normalizedSessionId = String(sessionId || '').trim() if (!normalizedSessionId) return { success: false, error: 'sessionId 不能为空' } try { @@ -3640,7 +3640,7 @@ export class WcdbCore { */ async installSnsBlockDeleteTrigger(): Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbInstallSnsBlockDeleteTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' } + if (!this.wcdbInstallSnsBlockDeleteTrigger) return { success: false, error: '当前数据服务版本不支持此功能' } try { const outPtr = [null] const status = this.wcdbInstallSnsBlockDeleteTrigger(this.handle, outPtr) @@ -3650,7 +3650,7 @@ export class WcdbCore { try { this.wcdbFreeString(outPtr[0]) } catch { } } if (status === 1) { - // DLL 返回 1 表示已安装 + //数据服务返回 1 表示已安装 return { success: true, alreadyInstalled: true } } if (status !== 0) { @@ -3667,7 +3667,7 @@ export class WcdbCore { */ async uninstallSnsBlockDeleteTrigger(): Promise<{ success: boolean; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbUninstallSnsBlockDeleteTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' } + if (!this.wcdbUninstallSnsBlockDeleteTrigger) return { success: false, error: '当前数据服务版本不支持此功能' } try { const outPtr = [null] const status = this.wcdbUninstallSnsBlockDeleteTrigger(this.handle, outPtr) @@ -3690,7 +3690,7 @@ export class WcdbCore { */ async checkSnsBlockDeleteTrigger(): Promise<{ success: boolean; installed?: boolean; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbCheckSnsBlockDeleteTrigger) return { success: false, error: '当前 DLL 版本不支持此功能' } + if (!this.wcdbCheckSnsBlockDeleteTrigger) return { success: false, error: '当前数据服务版本不支持此功能' } try { const outInstalled = [0] const status = this.wcdbCheckSnsBlockDeleteTrigger(this.handle, outInstalled) @@ -3705,7 +3705,7 @@ export class WcdbCore { async deleteSnsPost(postId: string): Promise<{ success: boolean; error?: string }> { if (!this.ensureReady()) return { success: false, error: 'WCDB 未连接' } - if (!this.wcdbDeleteSnsPost) return { success: false, error: '当前 DLL 版本不支持此功能' } + if (!this.wcdbDeleteSnsPost) return { success: false, error: '当前数据服务版本不支持此功能' } try { const outPtr = [null] const status = this.wcdbDeleteSnsPost(this.handle, postId, outPtr) diff --git a/electron/services/wcdbService.ts b/electron/services/wcdbService.ts index 491538b..9516922 100644 --- a/electron/services/wcdbService.ts +++ b/electron/services/wcdbService.ts @@ -80,7 +80,7 @@ export class WcdbService { // Worker 退出,需要 reject 所有 pending promises if (code !== 0) { console.error('WCDB Worker 异常退出,退出码:', code) - const errorMsg = `Worker 异常退出 (退出码: ${code})。可能是 DLL 加载失败,请检查是否安装了 Visual C++ Redistributable。` + const errorMsg = `Worker 异常退出 (退出码: ${code})。可能是数据服务加载失败,请检查是否安装了 Visual C++ Redistributable。` for (const [id, p] of this.pending) { p.reject(new Error(errorMsg)) } @@ -467,7 +467,7 @@ export class WcdbService { } /** - * 获取表情包释义(严格 DLL 接口) + * 获取表情包释义(严格数据服务接口) */ async getEmoticonCaptionStrict(md5: string): Promise<{ success: boolean; caption?: string; error?: string }> { return this.callWorker('getEmoticonCaptionStrict', { md5 }) @@ -608,7 +608,7 @@ export class WcdbService { } /** - * 获取 DLL 内部日志 + * 获取数据服务内部日志 */ async getLogs(): Promise<{ success: boolean; logs?: string[]; error?: string }> { return this.callWorker('getLogs') diff --git a/src/App.tsx b/src/App.tsx index 2f56226..92d0a93 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -430,7 +430,7 @@ function App() { } } else { - // 如果错误信息包含 VC++ 或 DLL 相关内容,不清除配置,只提示用户 + // 如果错误信息包含 VC++ 或数据服务相关内容,不清除配置,只提示用户 // 其他错误可能需要重新配置 const errorMsg = result.error || '' if (errorMsg.includes('Visual C++') || diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index b1a959f..8190a19 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -2712,43 +2712,76 @@ // 会话详情面板 .detail-panel { - width: 280px; + width: clamp(280px, 25vw, 360px); min-width: 280px; - background: var(--card-bg); - border-left: 1px solid var(--border-color); + max-width: 360px; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--card-bg) 94%, #fff 6%) 0%, + var(--card-bg) 100% + ); + border-left: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + box-shadow: -14px 0 28px rgba(0, 0, 0, 0.07); display: flex; flex-direction: column; overflow: hidden; - animation: slideInRight 0.2s ease; + animation: slideInRight 0.28s cubic-bezier(0.22, 1, 0.36, 1); + will-change: transform, opacity; .detail-header { display: flex; align-items: center; justify-content: space-between; - padding: 16px; + gap: 8px; + padding: 14px 14px 12px; + background: color-mix(in srgb, var(--card-bg) 92%, #fff 8%); border-bottom: 1px solid var(--border-color); + position: sticky; + top: 0; + z-index: 2; + backdrop-filter: blur(6px); + + .detail-title-wrap { + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; + } h4 { - font-size: 15px; + font-size: 16px; font-weight: 600; color: var(--text-primary); margin: 0; } + .detail-title-sub { + font-size: 12px; + color: var(--text-tertiary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .close-btn { + width: 28px; + height: 28px; background: none; border: none; - padding: 4px; + padding: 0; cursor: pointer; color: var(--text-secondary); - border-radius: 6px; + border-radius: 8px; display: flex; align-items: center; justify-content: center; + flex-shrink: 0; + transition: all 0.18s ease; &:hover { background: var(--bg-hover); color: var(--text-primary); + transform: rotate(90deg); } } } @@ -2780,69 +2813,135 @@ .detail-content { flex: 1; overflow-y: auto; - padding: 16px; + padding: 12px; + display: flex; + flex-direction: column; + gap: 12px; &::-webkit-scrollbar { - width: 4px; + width: 6px; } &::-webkit-scrollbar-thumb { - background: var(--text-tertiary); - border-radius: 2px; + background: color-mix(in srgb, var(--text-tertiary) 68%, transparent); + border-radius: 999px; + } + } + + .detail-overview-card { + display: flex; + align-items: center; + gap: 10px; + padding: 12px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 84%, transparent); + animation: detailCardEnter 0.24s ease both; + + .detail-overview-avatar { + flex-shrink: 0; + border: 1px solid color-mix(in srgb, var(--border-color) 60%, transparent); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); + } + + .detail-overview-meta { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; + } + + .detail-overview-name { + color: var(--text-primary); + font-size: 14px; + font-weight: 600; + line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .detail-overview-sub { + color: var(--text-tertiary); + font-size: 12px; + line-height: 1.25; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } } .detail-section { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } + margin: 0; + padding: 12px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 86%, transparent); + animation: detailCardEnter 0.24s ease both; .section-title { display: flex; align-items: center; - gap: 6px; - font-size: 14px; + gap: 8px; + font-size: 13px; font-weight: 600; color: var(--text-secondary); - margin-bottom: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; + margin-bottom: 10px; + letter-spacing: 0.3px; svg { - opacity: 0.7; + color: var(--primary); + opacity: 0.9; } } .detail-stats-meta { - margin-top: -6px; + margin-top: -2px; margin-bottom: 10px; + padding: 6px 8px; + border-radius: 8px; font-size: 12px; color: var(--text-tertiary); + background: color-mix(in srgb, var(--card-bg) 84%, transparent); } } + .detail-section:nth-child(2) { + animation-delay: 0.03s; + } + + .detail-section:nth-child(3) { + animation-delay: 0.06s; + } + + .detail-section:nth-child(4) { + animation-delay: 0.09s; + } + .detail-item { display: flex; align-items: center; - gap: 8px; + gap: 10px; padding: 8px 0; - border-bottom: 1px solid var(--border-color); + border-bottom: 1px dashed color-mix(in srgb, var(--border-color) 76%, transparent); font-size: 13px; &:last-child { border-bottom: none; } - svg { + > svg { color: var(--text-tertiary); flex-shrink: 0; + width: 14px; + height: 14px; } .label { color: var(--text-secondary); flex-shrink: 0; + width: 88px; + line-height: 1.3; } .value { @@ -2851,22 +2950,27 @@ color: var(--text-primary); word-break: break-all; user-select: text; + line-height: 1.35; &.highlight { color: var(--primary); font-weight: 600; + font-size: 21px; + letter-spacing: 0.2px; } } .detail-inline-btn { - border: none; - background: var(--bg-secondary); + border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); + background: color-mix(in srgb, var(--card-bg) 90%, transparent); color: var(--primary); - border-radius: 6px; - padding: 4px 8px; + border-radius: 999px; + padding: 5px 10px; font-size: 12px; line-height: 1; + font-weight: 500; cursor: pointer; + transition: all 0.16s ease; &:disabled { cursor: not-allowed; @@ -2874,6 +2978,7 @@ } &:hover:not(:disabled) { + transform: translateY(-1px); background: var(--bg-hover); } } @@ -2886,12 +2991,12 @@ height: 22px; padding: 0; border: none; - border-radius: 4px; + border-radius: 6px; background: transparent; color: var(--text-tertiary); cursor: pointer; flex-shrink: 0; - opacity: 0; + opacity: 0.2; transition: opacity 0.15s, color 0.15s, background 0.15s; &:hover { @@ -2907,18 +3012,27 @@ &:hover .copy-btn { opacity: 1; } + + &:focus-within .copy-btn { + opacity: 1; + } + } + + .detail-basic-section .label { + width: 70px; } .table-list { display: flex; flex-direction: column; - gap: 8px; + gap: 10px; } .detail-table-placeholder { - padding: 10px 12px; - background: var(--bg-secondary); - border-radius: 8px; + padding: 11px 12px; + background: color-mix(in srgb, var(--card-bg) 84%, transparent); + border: 1px dashed color-mix(in srgb, var(--border-color) 76%, transparent); + border-radius: 10px; font-size: 12px; color: var(--text-secondary); } @@ -2928,18 +3042,64 @@ align-items: center; justify-content: space-between; padding: 10px 12px; - background: var(--bg-secondary); - border-radius: 8px; + background: color-mix(in srgb, var(--card-bg) 90%, transparent); + border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); + border-radius: 10px; font-size: 12px; + transition: transform 0.16s ease, border-color 0.16s ease; + + &:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--primary) 26%, var(--border-color)); + } .db-name { color: var(--text-primary); font-weight: 500; + max-width: 62%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .table-count { color: var(--primary); - font-weight: 500; + font-weight: 600; + } + } +} + +.session-detail-panel { + .detail-content { + padding-top: 10px; + } + + .detail-overview-card { + gap: 10px; + + .detail-overview-meta { + flex: 1; + } + } + + .detail-overview-close-btn { + width: 28px; + height: 28px; + border: none; + border-radius: 8px; + background: color-mix(in srgb, var(--card-bg) 88%, transparent); + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + transition: all 0.16s ease; + + &:hover { + color: var(--text-primary); + background: var(--bg-hover); + transform: rotate(90deg); } } } @@ -3140,6 +3300,18 @@ } } +@keyframes detailCardEnter { + from { + opacity: 0; + transform: translateY(6px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + /* 语音转文字按钮样式 */ .voice-transcribe-btn { width: 28px; diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 7cd8cb2..36b7cc1 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -68,6 +68,21 @@ const MESSAGE_LIST_SCROLL_IDLE_MS = 160 const MESSAGE_TOP_WHEEL_LOAD_COOLDOWN_MS = 160 const MESSAGE_EDGE_TRIGGER_DISTANCE_PX = 96 +type RequestIdleCallbackCompat = (callback: () => void, options?: { timeout?: number }) => number + +function scheduleWhenIdle(task: () => void, options?: { timeout?: number; fallbackDelay?: number }): void { + const requestIdleCallbackFn = ( + globalThis as typeof globalThis & { requestIdleCallback?: RequestIdleCallbackCompat } + ).requestIdleCallback + + if (typeof requestIdleCallbackFn === 'function') { + requestIdleCallbackFn(task, options?.timeout !== undefined ? { timeout: options.timeout } : undefined) + return + } + + window.setTimeout(task, options?.fallbackDelay ?? 0) +} + function isGlobalMsgSearchCanceled(error: unknown): boolean { return String(error || '') === GLOBAL_MSG_SEARCH_CANCELED_ERROR } @@ -2959,15 +2974,9 @@ function ChatPage(props: ChatPageProps) { await loadContactInfoBatch(usernames) } else { await new Promise((resolve) => { - if ('requestIdleCallback' in window) { - window.requestIdleCallback(() => { - void loadContactInfoBatch(usernames).finally(resolve) - }, { timeout: 700 }) - } else { - setTimeout(() => { - void loadContactInfoBatch(usernames).finally(resolve) - }, 80) - } + scheduleWhenIdle(() => { + void loadContactInfoBatch(usernames).finally(resolve) + }, { timeout: 700, fallbackDelay: 80 }) }) } processedBatchCount += 1 @@ -3066,7 +3075,7 @@ function ChatPage(props: ChatPageProps) { const loadContactInfoBatch = async (usernames: string[]) => { const startTime = performance.now() try { - // 在 DLL 调用前让出控制权(使用 setTimeout 0 代替 setImmediate) + // 在数据服务调用前让出控制权(使用 setTimeout 0 代替 setImmediate) await new Promise(resolve => setTimeout(resolve, 0)) const dllStart = performance.now() @@ -3077,7 +3086,7 @@ function ChatPage(props: ChatPageProps) { } const dllTime = performance.now() - dllStart - // DLL 调用后再次让出控制权 + //数据服务调用后再次让出控制权 await new Promise(resolve => setTimeout(resolve, 0)) const totalTime = performance.now() - startTime @@ -3259,13 +3268,7 @@ function ChatPage(props: ChatPageProps) { } if (defer) { - if ('requestIdleCallback' in window) { - window.requestIdleCallback(() => { - runWarmup() - }, { timeout: 1200 }) - } else { - globalThis.setTimeout(runWarmup, 120) - } + scheduleWhenIdle(runWarmup, { timeout: 1200, fallbackDelay: 120 }) return } @@ -3288,11 +3291,7 @@ function ChatPage(props: ChatPageProps) { run() } - if ('requestIdleCallback' in window) { - window.requestIdleCallback(runWhenIdle, { timeout: 1200 }) - } else { - window.setTimeout(runWhenIdle, MESSAGE_LIST_SCROLL_IDLE_MS) - } + scheduleWhenIdle(runWhenIdle, { timeout: 1200, fallbackDelay: MESSAGE_LIST_SCROLL_IDLE_MS }) }, [warmupGroupSenderProfiles]) // 加载消息 @@ -6796,13 +6795,7 @@ function ChatPage(props: ChatPageProps) { {/* 会话详情面板 */} {showDetailPanel && ( -
-
-

会话详情

- -
+
{isLoadingDetail && !sessionDetail ? (
@@ -6810,7 +6803,27 @@ function ChatPage(props: ChatPageProps) {
) : sessionDetail ? (
-
+
+ +
+ + {sessionDetail.remark || sessionDetail.nickName || currentSession?.displayName || sessionDetail.alias || sessionDetail.wxid} + + + {sessionDetail.alias || sessionDetail.wxid} + +
+ +
+ +
微信ID @@ -6848,10 +6861,10 @@ function ChatPage(props: ChatPageProps) { )}
-
+
- 消息统计(导出口径) + 消息统计
{isRefreshingDetailStats @@ -7009,7 +7022,7 @@ function ChatPage(props: ChatPageProps) {
-
+
数据库分布 diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index ab340f4..0944735 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -1,14 +1,18 @@ .export-board-page { - min-height: calc(100% + 48px); - height: auto; - margin: -24px; - padding: 20px; - background: var(--bg-primary); + min-height: 100%; + height: 100%; + margin: -24px -24px 0; + padding: 18px 22px 12px; + background: + radial-gradient(1200px 520px at 6% -8%, color-mix(in srgb, var(--primary) 11%, transparent), transparent 65%), + radial-gradient(860px 420px at 90% 0%, color-mix(in srgb, var(--primary) 7%, transparent), transparent 66%), + var(--bg-primary); display: flex; flex-direction: column; gap: 16px; overflow-x: hidden; - overflow-y: visible; + overflow-y: hidden; + animation: exportPageEnter 0.34s ease-out; .spin { animation: exportSpin 1s linear infinite; @@ -18,12 +22,16 @@ .export-top-panel { display: block; flex-shrink: 0; + position: relative; + z-index: 55; + animation: exportSectionReveal 0.34s ease both; } .export-top-bar { display: flex; align-items: stretch; gap: 12px; + position: relative; } .export-section-title-row { @@ -31,6 +39,7 @@ align-items: center; justify-content: flex-start; gap: 6px; + animation: exportSectionReveal 0.38s ease both; } .session-load-detail-entry { @@ -58,6 +67,12 @@ background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary)); color: var(--text-primary); } + + &.open { + border-color: color-mix(in srgb, var(--primary) 30%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 5%, var(--bg-secondary)); + color: var(--text-primary); + } } .session-load-detail-entry-icon { @@ -98,6 +113,11 @@ opacity: 1; } +.session-load-detail-entry.open .session-load-detail-entry-bar { + animation: none; + opacity: 0.86; +} + @keyframes sessionLoadDetailBars { 0%, 100% { transform: scaleY(0.72); @@ -112,9 +132,10 @@ .export-section-title { margin: 0; - font-size: 15px; - font-weight: 600; + font-size: 18px; + font-weight: 700; color: var(--text-primary); + letter-spacing: 0.2px; } .section-info-tooltip { @@ -177,8 +198,9 @@ display: flex; align-items: center; justify-content: center; - z-index: 2200; + z-index: 7800; padding: 20px; + animation: exportOverlayFadeIn 0.2s ease both; } .session-load-detail-modal { @@ -191,6 +213,7 @@ box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28); display: flex; flex-direction: column; + animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both; } .session-load-detail-header { @@ -485,8 +508,9 @@ display: flex; align-items: center; justify-content: center; - z-index: 2250; + z-index: 7850; padding: 20px; + animation: exportOverlayFadeIn 0.2s ease both; } .session-mutual-friends-modal { @@ -499,6 +523,7 @@ box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28); display: flex; flex-direction: column; + animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both; } .session-mutual-friends-header { @@ -716,14 +741,23 @@ --top-inline-control-height: 34px; flex: 0 1 980px; width: min(980px, 100%); - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 12px; + background: + linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 75%, var(--bg-primary)) 0%, var(--card-bg) 100%); + border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); + border-radius: 14px; + box-shadow: 0 14px 28px rgba(15, 23, 42, 0.08); + padding: 13px; display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(240px, 1fr) auto; gap: 10px; align-items: stretch; + animation: exportSectionReveal 0.4s ease both; + transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; + + &:hover { + border-color: color-mix(in srgb, var(--primary) 30%, var(--border-color)); + box-shadow: 0 16px 30px rgba(15, 23, 42, 0.1); + } .control-label { font-size: 11px; @@ -759,12 +793,13 @@ .path-value { border: 1px dashed var(--border-color); border-radius: 8px; - background: var(--bg-secondary); + background: color-mix(in srgb, var(--bg-secondary) 86%, var(--bg-primary)); display: flex; align-items: stretch; min-width: 0; flex: 1; overflow: hidden; + transition: border-color 0.15s ease, background 0.15s ease; } .path-link { @@ -816,6 +851,11 @@ width: 100%; max-width: 100%; z-index: 40; + isolation: isolate; + + &.open { + z-index: 3200; + } } .more-export-settings-control { @@ -836,12 +876,13 @@ line-height: 1; white-space: nowrap; cursor: pointer; - transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease; + transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease, transform 0.12s ease; &:hover { border-color: var(--primary); color: var(--primary); background: color-mix(in srgb, var(--primary) 6%, var(--bg-secondary)); + transform: translateY(-1px); } } @@ -859,10 +900,11 @@ text-overflow: ellipsis; text-align: left; cursor: pointer; - transition: border-color 0.12s ease; + transition: border-color 0.12s ease, background 0.12s ease; &:hover { border-color: var(--primary); + background: color-mix(in srgb, var(--primary) 6%, var(--bg-secondary)); } &.active { @@ -877,16 +919,18 @@ right: auto; width: clamp(300px, 36vw, 420px); max-width: calc(100vw - 40px); - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: 12px; - box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28); + background: var(--bg-secondary-solid, #ffffff); + border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); + border-radius: 14px; + box-shadow: 0 22px 38px rgba(15, 23, 42, 0.2); padding: 6px; z-index: 3000; max-height: 260px; overflow-y: auto; + overflow-x: hidden; opacity: 0; transform: translateY(-4px); + transform-origin: top left; pointer-events: none; visibility: hidden; transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s step-end; @@ -899,6 +943,7 @@ pointer-events: auto; visibility: visible; transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s step-start; + animation: exportPopoverEnter 0.18s ease both; } } @@ -908,12 +953,14 @@ background: transparent; color: var(--text-primary); text-align: left; - padding: 8px 10px; + padding: 10px 10px; border-radius: 8px; cursor: pointer; display: flex; flex-direction: column; - gap: 2px; + gap: 3px; + min-height: 58px; + transition: background 0.12s ease, color 0.12s ease; &:hover { background: var(--bg-hover); @@ -939,7 +986,7 @@ .layout-prefix-toggle { margin-top: 4px; - padding: 10px; + padding: 11px 10px 10px; border-top: 1px solid color-mix(in srgb, var(--border-color) 85%, transparent); display: flex; align-items: center; @@ -1012,9 +1059,10 @@ min-width: 92px; min-height: 42px; margin-left: auto; - border: 1px solid var(--border-color); - border-radius: 12px; - background: var(--card-bg); + border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); + border-radius: 14px; + background: + linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 80%, var(--bg-primary)) 0%, var(--card-bg) 100%); color: var(--text-primary); padding: 10px 12px; display: inline-flex; @@ -1026,12 +1074,15 @@ cursor: pointer; flex-shrink: 0; align-self: stretch; - transition: border-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease, transform 0.12s ease; + transition: border-color 0.14s ease, color 0.14s ease, box-shadow 0.14s ease, transform 0.14s ease, background 0.14s ease; + animation: exportSectionReveal 0.46s ease both; &:hover { - border-color: var(--primary); + border-color: color-mix(in srgb, var(--primary) 48%, var(--border-color)); color: var(--primary); - transform: translateY(-1px); + transform: translateY(-2px); + background: color-mix(in srgb, var(--primary) 7%, var(--card-bg)); + box-shadow: 0 10px 18px rgba(15, 23, 42, 0.12); } &.has-alert { @@ -1047,8 +1098,9 @@ display: flex; align-items: center; justify-content: center; - z-index: 2300; + z-index: 7700; padding: 20px; + animation: exportOverlayFadeIn 0.2s ease both; } .export-defaults-modal { @@ -1061,6 +1113,7 @@ box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28); display: flex; flex-direction: column; + animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both; } .export-defaults-modal-header { @@ -1120,19 +1173,31 @@ .content-card-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); - gap: 8px; + grid-template-columns: repeat(auto-fit, minmax(142px, 1fr)); + gap: 10px; flex-shrink: 0; + animation: exportSectionReveal 0.52s ease both; } .content-card { - border: 1px solid var(--border-color); - border-radius: 12px; - background: var(--card-bg); - padding: 10px; + border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); + border-radius: 13px; + background: + linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 82%, var(--bg-primary)) 0%, var(--card-bg) 100%); + box-shadow: 0 10px 20px rgba(15, 23, 42, 0.06); + padding: 11px; display: flex; flex-direction: column; gap: 8px; + transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease, background 0.16s ease; + animation: exportCardReveal 0.42s ease both; + + &:hover { + border-color: color-mix(in srgb, var(--primary) 38%, var(--border-color)); + box-shadow: 0 16px 24px rgba(15, 23, 42, 0.11); + transform: translateY(-2px); + background: color-mix(in srgb, var(--primary) 4%, var(--card-bg)); + } .card-header { display: flex; @@ -1145,9 +1210,9 @@ display: flex; align-items: center; gap: 5px; - font-size: 13px; + font-size: 14px; color: var(--text-primary); - font-weight: 600; + font-weight: 700; } .card-title-meta { @@ -1172,12 +1237,12 @@ display: flex; align-items: center; justify-content: space-between; - font-size: 11px; + font-size: 12px; color: var(--text-secondary); strong { color: var(--text-primary); - font-size: 13px; + font-size: 14px; } } } @@ -1185,10 +1250,10 @@ .card-export-btn { margin-top: auto; border: 1px solid transparent; - border-radius: 7px; - padding: 7px 9px; + border-radius: 9px; + padding: 8px 10px; cursor: pointer; - font-size: 12px; + font-size: 13px; font-weight: 600; display: inline-flex; align-items: center; @@ -1203,6 +1268,8 @@ &.primary:hover { background: var(--primary-hover); + transform: translateY(-1px); + box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 24%, transparent); } &.secondary { @@ -1215,6 +1282,7 @@ border-color: color-mix(in srgb, var(--primary) 28%, transparent); color: var(--text-primary); background: color-mix(in srgb, var(--bg-primary) 94%, var(--primary) 6%); + transform: translateY(-1px); } &:disabled { @@ -1240,6 +1308,13 @@ } } +.content-card-grid .content-card:nth-child(1) { animation-delay: 0.03s; } +.content-card-grid .content-card:nth-child(2) { animation-delay: 0.07s; } +.content-card-grid .content-card:nth-child(3) { animation-delay: 0.11s; } +.content-card-grid .content-card:nth-child(4) { animation-delay: 0.15s; } +.content-card-grid .content-card:nth-child(5) { animation-delay: 0.19s; } +.content-card-grid .content-card:nth-child(6) { animation-delay: 0.23s; } + .count-loading { color: var(--text-tertiary); font-size: 12px; @@ -1255,12 +1330,13 @@ right: 0; bottom: 0; left: 0; - z-index: 1180; + z-index: 7600; background: rgba(15, 23, 42, 0.28); display: flex; align-items: flex-start; justify-content: center; padding: 24px 20px; + animation: exportOverlayFadeIn 0.2s ease both; } .task-center-modal { @@ -1273,6 +1349,7 @@ display: flex; flex-direction: column; overflow: hidden; + animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both; } .task-center-modal-header { @@ -1330,6 +1407,7 @@ gap: 10px; align-items: flex-start; background: var(--bg-secondary-solid, #ffffff); + animation: exportItemRise 0.2s ease both; &.running { border-color: var(--primary); @@ -1563,11 +1641,23 @@ } .session-table-section { - flex: 0 0 auto; - min-height: 420px; + flex: 1 1 420px; + min-height: 0; display: flex; flex-direction: column; overflow: visible; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + border-radius: 14px; + background: + linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 72%, var(--bg-primary)) 0%, color-mix(in srgb, var(--card-bg) 90%, var(--bg-primary)) 100%); + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.07); + animation: exportSectionReveal 0.58s ease both; + transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; + + &:hover { + border-color: color-mix(in srgb, var(--primary) 20%, var(--border-color)); + box-shadow: 0 18px 36px rgba(15, 23, 42, 0.09); + } } .table-stage-hint { @@ -1582,6 +1672,7 @@ color: var(--primary); font-size: 12px; width: fit-content; + animation: exportSectionReveal 0.35s ease both; } .table-toolbar { @@ -1590,9 +1681,10 @@ align-items: flex-start; gap: 12px; flex-wrap: wrap; - padding: 10px 12px; - border-bottom: 1px solid color-mix(in srgb, var(--border-color) 85%, transparent); - background: color-mix(in srgb, var(--bg-primary) 82%, var(--bg-secondary)); + padding: 12px 14px; + border-bottom: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: linear-gradient(180deg, color-mix(in srgb, var(--bg-primary) 88%, var(--card-bg)) 0%, color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary)) 100%); + transition: border-color 0.16s ease, background 0.16s ease; } .table-cache-meta { @@ -1627,7 +1719,8 @@ border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-secondary); - padding: 7px 6px; + min-height: 32px; + padding: 7px 10px; border-radius: 999px; cursor: pointer; font-size: 13px; @@ -1635,18 +1728,52 @@ display: inline-flex; align-items: center; justify-content: center; + transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease, transform 0.14s ease, box-shadow 0.14s ease; .tab-btn-content { display: inline-flex; align-items: center; - gap: 4px; + gap: 5px; line-height: 1; + + span:last-child { + min-width: 24px; + min-height: 18px; + padding: 0 6px; + border-radius: 999px; + background: color-mix(in srgb, var(--bg-primary) 82%, var(--bg-secondary)); + color: var(--text-secondary); + font-size: 11px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + font-variant-numeric: tabular-nums; + } + } + + &:hover { + border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color)); + color: var(--text-primary); + transform: translateY(-1px); + box-shadow: 0 7px 14px rgba(15, 23, 42, 0.08); } &.active { border-color: var(--primary); color: var(--primary); background: rgba(var(--primary-rgb), 0.12); + box-shadow: 0 6px 14px color-mix(in srgb, var(--primary) 24%, transparent); + + .tab-btn-content span:last-child { + background: color-mix(in srgb, var(--primary) 16%, var(--bg-secondary)); + color: color-mix(in srgb, var(--primary) 84%, var(--text-primary)); + } + } + + &:focus-visible { + outline: 2px solid color-mix(in srgb, var(--primary) 44%, transparent); + outline-offset: 2px; } } } @@ -1664,17 +1791,37 @@ align-items: center; gap: 8px; flex-wrap: wrap; + + .secondary-btn { + min-height: 34px; + border-radius: 10px; + transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease, transform 0.14s ease; + + &:hover:not(:disabled) { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color)); + color: var(--primary); + background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary)); + } + } } .search-input-wrap { display: flex; align-items: center; gap: 6px; - padding: 8px 10px; - border-radius: 8px; + padding: 8px 11px; + border-radius: 10px; border: 1px solid var(--border-color); - background: var(--bg-secondary); - min-width: 220px; + background: color-mix(in srgb, var(--bg-secondary) 92%, var(--bg-primary)); + min-width: 240px; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; + + &:focus-within { + border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color)); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 16%, transparent); + background: var(--bg-secondary); + } input { border: none; @@ -1682,15 +1829,27 @@ color: var(--text-primary); font-size: 13px; outline: none; - width: 180px; + width: 220px; } .clear-search { - border: none; - background: transparent; + border: 1px solid transparent; + background: color-mix(in srgb, var(--bg-primary) 84%, var(--bg-secondary)); color: var(--text-tertiary); cursor: pointer; - display: flex; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 999px; + transition: border-color 0.12s ease, background 0.12s ease, color 0.12s ease; + + &:hover { + border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color)); + color: var(--primary); + background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary)); + } } } @@ -1708,7 +1867,9 @@ .session-table-layout { display: flex; + flex: 1; min-height: 0; + padding: 10px; .table-wrap { flex: 1; @@ -1719,7 +1880,7 @@ .table-wrap { --contacts-native-scrollbar-compensation: 18px; --contacts-row-height: 76px; - --contacts-default-visible-rows: 10; + --contacts-default-visible-rows: 8; --contacts-default-list-height: calc(var(--contacts-row-height) * var(--contacts-default-visible-rows)); --contacts-select-col-width: 34px; --contacts-avatar-col-width: 44px; @@ -1731,32 +1892,49 @@ --contacts-message-col-width: 120px; --contacts-media-col-width: 72px; --contacts-action-col-width: 140px; + --contacts-actions-sticky-width: max(var(--contacts-action-col-width), 184px); --contacts-table-min-width: 1200px; overflow: hidden; - border: 1px solid var(--border-color); - border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + border-radius: 12px; min-height: 320px; height: auto; flex: 1; display: flex; flex-direction: column; - background: var(--bg-secondary); + background: linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 84%, var(--bg-primary)) 0%, var(--bg-secondary) 100%); + box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 18%, transparent); + transition: border-color 0.16s ease, box-shadow 0.16s ease; + + &:hover { + border-color: color-mix(in srgb, var(--primary) 22%, var(--border-color)); + box-shadow: + inset 0 1px 0 color-mix(in srgb, #fff 24%, transparent), + 0 8px 18px rgba(15, 23, 42, 0.06); + } } .table-wrap { .table-scroll-shell { overflow: hidden; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; } .table-scroll-viewport { + flex: 1; min-height: 0; overflow-x: auto; - overflow-y: visible; + overflow-y: hidden; scrollbar-width: none; -ms-overflow-style: none; background: var(--bg-secondary); padding-bottom: var(--contacts-native-scrollbar-compensation); margin-bottom: calc(-1 * var(--contacts-native-scrollbar-compensation)); + display: flex; + flex-direction: column; &::-webkit-scrollbar { display: none; @@ -1765,6 +1943,9 @@ .table-scroll-content { min-width: max(100%, var(--contacts-table-min-width)); + min-height: 0; + display: flex; + flex-direction: column; } .session-table-sticky { @@ -1902,6 +2083,7 @@ font-weight: 600; letter-spacing: 0.01em; flex-shrink: 0; + backdrop-filter: saturate(115%) blur(3px); &.is-draggable { cursor: grab; @@ -1938,7 +2120,7 @@ max-width: var(--contacts-main-col-width); display: flex; align-items: center; - gap: 8px; + gap: var(--contacts-column-gap); } .contacts-list-header-main-label { @@ -1977,8 +2159,8 @@ } .contacts-list-header-actions { - width: max(var(--contacts-action-col-width), 184px); - min-width: max(var(--contacts-action-col-width), 184px); + width: var(--contacts-actions-sticky-width); + min-width: var(--contacts-actions-sticky-width); display: flex; align-items: center; justify-content: flex-end; @@ -2007,8 +2189,8 @@ width: 100%; min-width: max(100%, var(--contacts-table-min-width)); flex: 1; - min-height: var(--contacts-default-list-height); - height: var(--contacts-default-list-height); + min-height: 0; + height: auto; position: relative; overflow-x: clip; overflow-y: auto; @@ -2074,10 +2256,13 @@ padding: 6px 10px; cursor: pointer; white-space: nowrap; + transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease, transform 0.14s ease; &:hover:not(:disabled) { border-color: var(--text-tertiary); color: var(--text-primary); + background: color-mix(in srgb, var(--bg-primary) 88%, var(--bg-secondary)); + transform: translateY(-1px); } &:disabled { @@ -2090,7 +2275,7 @@ border: none; border-radius: 8px; padding: 6px 10px; - background: var(--primary); + background: linear-gradient(180deg, color-mix(in srgb, var(--primary) 94%, #ffffff) 0%, var(--primary) 100%); color: #fff; font-size: 12px; cursor: pointer; @@ -2099,9 +2284,12 @@ gap: 6px; white-space: nowrap; flex-shrink: 0; + transition: transform 0.14s ease, box-shadow 0.14s ease, background 0.14s ease; &:hover:not(:disabled) { background: var(--primary-hover); + transform: translateY(-1px); + box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 30%, transparent); } .selection-export-count { @@ -2132,6 +2320,42 @@ &.selected .contact-item:hover { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 60%, transparent); + transform: none; + } + + &.selected-contiguous-bottom { + padding-bottom: 0; + } + + &.selected-contiguous-bottom .contact-item { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + box-shadow: + inset 1px 0 0 color-mix(in srgb, var(--primary) 52%, transparent), + inset -1px 0 0 color-mix(in srgb, var(--primary) 52%, transparent), + inset 0 1px 0 color-mix(in srgb, var(--primary) 52%, transparent); + } + + &.selected-contiguous-top .contact-item { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; + box-shadow: + inset 1px 0 0 color-mix(in srgb, var(--primary) 52%, transparent), + inset -1px 0 0 color-mix(in srgb, var(--primary) 52%, transparent), + inset 0 -1px 0 color-mix(in srgb, var(--primary) 52%, transparent); + } + + &.selected-contiguous-top.selected-contiguous-bottom .contact-item { + box-shadow: + inset 1px 0 0 color-mix(in srgb, var(--primary) 52%, transparent), + inset -1px 0 0 color-mix(in srgb, var(--primary) 52%, transparent); + } + + &.selected-contiguous-bottom .contact-item:hover, + &.selected-contiguous-top .contact-item:hover, + &.selected-contiguous-top.selected-contiguous-bottom .contact-item:hover { + box-shadow: inherit; } } @@ -2145,7 +2369,7 @@ height: 72px; box-sizing: border-box; border-radius: 10px; - transition: all 0.2s; + transition: box-shadow 0.18s ease, background 0.18s ease, transform 0.18s ease; cursor: default; background: var(--contacts-row-bg); box-shadow: inset 0 0 0 1px transparent; @@ -2153,6 +2377,7 @@ &:hover { background: var(--contacts-row-bg); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-tertiary) 24%, transparent); + transform: translateX(1px); } } @@ -2294,11 +2519,13 @@ color: var(--primary); font-variant-numeric: tabular-nums; cursor: pointer; + border-radius: 6px; + transition: color 0.12s ease, background 0.12s ease; &:hover { color: var(--primary-hover); - text-decoration: underline; - text-underline-offset: 2px; + background: color-mix(in srgb, var(--primary) 12%, transparent); + text-decoration: none; } &:focus-visible { @@ -2538,14 +2765,25 @@ justify-content: center; align-self: stretch; gap: 4px; - width: var(--contacts-action-col-width); - min-width: var(--contacts-action-col-width); + width: var(--contacts-actions-sticky-width); + min-width: var(--contacts-actions-sticky-width); flex-shrink: 0; position: sticky; right: 0; z-index: 10; background: var(--contacts-row-bg); + &::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: -9px; + width: 9px; + pointer-events: none; + background: linear-gradient(to right, transparent, var(--contacts-row-bg)); + } + .row-action-main { display: inline-flex; align-items: flex-start; @@ -2567,11 +2805,13 @@ font-size: 12px; cursor: pointer; white-space: nowrap; + transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease, transform 0.14s ease; &:hover { border-color: var(--text-tertiary); color: var(--text-primary); background: var(--bg-hover); + transform: translateY(-1px); } &.active { @@ -2596,7 +2836,7 @@ .row-export-link { border: none; - padding: 0; + padding: 2px 6px; margin: 0; background: transparent; color: var(--primary); @@ -2605,11 +2845,13 @@ line-height: 1.2; font-weight: 600; white-space: nowrap; + border-radius: 6px; + transition: color 0.12s ease, background 0.12s ease; &:hover:not(:disabled) { color: var(--primary-hover); - text-decoration: underline; - text-underline-offset: 2px; + background: color-mix(in srgb, var(--primary) 10%, transparent); + text-decoration: none; } &:disabled { @@ -2661,45 +2903,50 @@ .export-session-detail-overlay { position: fixed; - top: 40px; - right: 0; - bottom: 0; - left: 0; - z-index: 1100; + inset: 0; + z-index: 7900; display: flex; + align-items: stretch; justify-content: flex-end; - background: rgba(15, 23, 42, 0.24); + padding: 12px; + background: rgba(15, 23, 42, 0.42); + backdrop-filter: blur(1px); + animation: exportOverlayFadeIn 0.2s ease both; } .export-session-detail-panel { - width: min(360px, calc(100vw - 16px)); - height: calc(100vh - 40px); - border-left: 1px solid var(--border-color); - border-radius: 0; - background: var(--bg-secondary-solid, #ffffff); + width: min(448px, calc(100vw - 24px)); + height: 100%; + max-height: calc(100vh - 24px); + border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); + border-radius: 16px; + background: + linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 82%, var(--bg-primary)) 0%, var(--bg-secondary-solid, #ffffff) 100%); display: flex; flex-direction: column; overflow: hidden; - box-shadow: -12px 0 30px rgba(0, 0, 0, 0.18); + box-shadow: -18px 0 40px rgba(0, 0, 0, 0.24); + animation: exportDetailPanelIn 0.26s cubic-bezier(0.22, 0.8, 0.24, 1) both; .detail-header { display: flex; align-items: center; justify-content: space-between; - padding: 14px; - border-bottom: 1px solid var(--border-color); + padding: 16px 16px 12px; + border-bottom: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + background: color-mix(in srgb, var(--bg-primary) 92%, var(--card-bg)); .detail-header-main { display: flex; align-items: center; - gap: 10px; + gap: 12px; min-width: 0; } .detail-header-avatar { - width: 32px; - height: 32px; - border-radius: 8px; + width: 36px; + height: 36px; + border-radius: 10px; background: linear-gradient(135deg, var(--primary), var(--primary-hover)); display: flex; align-items: center; @@ -2725,8 +2972,8 @@ h4 { margin: 0; - font-size: 14px; - font-weight: 600; + font-size: 15px; + font-weight: 700; color: var(--text-primary); line-height: 1.2; overflow: hidden; @@ -2737,7 +2984,7 @@ .detail-header-id { margin-top: 3px; - font-size: 11px; + font-size: 12px; color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; @@ -2745,19 +2992,21 @@ } .close-btn { - border: none; - background: transparent; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary)); color: var(--text-secondary); - width: 26px; - height: 26px; - border-radius: 6px; + width: 30px; + height: 30px; + border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; + transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease; &:hover { - background: var(--bg-hover); + border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary)); color: var(--text-primary); } } @@ -2779,11 +3028,16 @@ flex: 1; min-height: 0; overflow-y: auto; - padding: 14px; + padding: 12px; + background: color-mix(in srgb, var(--bg-secondary-solid, #ffffff) 94%, var(--bg-primary)); } .detail-section { - margin-bottom: 18px; + margin-bottom: 12px; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + background: color-mix(in srgb, var(--bg-primary) 90%, var(--bg-secondary)); &:last-child { margin-bottom: 0; @@ -2793,17 +3047,16 @@ display: flex; align-items: center; gap: 6px; - font-size: 12px; + font-size: 13px; font-weight: 600; color: var(--text-secondary); - margin-bottom: 10px; - text-transform: uppercase; - letter-spacing: 0.4px; + margin-bottom: 8px; + letter-spacing: 0.1px; } .detail-stats-meta { - margin-top: -4px; - margin-bottom: 10px; + margin-top: 0; + margin-bottom: 8px; font-size: 12px; color: var(--text-tertiary); } @@ -2813,8 +3066,9 @@ display: flex; align-items: center; gap: 8px; + min-height: 34px; padding: 8px 0; - border-bottom: 1px solid var(--border-color); + border-bottom: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); font-size: 13px; &:last-child { @@ -2824,6 +3078,7 @@ .label { color: var(--text-secondary); flex-shrink: 0; + min-width: 70px; } .value { @@ -2840,14 +3095,15 @@ } .detail-inline-btn { - border: none; - background: var(--bg-secondary); + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary)); color: var(--primary); - border-radius: 6px; + border-radius: 7px; padding: 4px 8px; font-size: 12px; line-height: 1; cursor: pointer; + transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease; &:disabled { cursor: not-allowed; @@ -2855,7 +3111,8 @@ } &:hover:not(:disabled) { - background: var(--bg-hover); + border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary)); } } @@ -2880,7 +3137,7 @@ transition: opacity 0.15s, color 0.15s, background 0.15s; &:hover { - background: var(--bg-secondary); + background: color-mix(in srgb, var(--bg-primary) 84%, var(--bg-secondary)); color: var(--text-primary); } } @@ -2905,38 +3162,109 @@ } .detail-record-item { - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 8px 10px; - background: var(--bg-primary); + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + border-radius: 10px; + padding: 10px; + background: color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary)); + transition: border-color 0.14s ease, box-shadow 0.14s ease; - .record-row { + &:hover { + border-color: color-mix(in srgb, var(--primary) 28%, var(--border-color)); + box-shadow: 0 6px 12px rgba(15, 23, 42, 0.08); + } + + .detail-record-head { display: flex; align-items: center; - gap: 8px; - padding: 4px 0; + justify-content: space-between; + gap: 10px; + padding-bottom: 8px; + border-bottom: 1px dashed color-mix(in srgb, var(--border-color) 78%, transparent); + } + + .record-export-time { + font-size: 13px; + font-weight: 700; + color: var(--text-primary); + font-variant-numeric: tabular-nums; + white-space: nowrap; + } + + .record-content-pill { + min-width: 0; + max-width: 62%; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary)); + color: var(--text-secondary); font-size: 12px; + padding: 3px 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; + } - .label { - color: var(--text-secondary); - width: 56px; - flex-shrink: 0; + .detail-record-path-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + padding-top: 8px; + font-size: 12px; + } + + .path-label { + color: var(--text-secondary); + flex-shrink: 0; + white-space: nowrap; + } + + .path-value { + color: var(--text-primary); + min-width: 0; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break: normal; + } + + .detail-inline-btn { + flex-shrink: 0; + min-height: 26px; + padding: 0 10px; + border-radius: 7px; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + background: color-mix(in srgb, var(--bg-primary) 88%, var(--bg-secondary)); + color: var(--text-primary); + font-size: 12px; + font-weight: 600; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease, transform 0.14s ease; + + &:hover:not(:disabled) { + border-color: color-mix(in srgb, var(--primary) 38%, var(--border-color)); + color: var(--primary); + background: color-mix(in srgb, var(--primary) 9%, var(--bg-secondary)); + transform: translateY(-1px); } - .value { - color: var(--text-primary); - flex: 1; - text-align: right; - word-break: break-all; - - &.path { - text-align: left; - } + &:focus-visible { + outline: 2px solid color-mix(in srgb, var(--primary) 38%, transparent); + outline-offset: 2px; } + } - .detail-inline-btn { - flex-shrink: 0; - } + .detail-record-open-btn { + appearance: none; + -webkit-appearance: none; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + box-shadow: none; } } @@ -2959,8 +3287,9 @@ align-items: center; justify-content: space-between; padding: 10px 12px; - border-radius: 8px; - background: var(--bg-secondary); + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + background: color-mix(in srgb, var(--bg-primary) 90%, var(--bg-secondary)); font-size: 12px; .db-name { @@ -2977,12 +3306,13 @@ .export-session-sns-overlay { position: fixed; inset: 0; - z-index: 1200; + z-index: 7880; display: flex; align-items: center; justify-content: center; padding: 24px 16px; background: rgba(15, 23, 42, 0.38); + animation: exportOverlayFadeIn 0.2s ease both; } .export-session-sns-dialog { @@ -2995,6 +3325,7 @@ display: flex; flex-direction: column; overflow: hidden; + animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both; .sns-dialog-header { display: flex; @@ -3380,22 +3711,23 @@ .export-dialog-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.45); - backdrop-filter: blur(4px); + background: rgba(15, 18, 28, 0.48); + backdrop-filter: blur(7px); display: flex; align-items: center; justify-content: center; - padding: 16px; + padding: 20px; z-index: 1000; } .export-dialog { - width: min(1080px, calc(100vw - 32px)); - max-height: calc(100vh - 32px); + width: min(860px, calc(100vw - 40px)); + max-height: calc(100vh - 40px); background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 14px; - padding: 14px 14px 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + border-radius: 16px; + box-shadow: 0 26px 52px rgba(0, 0, 0, 0.3); + padding: 16px 16px 14px; display: flex; flex-direction: column; overflow: hidden; @@ -3406,20 +3738,33 @@ overflow-y: auto; display: flex; flex-direction: column; - gap: 10px; - padding-right: 14px; + gap: 12px; + padding-right: 6px; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-thumb { + border-radius: 999px; + background: color-mix(in srgb, var(--text-tertiary) 48%, transparent); + } } .dialog-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 10px; + margin-bottom: 12px; + padding: 2px 2px 0; + gap: 12px; h3 { margin: 0; color: var(--text-primary); - font-size: 18px; + font-size: 22px; + line-height: 1.2; + letter-spacing: 0.2px; } } @@ -3432,35 +3777,44 @@ .dialog-header-note { font-size: 12px; - line-height: 1.45; - color: var(--text-secondary); + line-height: 1.5; + color: var(--text-tertiary); } .close-icon-btn { - border: 1px solid var(--border-color); - background: var(--bg-secondary); - border-radius: 8px; - width: 30px; - height: 30px; + border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 86%, var(--bg-primary)); + border-radius: 10px; + width: 34px; + height: 34px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--text-secondary); + transition: border-color 0.16s ease, color 0.16s ease, transform 0.16s ease, background 0.16s ease; + + &:hover { + border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color)); + color: var(--primary); + background: color-mix(in srgb, var(--primary) 7%, var(--bg-primary)); + transform: translateY(-1px); + } } .dialog-section { - border: 1px solid var(--border-color); - border-radius: 10px; - padding: 12px; - background: var(--bg-secondary); + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + border-radius: 12px; + padding: 13px 14px; + background: color-mix(in srgb, var(--bg-secondary) 78%, var(--bg-primary)); h4 { - margin: 0 0 8px; - font-size: 13px; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.4px; + margin: 0 0 10px; + font-size: 15px; + color: var(--text-primary); + font-weight: 700; + letter-spacing: 0.2px; + line-height: 1.3; } } @@ -3476,21 +3830,23 @@ } .time-range-trigger { - border: 1px solid var(--border-color); - background: var(--bg-primary); + border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); + background: color-mix(in srgb, var(--bg-primary) 68%, var(--bg-secondary)); border-radius: 999px; color: var(--text-primary); - font-size: 12px; - min-height: 32px; - padding: 0 10px; + font-size: 13px; + min-height: 36px; + padding: 0 12px; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; + transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease; &:hover { - border-color: rgba(var(--primary-rgb), 0.45); + border-color: rgba(var(--primary-rgb), 0.52); color: var(--primary); + background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary)); } .time-range-arrow { @@ -3577,11 +3933,11 @@ .scope-tag { border-radius: 999px; - background: rgba(var(--primary-rgb), 0.15); + background: rgba(var(--primary-rgb), 0.14); color: var(--primary); - padding: 4px 10px; + padding: 6px 11px; font-size: 12px; - font-weight: 600; + font-weight: 700; } .scope-count { @@ -3593,64 +3949,70 @@ margin-top: 8px; display: flex; flex-wrap: wrap; - gap: 6px; + gap: 7px; max-height: 120px; overflow: auto; } .scope-item { - background: var(--bg-primary); - border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--bg-primary) 68%, var(--bg-secondary)); + border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); border-radius: 999px; - padding: 4px 9px; + padding: 6px 11px; font-size: 12px; - color: var(--text-primary); + color: var(--text-secondary); } .format-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 6px; + gap: 8px; } .format-note { - margin: 0 0 8px; + margin: 0 0 10px; font-size: 12px; - line-height: 1.45; + line-height: 1.55; color: var(--text-secondary); } .format-card { width: 100%; min-height: 0; - border: 1px solid var(--border-color); - border-radius: 10px; - padding: 8px 10px; + border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); + border-radius: 11px; + padding: 10px 11px; text-align: left; - background: var(--bg-primary); + background: color-mix(in srgb, var(--bg-primary) 70%, var(--bg-secondary)); cursor: pointer; display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; + transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; .format-label { - font-size: 13px; + font-size: 14px; font-weight: 600; color: var(--text-primary); line-height: 1.35; } .format-desc { - margin-top: 1px; - font-size: 11px; + margin-top: 2px; + font-size: 12px; color: var(--text-tertiary); - line-height: 1.35; + line-height: 1.45; + } + + &:hover { + border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color)); + transform: translateY(-1px); } &.active { - border-color: var(--primary); - background: rgba(var(--primary-rgb), 0.08); + border-color: color-mix(in srgb, var(--primary) 75%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 11%, var(--bg-secondary)); } } @@ -3685,24 +4047,281 @@ } } -.media-check-grid { - margin-top: 10px; +.media-section-header { + margin-bottom: 10px; +} + +.media-selection-pill { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--primary) 42%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 12%, var(--bg-secondary)); + color: color-mix(in srgb, var(--primary) 80%, var(--text-primary)); + min-height: 28px; + padding: 0 10px; + font-size: 12px; + font-weight: 700; + line-height: 1; +} + +.media-option-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); + gap: 9px; +} + +.media-option-card { + position: relative; + border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); + border-radius: 12px; + background: color-mix(in srgb, var(--bg-primary) 72%, var(--bg-secondary)); + min-height: 74px; + padding: 10px 11px; display: flex; align-items: center; - flex-wrap: wrap; - gap: 8px 16px; + justify-content: space-between; + gap: 9px; + cursor: pointer; + transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; - label { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--text-primary); - white-space: nowrap; + &:hover { + border-color: color-mix(in srgb, var(--primary) 46%, var(--border-color)); + transform: translateY(-1px); } - input[type='checkbox'] { - accent-color: var(--primary); + &:has(.media-option-input:focus-visible) { + outline: 2px solid color-mix(in srgb, var(--primary) 38%, transparent); + outline-offset: 1px; + } + + &.active { + border-color: color-mix(in srgb, var(--primary) 76%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 12%, var(--bg-secondary)); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 28%, transparent); + } +} + +.media-option-input { + position: absolute; + inset: 0; + opacity: 0; + pointer-events: none; +} + +.media-option-main { + min-width: 0; + display: inline-flex; + align-items: center; + gap: 9px; +} + +.media-option-icon { + width: 30px; + height: 30px; + border-radius: 9px; + border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); + background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary)); + color: var(--text-secondary); + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.media-option-card.active .media-option-icon { + border-color: color-mix(in srgb, var(--primary) 58%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 18%, var(--bg-secondary)); + color: var(--primary); +} + +.media-option-text { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.media-option-label { + font-size: 13px; + line-height: 1.3; + font-weight: 700; + color: var(--text-primary); +} + +.media-option-desc { + font-size: 11px; + line-height: 1.4; + color: var(--text-secondary); +} + +.media-option-check { + width: 18px; + height: 18px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: color-mix(in srgb, var(--bg-primary) 76%, var(--bg-secondary)); + color: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease; +} + +.media-option-check.active { + border-color: color-mix(in srgb, var(--primary) 85%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 90%, var(--bg-secondary)); + color: #fff; +} + +.dialog-collapse-slot { + display: grid; + grid-template-rows: 0fr; + margin-top: 0; + opacity: 0; + transform: translateY(-6px); + pointer-events: none; + transition: grid-template-rows 0.24s ease, opacity 0.18s ease, transform 0.24s ease, margin-top 0.24s ease; +} + +.dialog-collapse-slot.open { + grid-template-rows: 1fr; + margin-top: 12px; + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.dialog-collapse-inner { + min-height: 0; + overflow: hidden; +} + +.file-size-subsection { + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: color-mix(in srgb, var(--bg-primary) 64%, var(--bg-secondary)); + padding: 11px 12px; + display: flex; + flex-direction: column; + gap: 10px; + transition: opacity 0.16s ease; +} + +.file-size-subsection.disabled { + opacity: 0.66; +} + +.file-size-subsection-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.file-size-heading { + font-size: 13px; + font-weight: 700; + color: var(--text-primary); +} + +.file-size-current { + font-size: 12px; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} + +.file-size-note { + font-size: 12px; + line-height: 1.5; + color: var(--text-secondary); +} + +.file-size-preset-row { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.file-size-preset-btn { + border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--bg-primary) 78%, var(--bg-secondary)); + color: var(--text-secondary); + min-height: 29px; + padding: 0 10px; + font-size: 12px; + font-weight: 600; + line-height: 1; + cursor: pointer; + transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease; + + &:hover:not(:disabled) { + border-color: color-mix(in srgb, var(--primary) 45%, var(--border-color)); + color: var(--text-primary); + } + + &.active { + border-color: color-mix(in srgb, var(--primary) 72%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 12%, var(--bg-secondary)); + color: color-mix(in srgb, var(--primary) 82%, var(--text-primary)); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.58; + } +} + +.dialog-input-row { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 2px; + + input { + width: 128px; + height: 36px; + border-radius: 9px; + border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); + background: color-mix(in srgb, var(--bg-primary) 72%, var(--bg-secondary)); + color: var(--text-primary); + font-size: 15px; + font-variant-numeric: tabular-nums; + padding: 0 10px; + appearance: textfield; + -moz-appearance: textfield; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; + + &:focus { + outline: none; + border-color: color-mix(in srgb, var(--primary) 70%, var(--border-color)); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 16%, transparent); + background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary)); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.68; + border-color: color-mix(in srgb, var(--border-color) 92%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 90%, var(--bg-primary)); + } + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + } + + span { + font-size: 14px; + font-weight: 600; + letter-spacing: 0.15px; + line-height: 1; + color: var(--text-secondary); } } @@ -3732,14 +4351,15 @@ flex-shrink: 0; width: 46px; height: 26px; - border: none; + border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); border-radius: 999px; background: color-mix(in srgb, var(--text-tertiary) 45%, transparent); cursor: pointer; - transition: background 0.2s ease; + transition: background 0.2s ease, border-color 0.2s ease; &.on { background: var(--primary); + border-color: color-mix(in srgb, var(--primary) 90%, var(--border-color)); } } @@ -3761,80 +4381,96 @@ .display-name-options { display: grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } .display-name-item { - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 8px; + border: 1px solid color-mix(in srgb, var(--border-color) 85%, transparent); + border-radius: 10px; + padding: 10px; width: 100%; + min-height: 86px; display: flex; flex-direction: column; - gap: 2px; - background: var(--bg-primary); + gap: 4px; + background: color-mix(in srgb, var(--bg-primary) 72%, var(--bg-secondary)); text-align: left; cursor: pointer; color: inherit; font: inherit; appearance: none; -webkit-appearance: none; + transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; &:focus-visible { outline: 2px solid rgba(var(--primary-rgb), 0.35); outline-offset: 1px; } + &:hover { + border-color: color-mix(in srgb, var(--primary) 44%, var(--border-color)); + transform: translateY(-1px); + } + span { - font-size: 12px; + font-size: 13px; color: var(--text-primary); - font-weight: 600; + font-weight: 700; } small { color: var(--text-secondary); - font-size: 11px; - line-height: 1.4; + font-size: 12px; + line-height: 1.45; } &.active { - border-color: var(--primary); - background: rgba(var(--primary-rgb), 0.08); + border-color: color-mix(in srgb, var(--primary) 76%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 11%, var(--bg-secondary)); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 30%, transparent); } } .dialog-actions { - margin-top: 10px; - padding-top: 10px; + margin-top: 12px; + padding-top: 12px; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; - gap: 8px; + gap: 10px; flex-shrink: 0; - background: var(--card-bg); + background: linear-gradient( + 180deg, + transparent, + var(--card-bg) 38% + ); } .primary-btn, .secondary-btn { - border-radius: 8px; - padding: 7px 12px; - font-size: 12px; + border-radius: 10px; + min-height: 38px; + padding: 8px 14px; + font-size: 13px; font-weight: 600; border: 1px solid var(--border-color); display: inline-flex; align-items: center; gap: 6px; cursor: pointer; + transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease, transform 0.15s ease; } .primary-btn { border-color: var(--primary); background: var(--primary); color: #fff; + min-width: 168px; &:hover { background: var(--primary-hover); + transform: translateY(-1px); } &:disabled { @@ -3844,12 +4480,15 @@ } .secondary-btn { - background: var(--bg-secondary); + background: color-mix(in srgb, var(--bg-secondary) 85%, var(--bg-primary)); color: var(--text-primary); + min-width: 112px; &:hover { border-color: var(--primary); color: var(--primary); + background: color-mix(in srgb, var(--primary) 7%, var(--bg-secondary)); + transform: translateY(-1px); } } @@ -4081,6 +4720,96 @@ justify-content: flex-end; } +@keyframes exportPageEnter { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes exportSectionReveal { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes exportCardReveal { + 0% { + opacity: 0; + transform: translateY(14px) scale(0.987); + } + + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes exportOverlayFadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes exportModalPopIn { + 0% { + opacity: 0; + transform: translateY(10px) scale(0.985); + } + + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes exportDetailPanelIn { + 0% { + opacity: 0; + transform: translateX(16px) scale(0.995); + } + + 100% { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +@keyframes exportPopoverEnter { + 0% { + opacity: 0; + transform: translateY(-8px) scale(0.985); + } + + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes exportItemRise { + from { + opacity: 0; + transform: translateY(6px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + @keyframes exportSpin { from { transform: rotate(0deg); @@ -4124,6 +4853,49 @@ } } +@media (prefers-reduced-motion: reduce) { + .export-board-page, + .export-top-panel, + .export-section-title-row, + .global-export-controls, + .task-center-card, + .content-card-grid, + .content-card, + .session-table-section, + .table-stage-hint, + .dialog-collapse-slot { + animation: none !important; + transition: none !important; + transform: none !important; + } + + .animated-ellipsis, + .session-load-detail-entry-bar, + .task-center-card-badge, + .spin { + animation: none !important; + } + + .session-load-detail-overlay, + .task-center-modal-overlay, + .export-defaults-modal-overlay, + .session-mutual-friends-overlay, + .export-session-detail-overlay, + .session-load-detail-modal, + .task-center-modal, + .export-defaults-modal, + .session-mutual-friends-modal, + .export-session-detail-panel, + .export-session-sns-overlay, + .export-session-sns-dialog, + .task-card, + .layout-dropdown.open { + animation: none !important; + transition: none !important; + transform: none !important; + } +} + @media (max-width: 1360px) { .export-top-bar { gap: 10px; @@ -4142,11 +4914,11 @@ } .display-name-options { - grid-template-columns: 1fr; + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .media-check-grid { - gap: 8px 12px; + .media-option-grid { + gap: 8px; } } @@ -4292,6 +5064,77 @@ width: calc(100vw - 20px); max-height: calc(100vh - 20px); padding: 12px 10px 10px; + border-radius: 14px; + } + + .dialog-header { + margin-bottom: 10px; + + h3 { + font-size: 19px; + } + } + + .dialog-section { + padding: 11px 11px; + + h4 { + font-size: 14px; + } + } + + .section-header-action { + align-items: flex-start; + flex-direction: column; + gap: 8px; + } + + .time-range-trigger { + width: 100%; + justify-content: space-between; + } + + .display-name-options { + grid-template-columns: 1fr; + } + + .dialog-input-row { + width: 100%; + display: flex; + align-items: center; + + input { + width: 100%; + min-width: 0; + flex: 1; + } + } + + .dialog-actions { + display: grid; + grid-template-columns: 1fr 1.35fr; + gap: 8px; + } + + .secondary-btn, + .primary-btn { + min-width: 0; + width: 100%; + justify-content: center; + } + + .media-option-grid { + grid-template-columns: 1fr; + } + + .media-option-card { + min-height: 68px; + } + + .file-size-subsection-header { + align-items: flex-start; + flex-direction: column; + gap: 4px; } .format-grid { diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index a1d1f5c..dbfcfc1 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -12,6 +12,7 @@ import { Database, Download, ExternalLink, + File as FileIcon, FolderOpen, Hash, Image as ImageIcon, @@ -201,6 +202,7 @@ const contentTypeLabels: Record = { emoji: '表情包', file: '文件' } +const FILE_SIZE_PRESETS_MB = [0, 100, 200, 500, 1024] as const const backgroundTaskSourceLabels: Record = { export: '导出页', @@ -1210,16 +1212,18 @@ const WriteLayoutSelector = memo(function WriteLayoutSelector({ const writeLayoutLabel = writeLayoutOptions.find(option => option.value === writeLayout)?.label || 'A(类型分目录)' return ( -
+
写入目录方式 -
+
{writeLayoutOptions.map(option => (
-
+
, + document.body ) }) @@ -6491,6 +6496,10 @@ function ExportPage() { const useCollapsedSessionFormatSelector = isSessionScopeDialog || isContentTextDialog const shouldShowFormatSection = !isContentScopeDialog || isContentTextDialog const shouldShowMediaSection = !isContentScopeDialog + const shouldRenderImageDeepSearchToggle = exportDialog.scope !== 'sns' && ( + isSessionScopeDialog || + (isContentScopeDialog && exportDialog.contentType === 'image') + ) const shouldShowImageDeepSearchToggle = exportDialog.scope !== 'sns' && ( (isSessionScopeDialog && options.exportImages) || (isContentScopeDialog && exportDialog.contentType === 'image') @@ -6500,6 +6509,80 @@ function ExportPage() { const activeDialogFormatLabel = exportDialog.scope === 'sns' ? (snsFormatOptions.find(option => option.value === snsExportFormat)?.label ?? snsExportFormat) : (formatOptions.find(option => option.value === options.format)?.label ?? options.format) + const sessionMediaOptions = [ + { + key: 'images', + label: '图片', + desc: '聊天图片与缩略图', + icon: ImageIcon, + checked: options.exportImages, + onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportImages: checked })) + }, + { + key: 'voices', + label: '语音', + desc: '语音消息文件', + icon: Mic, + checked: options.exportVoices, + onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportVoices: checked })) + }, + { + key: 'videos', + label: '视频', + desc: '聊天视频与封面', + icon: Video, + checked: options.exportVideos, + onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportVideos: checked })) + }, + { + key: 'emojis', + label: '表情包', + desc: '静态与动态表情', + icon: MessageSquare, + checked: options.exportEmojis, + onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportEmojis: checked })) + }, + { + key: 'files', + label: '文件', + desc: '文档与附件', + icon: FileIcon, + checked: options.exportFiles, + onToggle: (checked: boolean) => setOptions(prev => ({ ...prev, exportFiles: checked })) + } + ] + const snsMediaOptions = [ + { + key: 'images', + label: '图片', + desc: '朋友圈图片', + icon: ImageIcon, + checked: snsExportImages, + onToggle: (checked: boolean) => setSnsExportImages(checked) + }, + { + key: 'live-photos', + label: '实况图', + desc: 'Live Photo', + icon: Aperture, + checked: snsExportLivePhotos, + onToggle: (checked: boolean) => setSnsExportLivePhotos(checked) + }, + { + key: 'videos', + label: '视频', + desc: '朋友圈视频', + icon: Video, + checked: snsExportVideos, + onToggle: (checked: boolean) => setSnsExportVideos(checked) + } + ] + const dialogMediaOptions = exportDialog.scope === 'sns' ? snsMediaOptions : sessionMediaOptions + const mediaSelectionSummaryLabel = `已选择 ${dialogMediaOptions.filter(option => option.checked).length}/${dialogMediaOptions.length}` + const voiceAsTextStatusLabel = options.exportVoices + ? '已勾选导出语音:会同时导出语音文件,并在文本中追加语音转写结果。' + : '未勾选导出语音时,仅在文本里追加语音转写结果,不导出语音文件。' + const fileSizeLimitLabel = options.maxFileSizeMb <= 0 ? '不限' : `${options.maxFileSizeMb} MB` const shouldShowDisplayNameSection = !( exportDialog.scope === 'sns' || ( @@ -6518,8 +6601,9 @@ function ExportPage() { const taskQueuedCount = tasks.filter(task => task.status === 'queued').length const taskCenterAlertCount = taskRunningCount + taskQueuedCount const hasFilteredContacts = filteredContacts.length > 0 + const CONTACTS_ACTION_STICKY_WIDTH = 184 const contactsTableMinWidth = useMemo(() => { - const baseWidth = 24 + 34 + 44 + 280 + 120 + (4 * 72) + 140 + (8 * 12) + const baseWidth = 24 + 34 + 44 + 280 + 120 + (4 * 72) + CONTACTS_ACTION_STICKY_WIDTH + (8 * 12) const snsWidth = shouldShowSnsColumn ? 72 + 12 : 0 const mutualFriendsWidth = shouldShowMutualFriendsColumn ? 72 + 12 : 0 return baseWidth + snsWidth + mutualFriendsWidth @@ -6710,7 +6794,7 @@ function ExportPage() { const toggleTaskPerfDetail = useCallback((taskId: string) => { setExpandedPerfTaskId(prev => (prev === taskId ? null : taskId)) }, []) - const renderContactRow = useCallback((_: number, contact: ContactInfo) => { + const renderContactRow = useCallback((index: number, contact: ContactInfo) => { const matchedSession = sessionRowByUsername.get(contact.username) const canExport = Boolean(matchedSession?.hasSession) const isSessionBindingPending = !matchedSession && (isLoading || isSessionEnriching) @@ -6776,8 +6860,20 @@ function ExportPage() { : contact.type === 'group' ? '打开群聊' : '打开对话' + const previousContact = index > 0 ? filteredContacts[index - 1] : null + const nextContact = index < filteredContacts.length - 1 ? filteredContacts[index + 1] : null + const previousCanExport = Boolean(previousContact && sessionRowByUsername.get(previousContact.username)?.hasSession) + const nextCanExport = Boolean(nextContact && sessionRowByUsername.get(nextContact.username)?.hasSession) + const previousSelected = Boolean(previousContact && previousCanExport && selectedSessions.has(previousContact.username)) + const nextSelected = Boolean(nextContact && nextCanExport && selectedSessions.has(nextContact.username)) + const rowClassName = [ + 'contact-row', + checked ? 'selected' : '', + checked && previousSelected ? 'selected-contiguous-top' : '', + checked && nextSelected ? 'selected-contiguous-bottom' : '' + ].filter(Boolean).join(' ') return ( -
+
@@ -6926,6 +7022,7 @@ function ExportPage() {
) }, [ + filteredContacts, lastExportBySession, navigate, nowTick, @@ -7095,7 +7192,7 @@ function ExportPage() { onTogglePerfTask={toggleTaskPerfDetail} /> - {isExportDefaultsModalOpen && ( + {isExportDefaultsModalOpen && createPortal(
setIsExportDefaultsModalOpen(false)} @@ -7133,7 +7230,8 @@ function ExportPage() {
-
+
, + document.body )}
@@ -7218,7 +7316,7 @@ function ExportPage() { ]} />
- {showSessionLoadDetailModal && ( + {showSessionLoadDetailModal && createPortal(
setShowSessionLoadDetailModal(false)} @@ -7663,10 +7761,11 @@ function ExportPage() {
-
+
, + document.body )} - {sessionMutualFriendsDialogTarget && sessionMutualFriendsDialogMetric && ( + {sessionMutualFriendsDialogTarget && sessionMutualFriendsDialogMetric && createPortal(
-
+
, + document.body )} - {showSessionDetailPanel && ( + {showSessionDetailPanel && createPortal(
{currentSessionExportRecords.map((record, index) => (
-
- 导出时间 - {formatYmdHmDateTime(record.exportTime)} +
+ {formatYmdHmDateTime(record.exportTime)} + {record.content}
-
- 导出内容 - {record.content} -
-
- 导出目录 - {formatPathBrief(record.outputDir)} +
+ 导出目录 + {formatPathBrief(record.outputDir)} + ))} +
+
+ { + const raw = Number(event.target.value) + setOptions(prev => ({ ...prev, maxFileSizeMb: Number.isFinite(raw) ? Math.max(0, Math.floor(raw)) : 0 })) + }} + /> + MB +
+
+
+
)}
)} - {shouldShowMediaSection && exportDialog.scope !== 'sns' && options.exportFiles && ( -
-

文件大小上限

-
仅导出不超过该大小的文件,0 表示不限制。
-
- { - const raw = Number(event.target.value) - setOptions(prev => ({ ...prev, maxFileSizeMb: Number.isFinite(raw) ? Math.max(0, Math.floor(raw)) : 0 })) - }} - /> - MB -
-
- )} - - {shouldShowImageDeepSearchToggle && ( -
-
-
-

缺图时深度搜索

-
关闭后仅尝试 hardlink 命中,未命中将直接显示占位符,导出速度更快。
+ {shouldRenderImageDeepSearchToggle && ( +
+
+
+
+
+

缺图时深度搜索

+
关闭后仅尝试 hardlink 命中,未命中将直接显示占位符,导出速度更快。
+
+ +
-
)} @@ -8262,6 +8396,7 @@ function ExportPage() {

语音转文字

默认状态跟随更多导出设置中的语音转文字开关。
+
{voiceAsTextStatusLabel}