This commit is contained in:
1 parent
0497d6e31e
commit
4daeb59b6b
12 files changed
+207
-26
No files matched your search
@@ -60,6 +60,7 @@ interface ConfigSchema {
|
|||||||
transcribeLanguages: string[]
|
transcribeLanguages: string[]
|
||||||
exportDefaultConcurrency: number
|
exportDefaultConcurrency: number
|
||||||
exportDefaultPathStyle: 'auto' | 'posix' | 'windows'
|
exportDefaultPathStyle: 'auto' | 'posix' | 'windows'
|
||||||
|
exportDefaultDisplayNamePreference: 'group-nickname' | 'remark' | 'nickname'
|
||||||
analyticsExcludedUsernames: string[]
|
analyticsExcludedUsernames: string[]
|
||||||
|
|
||||||
// 安全相关
|
// 安全相关
|
||||||
@@ -218,6 +219,7 @@ export class ConfigService {
|
|||||||
transcribeLanguages: ['zh'],
|
transcribeLanguages: ['zh'],
|
||||||
exportDefaultConcurrency: 4,
|
exportDefaultConcurrency: 4,
|
||||||
exportDefaultPathStyle: 'auto',
|
exportDefaultPathStyle: 'auto',
|
||||||
|
exportDefaultDisplayNamePreference: 'remark',
|
||||||
analyticsExcludedUsernames: [],
|
analyticsExcludedUsernames: [],
|
||||||
authEnabled: false,
|
authEnabled: false,
|
||||||
authPassword: '',
|
authPassword: '',
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export function getPreferredDisplayName(
|
|||||||
case 'remark':
|
case 'remark':
|
||||||
return remark || nickname || wxid
|
return remark || nickname || wxid
|
||||||
case 'nickname':
|
case 'nickname':
|
||||||
return nickname || wxid
|
return nickname || remark || wxid
|
||||||
default:
|
default:
|
||||||
return nickname || wxid
|
return nickname || remark || wxid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,18 +25,13 @@ export function resolveGroupNicknameByCandidates(groupNicknamesMap: Map<string,
|
|||||||
const idCandidates = buildGroupNicknameIdCandidates(candidates)
|
const idCandidates = buildGroupNicknameIdCandidates(candidates)
|
||||||
if (idCandidates.length === 0) return ''
|
if (idCandidates.length === 0) return ''
|
||||||
|
|
||||||
let resolved = ''
|
|
||||||
for (const id of idCandidates) {
|
for (const id of idCandidates) {
|
||||||
const normalizedId = normalizeGroupNicknameIdentity(id)
|
const normalizedId = normalizeGroupNicknameIdentity(id)
|
||||||
if (!normalizedId) continue
|
if (!normalizedId) continue
|
||||||
const candidateNickname = normalizeGroupNickname(groupNicknamesMap.get(normalizedId) || '')
|
const candidateNickname = normalizeGroupNickname(groupNicknamesMap.get(normalizedId) || '')
|
||||||
if (!candidateNickname) continue
|
if (!candidateNickname) continue
|
||||||
if (!resolved) {
|
return candidateNickname
|
||||||
resolved = candidateNickname
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (resolved !== candidateNickname) return ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved
|
return ''
|
||||||
}
|
}
|
||||||
@@ -332,6 +332,61 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.display-name-inline-options {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name-option {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 56px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(var(--primary-rgb), 0.08);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name-option-label,
|
||||||
|
.display-name-option-desc {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name-option-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name-option-desc {
|
||||||
|
margin-top: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name-option.active .display-name-option-desc {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
.concurrency-option {
|
.concurrency-option {
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -472,6 +527,11 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.display-name-inline-options {
|
||||||
|
max-width: 360px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.format-setting-group {
|
.format-setting-group {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -526,6 +586,7 @@
|
|||||||
.media-default-grid,
|
.media-default-grid,
|
||||||
.media-size-limit-control,
|
.media-size-limit-control,
|
||||||
.concurrency-inline-options,
|
.concurrency-inline-options,
|
||||||
|
.display-name-inline-options,
|
||||||
.format-grid {
|
.format-grid {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
@@ -595,7 +656,8 @@
|
|||||||
.log-toggle-line,
|
.log-toggle-line,
|
||||||
.media-default-grid,
|
.media-default-grid,
|
||||||
.media-size-limit-control,
|
.media-size-limit-control,
|
||||||
.concurrency-inline-options {
|
.concurrency-inline-options,
|
||||||
|
.display-name-inline-options {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -626,6 +688,14 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.display-name-inline-options {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name-option {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.format-setting-group {
|
.format-setting-group {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface ExportDefaultsSettingsPatch {
|
|||||||
avatars?: boolean
|
avatars?: boolean
|
||||||
dateRange?: ExportDateRangeSelection
|
dateRange?: ExportDateRangeSelection
|
||||||
fileNamingMode?: configService.ExportFileNamingMode
|
fileNamingMode?: configService.ExportFileNamingMode
|
||||||
|
displayNamePreference?: configService.ExportDisplayNamePreference
|
||||||
media?: configService.ExportDefaultMediaConfig
|
media?: configService.ExportDefaultMediaConfig
|
||||||
voiceAsText?: boolean
|
voiceAsText?: boolean
|
||||||
excelCompactColumns?: boolean
|
excelCompactColumns?: boolean
|
||||||
@@ -53,6 +54,12 @@ const exportFileNamingModeOptions: Array<{ value: configService.ExportFileNaming
|
|||||||
{ value: 'date-range', label: '时间范围模式', desc: '示例:私聊_张三_20250101-20250331(推荐)' }
|
{ value: 'date-range', label: '时间范围模式', desc: '示例:私聊_张三_20250101-20250331(推荐)' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const exportDisplayNameOptions: Array<{ value: configService.ExportDisplayNamePreference; label: string; desc: string }> = [
|
||||||
|
{ value: 'group-nickname', label: '群昵称优先', desc: '群聊显示群昵称,缺失时回退备注/用户名' },
|
||||||
|
{ value: 'remark', label: '备注优先', desc: '有备注显示备注,否则显示用户名' },
|
||||||
|
{ value: 'nickname', label: '用户名优先', desc: '有用户名显示用户名,否则显示备注' }
|
||||||
|
]
|
||||||
|
|
||||||
const exportConcurrencyOptions = [1, 2, 3, 4, 5, 6] as const
|
const exportConcurrencyOptions = [1, 2, 3, 4, 5, 6] as const
|
||||||
|
|
||||||
const getOptionLabel = (options: ReadonlyArray<{ value: string; label: string }>, value: string) => {
|
const getOptionLabel = (options: ReadonlyArray<{ value: string; label: string }>, value: string) => {
|
||||||
@@ -120,6 +127,7 @@ export function ExportDefaultsSettingsForm({
|
|||||||
const [exportDefaultAvatars, setExportDefaultAvatars] = useState(true)
|
const [exportDefaultAvatars, setExportDefaultAvatars] = useState(true)
|
||||||
const [exportDefaultDateRange, setExportDefaultDateRange] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
|
const [exportDefaultDateRange, setExportDefaultDateRange] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
|
||||||
const [exportDefaultFileNamingMode, setExportDefaultFileNamingMode] = useState<configService.ExportFileNamingMode>('classic')
|
const [exportDefaultFileNamingMode, setExportDefaultFileNamingMode] = useState<configService.ExportFileNamingMode>('classic')
|
||||||
|
const [exportDefaultDisplayNamePreference, setExportDefaultDisplayNamePreference] = useState<configService.ExportDisplayNamePreference>('remark')
|
||||||
const [exportDefaultMedia, setExportDefaultMedia] = useState<configService.ExportDefaultMediaConfig>({
|
const [exportDefaultMedia, setExportDefaultMedia] = useState<configService.ExportDefaultMediaConfig>({
|
||||||
images: true,
|
images: true,
|
||||||
videos: true,
|
videos: true,
|
||||||
@@ -135,11 +143,22 @@ export function ExportDefaultsSettingsForm({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const [savedFormat, savedAvatars, savedDateRange, savedFileNamingMode, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedConcurrency] = await Promise.all([
|
const [
|
||||||
|
savedFormat,
|
||||||
|
savedAvatars,
|
||||||
|
savedDateRange,
|
||||||
|
savedFileNamingMode,
|
||||||
|
savedDisplayNamePreference,
|
||||||
|
savedMedia,
|
||||||
|
savedVoiceAsText,
|
||||||
|
savedExcelCompactColumns,
|
||||||
|
savedConcurrency
|
||||||
|
] = await Promise.all([
|
||||||
configService.getExportDefaultFormat(),
|
configService.getExportDefaultFormat(),
|
||||||
configService.getExportDefaultAvatars(),
|
configService.getExportDefaultAvatars(),
|
||||||
configService.getExportDefaultDateRange(),
|
configService.getExportDefaultDateRange(),
|
||||||
configService.getExportDefaultFileNamingMode(),
|
configService.getExportDefaultFileNamingMode(),
|
||||||
|
configService.getExportDefaultDisplayNamePreference(),
|
||||||
configService.getExportDefaultMedia(),
|
configService.getExportDefaultMedia(),
|
||||||
configService.getExportDefaultVoiceAsText(),
|
configService.getExportDefaultVoiceAsText(),
|
||||||
configService.getExportDefaultExcelCompactColumns(),
|
configService.getExportDefaultExcelCompactColumns(),
|
||||||
@@ -152,6 +171,7 @@ export function ExportDefaultsSettingsForm({
|
|||||||
setExportDefaultAvatars(savedAvatars ?? true)
|
setExportDefaultAvatars(savedAvatars ?? true)
|
||||||
setExportDefaultDateRange(resolveExportDateRangeConfig(savedDateRange))
|
setExportDefaultDateRange(resolveExportDateRangeConfig(savedDateRange))
|
||||||
setExportDefaultFileNamingMode(savedFileNamingMode ?? 'classic')
|
setExportDefaultFileNamingMode(savedFileNamingMode ?? 'classic')
|
||||||
|
setExportDefaultDisplayNamePreference(savedDisplayNamePreference ?? 'remark')
|
||||||
setExportDefaultMedia(savedMedia ?? {
|
setExportDefaultMedia(savedMedia ?? {
|
||||||
images: true,
|
images: true,
|
||||||
videos: true,
|
videos: true,
|
||||||
@@ -441,6 +461,35 @@ export function ExportDefaultsSettingsForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group display-name-setting-group">
|
||||||
|
<div className="form-copy">
|
||||||
|
<label>消息发送者命名方式</label>
|
||||||
|
<span className="form-hint">控制群消息导出时发送者名称的优先级</span>
|
||||||
|
</div>
|
||||||
|
<div className="form-control">
|
||||||
|
<div className="display-name-inline-options" role="radiogroup" aria-label="消息发送者命名方式">
|
||||||
|
{exportDisplayNameOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
className={`display-name-option ${exportDefaultDisplayNamePreference === option.value ? 'active' : ''}`}
|
||||||
|
aria-pressed={exportDefaultDisplayNamePreference === option.value}
|
||||||
|
title={option.desc}
|
||||||
|
onClick={async () => {
|
||||||
|
setExportDefaultDisplayNamePreference(option.value)
|
||||||
|
await configService.setExportDefaultDisplayNamePreference(option.value)
|
||||||
|
onDefaultsChanged?.({ displayNamePreference: option.value })
|
||||||
|
notify('已更新消息发送者命名方式', true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="display-name-option-label">{option.label}</span>
|
||||||
|
<span className="display-name-option-desc">{option.desc}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="form-copy">
|
<div className="form-copy">
|
||||||
<label>Excel 列显示</label>
|
<label>Excel 列显示</label>
|
||||||
|
|||||||
@@ -154,14 +154,18 @@ function ExportPage() {
|
|||||||
const sessionId = String(payload.sessionId || '').trim()
|
const sessionId = String(payload.sessionId || '').trim()
|
||||||
const payloadName = pickDisplayName(payload.sessionName)
|
const payloadName = pickDisplayName(payload.sessionName)
|
||||||
const sessionRow = sessions.find(s => s.username === sessionId)
|
const sessionRow = sessions.find(s => s.username === sessionId)
|
||||||
|
if (options.displayNamePreference === 'nickname') {
|
||||||
|
return displayNameOrFallback(sessionId, sessionRow?.nickname, payloadName, sessionRow?.remark, sessionRow?.displayName)
|
||||||
|
}
|
||||||
return displayNameOrFallback(sessionId, payloadName, sessionRow?.displayName, sessionRow?.remark, sessionRow?.nickname)
|
return displayNameOrFallback(sessionId, payloadName, sessionRow?.displayName, sessionRow?.remark, sessionRow?.nickname)
|
||||||
}, [sessions])
|
}, [options.displayNamePreference, sessions])
|
||||||
|
|
||||||
const handleExportDefaultsChanged = useCallback((patch: ExportDefaultsSettingsPatch) => {
|
const handleExportDefaultsChanged = useCallback((patch: ExportDefaultsSettingsPatch) => {
|
||||||
updateOptions({
|
updateOptions({
|
||||||
...(patch.format ? { format: patch.format as TextExportFormat } : {}),
|
...(patch.format ? { format: patch.format as TextExportFormat } : {}),
|
||||||
...(patch.avatars !== undefined ? { exportAvatars: patch.avatars } : {}),
|
...(patch.avatars !== undefined ? { exportAvatars: patch.avatars } : {}),
|
||||||
...(patch.fileNamingMode !== undefined ? { fileNamingMode: patch.fileNamingMode } : {}),
|
...(patch.fileNamingMode !== undefined ? { fileNamingMode: patch.fileNamingMode } : {}),
|
||||||
|
...(patch.displayNamePreference !== undefined ? { displayNamePreference: patch.displayNamePreference } : {}),
|
||||||
...(patch.media ? {
|
...(patch.media ? {
|
||||||
exportImages: patch.media.images,
|
exportImages: patch.media.images,
|
||||||
exportVideos: patch.media.videos,
|
exportVideos: patch.media.videos,
|
||||||
@@ -282,6 +286,7 @@ function ExportPage() {
|
|||||||
}, [handleOpenSingleExportRequest, isConfigLoaded, pendingSingleExportRequest])
|
}, [handleOpenSingleExportRequest, isConfigLoaded, pendingSingleExportRequest])
|
||||||
|
|
||||||
const handleConfirmExport = useCallback((finalOptions: any) => {
|
const handleConfirmExport = useCallback((finalOptions: any) => {
|
||||||
|
updateOptions(finalOptions)
|
||||||
startTask({
|
startTask({
|
||||||
sessionIds: dialogState.sessionIds,
|
sessionIds: dialogState.sessionIds,
|
||||||
sessionNames: dialogState.sessionNames,
|
sessionNames: dialogState.sessionNames,
|
||||||
@@ -291,9 +296,10 @@ function ExportPage() {
|
|||||||
options: finalOptions
|
options: finalOptions
|
||||||
})
|
})
|
||||||
closeDialog()
|
closeDialog()
|
||||||
}, [dialogState, exportPath, startTask, closeDialog])
|
}, [dialogState, exportPath, startTask, closeDialog, updateOptions])
|
||||||
|
|
||||||
const handleAutomationCreateFromDialog = useCallback((finalOptions: any) => {
|
const handleAutomationCreateFromDialog = useCallback((finalOptions: any) => {
|
||||||
|
updateOptions(finalOptions)
|
||||||
setDraftAutomationPayload({
|
setDraftAutomationPayload({
|
||||||
sessionIds: dialogState.sessionIds,
|
sessionIds: dialogState.sessionIds,
|
||||||
sessionNames: dialogState.sessionNames,
|
sessionNames: dialogState.sessionNames,
|
||||||
@@ -303,7 +309,7 @@ function ExportPage() {
|
|||||||
options: finalOptions
|
options: finalOptions
|
||||||
})
|
})
|
||||||
closeDialog()
|
closeDialog()
|
||||||
}, [closeDialog, dialogState, exportPath])
|
}, [closeDialog, dialogState, exportPath, updateOptions])
|
||||||
|
|
||||||
const handleAutomationSave = useCallback((task: any) => {
|
const handleAutomationSave = useCallback((task: any) => {
|
||||||
void addTask(task)
|
void addTask(task)
|
||||||
|
|||||||
@@ -326,7 +326,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-size-limit, .concurrency-control, .path-style-control, .conflict-strategy-control {
|
.file-size-limit, .concurrency-control, .path-style-control, .conflict-strategy-control, .display-name-control {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-style-control, .conflict-strategy-control {
|
.path-style-control, .conflict-strategy-control, .display-name-control {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-style-segmented, .conflict-strategy-segmented {
|
.path-style-segmented, .conflict-strategy-segmented, .display-name-segmented {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
@@ -396,6 +396,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.display-name-control {
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
padding-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name-segmented {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 96px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
type ExportDefaultDateRangeConfig
|
type ExportDefaultDateRangeConfig
|
||||||
} from '../../../../utils/exportDateRange'
|
} from '../../../../utils/exportDateRange'
|
||||||
import type { ExportDialogState, ExportOptions, TextExportFormat } from '../../types'
|
import type { ExportDialogState, ExportOptions, TextExportFormat } from '../../types'
|
||||||
import { conflictStrategyOptions, formatOptions, MAX_EXPORT_FILE_SIZE_MB_LIMIT } from '../../constants'
|
import { conflictStrategyOptions, displayNameOptions, formatOptions, MAX_EXPORT_FILE_SIZE_MB_LIMIT } from '../../constants'
|
||||||
import { formatPathBrief } from '../../utils/format'
|
import { formatPathBrief } from '../../utils/format'
|
||||||
import './ExportDialog.scss'
|
import './ExportDialog.scss'
|
||||||
|
|
||||||
@@ -318,6 +318,23 @@ const ExportDialog: React.FC<ExportDialogProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="display-name-control">
|
||||||
|
<span>命名方式:</span>
|
||||||
|
<div className="display-name-segmented" title="控制导出群消息时发送者名称的优先级">
|
||||||
|
{displayNameOptions.map(item => (
|
||||||
|
<button
|
||||||
|
key={item.value}
|
||||||
|
type="button"
|
||||||
|
className={draftOptions.displayNamePreference === item.value ? 'active' : ''}
|
||||||
|
title={item.desc}
|
||||||
|
onClick={() => updateDraftOptions({ displayNamePreference: item.value })}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="concurrency-control">
|
<div className="concurrency-control">
|
||||||
<span>导出并发数:</span>
|
<span>导出并发数:</span>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ export const formatOptions: Array<{ value: TextExportFormat; label: string; desc
|
|||||||
// ─── Display name options ────────────────────────────────────
|
// ─── Display name options ────────────────────────────────────
|
||||||
|
|
||||||
export const displayNameOptions: Array<{ value: DisplayNamePreference; label: string; desc: string }> = [
|
export const displayNameOptions: Array<{ value: DisplayNamePreference; label: string; desc: string }> = [
|
||||||
{ value: 'group-nickname', label: '群昵称优先', desc: '仅群聊有效,私聊显示备注/昵称' },
|
{ value: 'group-nickname', label: '群昵称优先', desc: '群聊显示群昵称,缺失时回退备注/用户名' },
|
||||||
{ value: 'remark', label: '备注优先', desc: '有备注显示备注,否则显示昵称' },
|
{ value: 'remark', label: '备注优先', desc: '有备注显示备注,否则显示用户名' },
|
||||||
{ value: 'nickname', label: '微信昵称', desc: '始终显示微信昵称' }
|
{ value: 'nickname', label: '用户名优先', desc: '有用户名显示用户名,否则显示备注' }
|
||||||
]
|
]
|
||||||
|
|
||||||
export const conflictStrategyOptions: Array<{ value: ExportConflictStrategy; label: string; desc: string }> = [
|
export const conflictStrategyOptions: Array<{ value: ExportConflictStrategy; label: string; desc: string }> = [
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ export function useExportConfig(): ExportConfigResult {
|
|||||||
excelCompact,
|
excelCompact,
|
||||||
txtCols,
|
txtCols,
|
||||||
concurrency,
|
concurrency,
|
||||||
fileNamingMode
|
fileNamingMode,
|
||||||
|
displayNamePreference
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
configService.getExportPath(),
|
configService.getExportPath(),
|
||||||
configService.getExportWriteLayout(),
|
configService.getExportWriteLayout(),
|
||||||
@@ -76,7 +77,8 @@ export function useExportConfig(): ExportConfigResult {
|
|||||||
configService.getExportDefaultExcelCompactColumns(),
|
configService.getExportDefaultExcelCompactColumns(),
|
||||||
configService.getExportDefaultTxtColumns(),
|
configService.getExportDefaultTxtColumns(),
|
||||||
configService.getExportDefaultConcurrency(),
|
configService.getExportDefaultConcurrency(),
|
||||||
configService.getExportDefaultFileNamingMode()
|
configService.getExportDefaultFileNamingMode(),
|
||||||
|
configService.getExportDefaultDisplayNamePreference()
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!isMounted) return
|
if (!isMounted) return
|
||||||
@@ -124,6 +126,9 @@ export function useExportConfig(): ExportConfigResult {
|
|||||||
if (Array.isArray(txtCols)) newOptions.txtColumns = txtCols
|
if (Array.isArray(txtCols)) newOptions.txtColumns = txtCols
|
||||||
if (typeof concurrency === 'number' && concurrency > 0) newOptions.exportConcurrency = concurrency
|
if (typeof concurrency === 'number' && concurrency > 0) newOptions.exportConcurrency = concurrency
|
||||||
if (fileNamingMode === 'classic' || fileNamingMode === 'date-range') newOptions.fileNamingMode = fileNamingMode
|
if (fileNamingMode === 'classic' || fileNamingMode === 'date-range') newOptions.fileNamingMode = fileNamingMode
|
||||||
|
if (displayNamePreference === 'group-nickname' || displayNamePreference === 'remark' || displayNamePreference === 'nickname') {
|
||||||
|
newOptions.displayNamePreference = displayNamePreference
|
||||||
|
}
|
||||||
|
|
||||||
// Date range
|
// Date range
|
||||||
setRawDateRangeConfigState(dateRange)
|
setRawDateRangeConfigState(dateRange)
|
||||||
@@ -202,6 +207,9 @@ export function useExportConfig(): ExportConfigResult {
|
|||||||
if (patch.fileNamingMode !== undefined) {
|
if (patch.fileNamingMode !== undefined) {
|
||||||
void configService.setExportDefaultFileNamingMode(patch.fileNamingMode)
|
void configService.setExportDefaultFileNamingMode(patch.fileNamingMode)
|
||||||
}
|
}
|
||||||
|
if (patch.displayNamePreference !== undefined) {
|
||||||
|
void configService.setExportDefaultDisplayNamePreference(patch.displayNamePreference)
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-derive exportMedia
|
// Auto-derive exportMedia
|
||||||
next.exportMedia = (
|
next.exportMedia = (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ChatSession as AppChatSession, ContactInfo } from '../../../types/models'
|
import type { ChatSession as AppChatSession, ContactInfo } from '../../../types/models'
|
||||||
import type { ConversationTab, SessionRow } from '../types'
|
import type { ConversationTab, DisplayNamePreference, SessionRow } from '../types'
|
||||||
import { displayNameOrFallback, pickDisplayName } from '../../../utils/displayName'
|
import { displayNameOrFallback, pickDisplayName } from '../../../utils/displayName'
|
||||||
|
|
||||||
// ─── Session type classification ─────────────────────────────
|
// ─── Session type classification ─────────────────────────────
|
||||||
@@ -135,9 +135,12 @@ export const getSelectionScopeFromRows = (rows: SessionRow[]): import('../types'
|
|||||||
|
|
||||||
export const resolveScopeDisplayNames = (
|
export const resolveScopeDisplayNames = (
|
||||||
rows: SessionRow[],
|
rows: SessionRow[],
|
||||||
pref: import('../types').DisplayNamePreference
|
pref: DisplayNamePreference
|
||||||
): string[] => {
|
): string[] => {
|
||||||
return rows.map(r => displayNameOrFallback(r.username, r.remark, r.nickname, r.displayName))
|
return rows.map(r => {
|
||||||
|
if (pref === 'nickname') return displayNameOrFallback(r.username, r.nickname, r.remark, r.displayName)
|
||||||
|
return displayNameOrFallback(r.username, r.remark, r.nickname, r.displayName)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Name comparison helper ──────────────────────────────────
|
// ─── Name comparison helper ──────────────────────────────────
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export const CONFIG_KEYS = {
|
|||||||
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns',
|
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns',
|
||||||
EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns',
|
EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns',
|
||||||
EXPORT_DEFAULT_CONCURRENCY: 'exportDefaultConcurrency',
|
EXPORT_DEFAULT_CONCURRENCY: 'exportDefaultConcurrency',
|
||||||
|
EXPORT_DEFAULT_DISPLAY_NAME_PREFERENCE: 'exportDefaultDisplayNamePreference',
|
||||||
EXPORT_WRITE_LAYOUT: 'exportWriteLayout',
|
EXPORT_WRITE_LAYOUT: 'exportWriteLayout',
|
||||||
EXPORT_SESSION_NAME_PREFIX_ENABLED: 'exportSessionNamePrefixEnabled',
|
EXPORT_SESSION_NAME_PREFIX_ENABLED: 'exportSessionNamePrefixEnabled',
|
||||||
EXPORT_LAST_SESSION_RUN_MAP: 'exportLastSessionRunMap',
|
EXPORT_LAST_SESSION_RUN_MAP: 'exportLastSessionRunMap',
|
||||||
@@ -163,6 +164,7 @@ export interface ExportDefaultMediaConfig {
|
|||||||
|
|
||||||
export type ExportFileNamingMode = 'classic' | 'date-range'
|
export type ExportFileNamingMode = 'classic' | 'date-range'
|
||||||
export type ExportPathStyle = 'auto' | 'posix' | 'windows'
|
export type ExportPathStyle = 'auto' | 'posix' | 'windows'
|
||||||
|
export type ExportDisplayNamePreference = 'group-nickname' | 'remark' | 'nickname'
|
||||||
|
|
||||||
export type WindowCloseBehavior = 'ask' | 'tray' | 'quit'
|
export type WindowCloseBehavior = 'ask' | 'tray' | 'quit'
|
||||||
export type QuoteLayout = 'quote-top' | 'quote-bottom'
|
export type QuoteLayout = 'quote-top' | 'quote-bottom'
|
||||||
@@ -640,6 +642,18 @@ export async function setExportDefaultConcurrency(concurrency: number): Promise<
|
|||||||
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY, concurrency)
|
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY, concurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取导出默认命名方式
|
||||||
|
export async function getExportDefaultDisplayNamePreference(): Promise<ExportDisplayNamePreference | null> {
|
||||||
|
const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_DISPLAY_NAME_PREFERENCE)
|
||||||
|
if (value === 'group-nickname' || value === 'remark' || value === 'nickname') return value
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置导出默认命名方式
|
||||||
|
export async function setExportDefaultDisplayNamePreference(preference: ExportDisplayNamePreference): Promise<void> {
|
||||||
|
await config.set(CONFIG_KEYS.EXPORT_DEFAULT_DISPLAY_NAME_PREFERENCE, preference)
|
||||||
|
}
|
||||||
|
|
||||||
export type ExportWriteLayout = 'A' | 'B' | 'C'
|
export type ExportWriteLayout = 'A' | 'B' | 'C'
|
||||||
|
|
||||||
export async function getExportWriteLayout(): Promise<ExportWriteLayout> {
|
export async function getExportWriteLayout(): Promise<ExportWriteLayout> {
|
||||||
|
|||||||
Reference in new issue
Block a user