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[]
|
||||
exportDefaultConcurrency: number
|
||||
exportDefaultPathStyle: 'auto' | 'posix' | 'windows'
|
||||
exportDefaultDisplayNamePreference: 'group-nickname' | 'remark' | 'nickname'
|
||||
analyticsExcludedUsernames: string[]
|
||||
|
||||
// 安全相关
|
||||
@@ -218,6 +219,7 @@ export class ConfigService {
|
||||
transcribeLanguages: ['zh'],
|
||||
exportDefaultConcurrency: 4,
|
||||
exportDefaultPathStyle: 'auto',
|
||||
exportDefaultDisplayNamePreference: 'remark',
|
||||
analyticsExcludedUsernames: [],
|
||||
authEnabled: false,
|
||||
authPassword: '',
|
||||
|
||||
@@ -14,9 +14,9 @@ export function getPreferredDisplayName(
|
||||
case 'remark':
|
||||
return remark || nickname || wxid
|
||||
case 'nickname':
|
||||
return nickname || wxid
|
||||
return nickname || remark || wxid
|
||||
default:
|
||||
return nickname || wxid
|
||||
return nickname || remark || wxid
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,18 +25,13 @@ export function resolveGroupNicknameByCandidates(groupNicknamesMap: Map<string,
|
||||
const idCandidates = buildGroupNicknameIdCandidates(candidates)
|
||||
if (idCandidates.length === 0) return ''
|
||||
|
||||
let resolved = ''
|
||||
for (const id of idCandidates) {
|
||||
const normalizedId = normalizeGroupNicknameIdentity(id)
|
||||
if (!normalizedId) continue
|
||||
const candidateNickname = normalizeGroupNickname(groupNicknamesMap.get(normalizedId) || '')
|
||||
if (!candidateNickname) continue
|
||||
if (!resolved) {
|
||||
resolved = candidateNickname
|
||||
continue
|
||||
}
|
||||
if (resolved !== candidateNickname) return ''
|
||||
return candidateNickname
|
||||
}
|
||||
|
||||
return resolved
|
||||
return ''
|
||||
}
|
||||
@@ -332,6 +332,61 @@
|
||||
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 {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
@@ -472,6 +527,11 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.display-name-inline-options {
|
||||
max-width: 360px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.format-setting-group {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
@@ -526,6 +586,7 @@
|
||||
.media-default-grid,
|
||||
.media-size-limit-control,
|
||||
.concurrency-inline-options,
|
||||
.display-name-inline-options,
|
||||
.format-grid {
|
||||
max-width: none;
|
||||
}
|
||||
@@ -595,7 +656,8 @@
|
||||
.log-toggle-line,
|
||||
.media-default-grid,
|
||||
.media-size-limit-control,
|
||||
.concurrency-inline-options {
|
||||
.concurrency-inline-options,
|
||||
.display-name-inline-options {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -626,6 +688,14 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.display-name-inline-options {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.display-name-option {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.format-setting-group {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ExportDefaultsSettingsPatch {
|
||||
avatars?: boolean
|
||||
dateRange?: ExportDateRangeSelection
|
||||
fileNamingMode?: configService.ExportFileNamingMode
|
||||
displayNamePreference?: configService.ExportDisplayNamePreference
|
||||
media?: configService.ExportDefaultMediaConfig
|
||||
voiceAsText?: boolean
|
||||
excelCompactColumns?: boolean
|
||||
@@ -53,6 +54,12 @@ const exportFileNamingModeOptions: Array<{ value: configService.ExportFileNaming
|
||||
{ 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 getOptionLabel = (options: ReadonlyArray<{ value: string; label: string }>, value: string) => {
|
||||
@@ -120,6 +127,7 @@ export function ExportDefaultsSettingsForm({
|
||||
const [exportDefaultAvatars, setExportDefaultAvatars] = useState(true)
|
||||
const [exportDefaultDateRange, setExportDefaultDateRange] = useState<ExportDateRangeSelection>(() => createDefaultExportDateRangeSelection())
|
||||
const [exportDefaultFileNamingMode, setExportDefaultFileNamingMode] = useState<configService.ExportFileNamingMode>('classic')
|
||||
const [exportDefaultDisplayNamePreference, setExportDefaultDisplayNamePreference] = useState<configService.ExportDisplayNamePreference>('remark')
|
||||
const [exportDefaultMedia, setExportDefaultMedia] = useState<configService.ExportDefaultMediaConfig>({
|
||||
images: true,
|
||||
videos: true,
|
||||
@@ -135,11 +143,22 @@ export function ExportDefaultsSettingsForm({
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
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.getExportDefaultAvatars(),
|
||||
configService.getExportDefaultDateRange(),
|
||||
configService.getExportDefaultFileNamingMode(),
|
||||
configService.getExportDefaultDisplayNamePreference(),
|
||||
configService.getExportDefaultMedia(),
|
||||
configService.getExportDefaultVoiceAsText(),
|
||||
configService.getExportDefaultExcelCompactColumns(),
|
||||
@@ -152,6 +171,7 @@ export function ExportDefaultsSettingsForm({
|
||||
setExportDefaultAvatars(savedAvatars ?? true)
|
||||
setExportDefaultDateRange(resolveExportDateRangeConfig(savedDateRange))
|
||||
setExportDefaultFileNamingMode(savedFileNamingMode ?? 'classic')
|
||||
setExportDefaultDisplayNamePreference(savedDisplayNamePreference ?? 'remark')
|
||||
setExportDefaultMedia(savedMedia ?? {
|
||||
images: true,
|
||||
videos: true,
|
||||
@@ -441,6 +461,35 @@ export function ExportDefaultsSettingsForm({
|
||||
</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-copy">
|
||||
<label>Excel 列显示</label>
|
||||
|
||||
@@ -154,14 +154,18 @@ function ExportPage() {
|
||||
const sessionId = String(payload.sessionId || '').trim()
|
||||
const payloadName = pickDisplayName(payload.sessionName)
|
||||
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)
|
||||
}, [sessions])
|
||||
}, [options.displayNamePreference, sessions])
|
||||
|
||||
const handleExportDefaultsChanged = useCallback((patch: ExportDefaultsSettingsPatch) => {
|
||||
updateOptions({
|
||||
...(patch.format ? { format: patch.format as TextExportFormat } : {}),
|
||||
...(patch.avatars !== undefined ? { exportAvatars: patch.avatars } : {}),
|
||||
...(patch.fileNamingMode !== undefined ? { fileNamingMode: patch.fileNamingMode } : {}),
|
||||
...(patch.displayNamePreference !== undefined ? { displayNamePreference: patch.displayNamePreference } : {}),
|
||||
...(patch.media ? {
|
||||
exportImages: patch.media.images,
|
||||
exportVideos: patch.media.videos,
|
||||
@@ -282,6 +286,7 @@ function ExportPage() {
|
||||
}, [handleOpenSingleExportRequest, isConfigLoaded, pendingSingleExportRequest])
|
||||
|
||||
const handleConfirmExport = useCallback((finalOptions: any) => {
|
||||
updateOptions(finalOptions)
|
||||
startTask({
|
||||
sessionIds: dialogState.sessionIds,
|
||||
sessionNames: dialogState.sessionNames,
|
||||
@@ -291,9 +296,10 @@ function ExportPage() {
|
||||
options: finalOptions
|
||||
})
|
||||
closeDialog()
|
||||
}, [dialogState, exportPath, startTask, closeDialog])
|
||||
}, [dialogState, exportPath, startTask, closeDialog, updateOptions])
|
||||
|
||||
const handleAutomationCreateFromDialog = useCallback((finalOptions: any) => {
|
||||
updateOptions(finalOptions)
|
||||
setDraftAutomationPayload({
|
||||
sessionIds: dialogState.sessionIds,
|
||||
sessionNames: dialogState.sessionNames,
|
||||
@@ -303,7 +309,7 @@ function ExportPage() {
|
||||
options: finalOptions
|
||||
})
|
||||
closeDialog()
|
||||
}, [closeDialog, dialogState, exportPath])
|
||||
}, [closeDialog, dialogState, exportPath, updateOptions])
|
||||
|
||||
const handleAutomationSave = useCallback((task: any) => {
|
||||
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;
|
||||
display: flex;
|
||||
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%;
|
||||
margin-top: 4px;
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.path-style-segmented, .conflict-strategy-segmented {
|
||||
.path-style-segmented, .conflict-strategy-segmented, .display-name-segmented {
|
||||
display: inline-flex;
|
||||
min-height: 34px;
|
||||
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
|
||||
} from '../../../../utils/exportDateRange'
|
||||
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 './ExportDialog.scss'
|
||||
|
||||
@@ -318,6 +318,23 @@ const ExportDialog: React.FC<ExportDialogProps> = ({
|
||||
</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">
|
||||
<span>导出并发数:</span>
|
||||
<select
|
||||
|
||||
@@ -56,9 +56,9 @@ export const formatOptions: Array<{ value: TextExportFormat; label: string; desc
|
||||
// ─── Display name options ────────────────────────────────────
|
||||
|
||||
export const displayNameOptions: Array<{ value: DisplayNamePreference; label: string; desc: string }> = [
|
||||
{ value: 'group-nickname', label: '群昵称优先', desc: '仅群聊有效,私聊显示备注/昵称' },
|
||||
{ value: 'remark', label: '备注优先', desc: '有备注显示备注,否则显示昵称' },
|
||||
{ value: 'nickname', label: '微信昵称', desc: '始终显示微信昵称' }
|
||||
{ value: 'group-nickname', label: '群昵称优先', desc: '群聊显示群昵称,缺失时回退备注/用户名' },
|
||||
{ value: 'remark', label: '备注优先', desc: '有备注显示备注,否则显示用户名' },
|
||||
{ value: 'nickname', label: '用户名优先', desc: '有用户名显示用户名,否则显示备注' }
|
||||
]
|
||||
|
||||
export const conflictStrategyOptions: Array<{ value: ExportConflictStrategy; label: string; desc: string }> = [
|
||||
|
||||
@@ -63,7 +63,8 @@ export function useExportConfig(): ExportConfigResult {
|
||||
excelCompact,
|
||||
txtCols,
|
||||
concurrency,
|
||||
fileNamingMode
|
||||
fileNamingMode,
|
||||
displayNamePreference
|
||||
] = await Promise.all([
|
||||
configService.getExportPath(),
|
||||
configService.getExportWriteLayout(),
|
||||
@@ -76,7 +77,8 @@ export function useExportConfig(): ExportConfigResult {
|
||||
configService.getExportDefaultExcelCompactColumns(),
|
||||
configService.getExportDefaultTxtColumns(),
|
||||
configService.getExportDefaultConcurrency(),
|
||||
configService.getExportDefaultFileNamingMode()
|
||||
configService.getExportDefaultFileNamingMode(),
|
||||
configService.getExportDefaultDisplayNamePreference()
|
||||
])
|
||||
|
||||
if (!isMounted) return
|
||||
@@ -124,6 +126,9 @@ export function useExportConfig(): ExportConfigResult {
|
||||
if (Array.isArray(txtCols)) newOptions.txtColumns = txtCols
|
||||
if (typeof concurrency === 'number' && concurrency > 0) newOptions.exportConcurrency = concurrency
|
||||
if (fileNamingMode === 'classic' || fileNamingMode === 'date-range') newOptions.fileNamingMode = fileNamingMode
|
||||
if (displayNamePreference === 'group-nickname' || displayNamePreference === 'remark' || displayNamePreference === 'nickname') {
|
||||
newOptions.displayNamePreference = displayNamePreference
|
||||
}
|
||||
|
||||
// Date range
|
||||
setRawDateRangeConfigState(dateRange)
|
||||
@@ -202,6 +207,9 @@ export function useExportConfig(): ExportConfigResult {
|
||||
if (patch.fileNamingMode !== undefined) {
|
||||
void configService.setExportDefaultFileNamingMode(patch.fileNamingMode)
|
||||
}
|
||||
if (patch.displayNamePreference !== undefined) {
|
||||
void configService.setExportDefaultDisplayNamePreference(patch.displayNamePreference)
|
||||
}
|
||||
|
||||
// Auto-derive exportMedia
|
||||
next.exportMedia = (
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
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'
|
||||
|
||||
// ─── Session type classification ─────────────────────────────
|
||||
@@ -135,9 +135,12 @@ export const getSelectionScopeFromRows = (rows: SessionRow[]): import('../types'
|
||||
|
||||
export const resolveScopeDisplayNames = (
|
||||
rows: SessionRow[],
|
||||
pref: import('../types').DisplayNamePreference
|
||||
pref: DisplayNamePreference
|
||||
): 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 ──────────────────────────────────
|
||||
|
||||
@@ -39,6 +39,7 @@ export const CONFIG_KEYS = {
|
||||
EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns',
|
||||
EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns',
|
||||
EXPORT_DEFAULT_CONCURRENCY: 'exportDefaultConcurrency',
|
||||
EXPORT_DEFAULT_DISPLAY_NAME_PREFERENCE: 'exportDefaultDisplayNamePreference',
|
||||
EXPORT_WRITE_LAYOUT: 'exportWriteLayout',
|
||||
EXPORT_SESSION_NAME_PREFIX_ENABLED: 'exportSessionNamePrefixEnabled',
|
||||
EXPORT_LAST_SESSION_RUN_MAP: 'exportLastSessionRunMap',
|
||||
@@ -163,6 +164,7 @@ export interface ExportDefaultMediaConfig {
|
||||
|
||||
export type ExportFileNamingMode = 'classic' | 'date-range'
|
||||
export type ExportPathStyle = 'auto' | 'posix' | 'windows'
|
||||
export type ExportDisplayNamePreference = 'group-nickname' | 'remark' | 'nickname'
|
||||
|
||||
export type WindowCloseBehavior = 'ask' | 'tray' | 'quit'
|
||||
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)
|
||||
}
|
||||
|
||||
// 获取导出默认命名方式
|
||||
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 async function getExportWriteLayout(): Promise<ExportWriteLayout> {
|
||||
|
||||
Reference in new issue
Block a user