mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat: 解决了一些问题
This commit is contained in:
@@ -29,7 +29,22 @@ interface WxidOption {
|
||||
}
|
||||
|
||||
function SettingsPage() {
|
||||
const { isDbConnected, setDbConnected, setLoading, reset } = useAppStore()
|
||||
const {
|
||||
isDbConnected,
|
||||
setDbConnected,
|
||||
setLoading,
|
||||
reset,
|
||||
updateInfo,
|
||||
setUpdateInfo,
|
||||
isDownloading,
|
||||
setIsDownloading,
|
||||
downloadProgress,
|
||||
setDownloadProgress,
|
||||
showUpdateDialog,
|
||||
setShowUpdateDialog,
|
||||
setUpdateError
|
||||
} = useAppStore()
|
||||
|
||||
const resetChatStore = useChatStore((state) => state.reset)
|
||||
const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore()
|
||||
const clearAnalyticsStoreCache = useAnalyticsStore((state) => state.clearCache)
|
||||
@@ -69,10 +84,7 @@ function SettingsPage() {
|
||||
const [isFetchingDbKey, setIsFetchingDbKey] = useState(false)
|
||||
const [isFetchingImageKey, setIsFetchingImageKey] = useState(false)
|
||||
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false)
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||
const [appVersion, setAppVersion] = useState('')
|
||||
const [updateInfo, setUpdateInfo] = useState<{ hasUpdate: boolean; version?: string; releaseNotes?: string } | null>(null)
|
||||
const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null)
|
||||
const [showDecryptKey, setShowDecryptKey] = useState(false)
|
||||
const [dbKeyStatus, setDbKeyStatus] = useState('')
|
||||
@@ -209,7 +221,7 @@ function SettingsPage() {
|
||||
|
||||
// 监听下载进度
|
||||
useEffect(() => {
|
||||
const removeListener = window.electronAPI.app.onDownloadProgress?.((progress: number) => {
|
||||
const removeListener = window.electronAPI.app.onDownloadProgress?.((progress: any) => {
|
||||
setDownloadProgress(progress)
|
||||
})
|
||||
return () => removeListener?.()
|
||||
@@ -229,12 +241,14 @@ function SettingsPage() {
|
||||
}, [whisperModelDir])
|
||||
|
||||
const handleCheckUpdate = async () => {
|
||||
if (isCheckingUpdate) return
|
||||
setIsCheckingUpdate(true)
|
||||
setUpdateInfo(null)
|
||||
try {
|
||||
const result = await window.electronAPI.app.checkForUpdates()
|
||||
if (result.hasUpdate) {
|
||||
setUpdateInfo(result)
|
||||
setShowUpdateDialog(true)
|
||||
showMessage(`发现新版:${result.version}`, true)
|
||||
} else {
|
||||
showMessage('当前已是最新版', true)
|
||||
@@ -247,8 +261,10 @@ function SettingsPage() {
|
||||
}
|
||||
|
||||
const handleUpdateNow = async () => {
|
||||
setShowUpdateDialog(false)
|
||||
|
||||
setIsDownloading(true)
|
||||
setDownloadProgress(0)
|
||||
setDownloadProgress({ percent: 0 })
|
||||
try {
|
||||
showMessage('正在下载更新...', true)
|
||||
await window.electronAPI.app.downloadAndInstall()
|
||||
@@ -258,6 +274,8 @@ function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const showMessage = (text: string, success: boolean) => {
|
||||
setMessage({ text, success })
|
||||
setTimeout(() => setMessage(null), 3000)
|
||||
@@ -989,171 +1007,171 @@ function SettingsPage() {
|
||||
const exportExcelColumnsLabel = getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue)
|
||||
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="form-group">
|
||||
<label>默认导出格式</label>
|
||||
<span className="form-hint">导出页面默认选中的格式</span>
|
||||
<div className="select-field" ref={exportFormatDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`select-trigger ${showExportFormatSelect ? 'open' : ''}`}
|
||||
onClick={() => {
|
||||
setShowExportFormatSelect(!showExportFormatSelect)
|
||||
setShowExportDateRangeSelect(false)
|
||||
setShowExportExcelColumnsSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="select-value">{exportFormatLabel}</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{showExportFormatSelect && (
|
||||
<div className="select-dropdown">
|
||||
{exportFormatOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`select-option ${exportDefaultFormat === option.value ? 'active' : ''}`}
|
||||
onClick={async () => {
|
||||
setExportDefaultFormat(option.value)
|
||||
await configService.setExportDefaultFormat(option.value)
|
||||
showMessage('已更新导出格式默认值', true)
|
||||
setShowExportFormatSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
{option.desc && <span className="option-desc">{option.desc}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>默认导出时间范围</label>
|
||||
<span className="form-hint">控制导出页面的默认时间选择</span>
|
||||
<div className="select-field" ref={exportDateRangeDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`select-trigger ${showExportDateRangeSelect ? 'open' : ''}`}
|
||||
onClick={() => {
|
||||
setShowExportDateRangeSelect(!showExportDateRangeSelect)
|
||||
setShowExportFormatSelect(false)
|
||||
setShowExportExcelColumnsSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="select-value">{exportDateRangeLabel}</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{showExportDateRangeSelect && (
|
||||
<div className="select-dropdown">
|
||||
{exportDateRangeOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`select-option ${exportDefaultDateRange === option.value ? 'active' : ''}`}
|
||||
onClick={async () => {
|
||||
setExportDefaultDateRange(option.value)
|
||||
await configService.setExportDefaultDateRange(option.value)
|
||||
showMessage('已更新默认导出时间范围', true)
|
||||
setShowExportDateRangeSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>默认导出媒体文件</label>
|
||||
<span className="form-hint">控制图片/语音/表情的默认导出开关</span>
|
||||
<div className="log-toggle-line">
|
||||
<span className="log-status">{exportDefaultMedia ? '已开启' : '已关闭'}</span>
|
||||
<label className="switch" htmlFor="export-default-media">
|
||||
<input
|
||||
id="export-default-media"
|
||||
className="switch-input"
|
||||
type="checkbox"
|
||||
checked={exportDefaultMedia}
|
||||
onChange={async (e) => {
|
||||
const enabled = e.target.checked
|
||||
setExportDefaultMedia(enabled)
|
||||
await configService.setExportDefaultMedia(enabled)
|
||||
showMessage(enabled ? '已开启默认媒体导出' : '已关闭默认媒体导出', true)
|
||||
<div className="tab-content">
|
||||
<div className="form-group">
|
||||
<label>默认导出格式</label>
|
||||
<span className="form-hint">导出页面默认选中的格式</span>
|
||||
<div className="select-field" ref={exportFormatDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`select-trigger ${showExportFormatSelect ? 'open' : ''}`}
|
||||
onClick={() => {
|
||||
setShowExportFormatSelect(!showExportFormatSelect)
|
||||
setShowExportDateRangeSelect(false)
|
||||
setShowExportExcelColumnsSelect(false)
|
||||
}}
|
||||
/>
|
||||
<span className="switch-slider" />
|
||||
</label>
|
||||
>
|
||||
<span className="select-value">{exportFormatLabel}</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{showExportFormatSelect && (
|
||||
<div className="select-dropdown">
|
||||
{exportFormatOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`select-option ${exportDefaultFormat === option.value ? 'active' : ''}`}
|
||||
onClick={async () => {
|
||||
setExportDefaultFormat(option.value)
|
||||
await configService.setExportDefaultFormat(option.value)
|
||||
showMessage('已更新导出格式默认值', true)
|
||||
setShowExportFormatSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
{option.desc && <span className="option-desc">{option.desc}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>默认语音转文字</label>
|
||||
<span className="form-hint">导出时默认将语音转写为文字</span>
|
||||
<div className="log-toggle-line">
|
||||
<span className="log-status">{exportDefaultVoiceAsText ? '已开启' : '已关闭'}</span>
|
||||
<label className="switch" htmlFor="export-default-voice-as-text">
|
||||
<input
|
||||
id="export-default-voice-as-text"
|
||||
className="switch-input"
|
||||
type="checkbox"
|
||||
checked={exportDefaultVoiceAsText}
|
||||
onChange={async (e) => {
|
||||
const enabled = e.target.checked
|
||||
setExportDefaultVoiceAsText(enabled)
|
||||
await configService.setExportDefaultVoiceAsText(enabled)
|
||||
showMessage(enabled ? '已开启默认语音转文字' : '已关闭默认语音转文字', true)
|
||||
<div className="form-group">
|
||||
<label>默认导出时间范围</label>
|
||||
<span className="form-hint">控制导出页面的默认时间选择</span>
|
||||
<div className="select-field" ref={exportDateRangeDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`select-trigger ${showExportDateRangeSelect ? 'open' : ''}`}
|
||||
onClick={() => {
|
||||
setShowExportDateRangeSelect(!showExportDateRangeSelect)
|
||||
setShowExportFormatSelect(false)
|
||||
setShowExportExcelColumnsSelect(false)
|
||||
}}
|
||||
/>
|
||||
<span className="switch-slider" />
|
||||
</label>
|
||||
>
|
||||
<span className="select-value">{exportDateRangeLabel}</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{showExportDateRangeSelect && (
|
||||
<div className="select-dropdown">
|
||||
{exportDateRangeOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`select-option ${exportDefaultDateRange === option.value ? 'active' : ''}`}
|
||||
onClick={async () => {
|
||||
setExportDefaultDateRange(option.value)
|
||||
await configService.setExportDefaultDateRange(option.value)
|
||||
showMessage('已更新默认导出时间范围', true)
|
||||
setShowExportDateRangeSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Excel 列显示</label>
|
||||
<span className="form-hint">控制 Excel 导出的列字段</span>
|
||||
<div className="select-field" ref={exportExcelColumnsDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`select-trigger ${showExportExcelColumnsSelect ? 'open' : ''}`}
|
||||
onClick={() => {
|
||||
setShowExportExcelColumnsSelect(!showExportExcelColumnsSelect)
|
||||
setShowExportFormatSelect(false)
|
||||
setShowExportDateRangeSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="select-value">{exportExcelColumnsLabel}</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{showExportExcelColumnsSelect && (
|
||||
<div className="select-dropdown">
|
||||
{exportExcelColumnOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`select-option ${exportExcelColumnsValue === option.value ? 'active' : ''}`}
|
||||
onClick={async () => {
|
||||
const compact = option.value === 'compact'
|
||||
setExportDefaultExcelCompactColumns(compact)
|
||||
await configService.setExportDefaultExcelCompactColumns(compact)
|
||||
showMessage(compact ? '已启用精简列' : '已启用完整列', true)
|
||||
setShowExportExcelColumnsSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
{option.desc && <span className="option-desc">{option.desc}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label>默认导出媒体文件</label>
|
||||
<span className="form-hint">控制图片/语音/表情的默认导出开关</span>
|
||||
<div className="log-toggle-line">
|
||||
<span className="log-status">{exportDefaultMedia ? '已开启' : '已关闭'}</span>
|
||||
<label className="switch" htmlFor="export-default-media">
|
||||
<input
|
||||
id="export-default-media"
|
||||
className="switch-input"
|
||||
type="checkbox"
|
||||
checked={exportDefaultMedia}
|
||||
onChange={async (e) => {
|
||||
const enabled = e.target.checked
|
||||
setExportDefaultMedia(enabled)
|
||||
await configService.setExportDefaultMedia(enabled)
|
||||
showMessage(enabled ? '已开启默认媒体导出' : '已关闭默认媒体导出', true)
|
||||
}}
|
||||
/>
|
||||
<span className="switch-slider" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>默认语音转文字</label>
|
||||
<span className="form-hint">导出时默认将语音转写为文字</span>
|
||||
<div className="log-toggle-line">
|
||||
<span className="log-status">{exportDefaultVoiceAsText ? '已开启' : '已关闭'}</span>
|
||||
<label className="switch" htmlFor="export-default-voice-as-text">
|
||||
<input
|
||||
id="export-default-voice-as-text"
|
||||
className="switch-input"
|
||||
type="checkbox"
|
||||
checked={exportDefaultVoiceAsText}
|
||||
onChange={async (e) => {
|
||||
const enabled = e.target.checked
|
||||
setExportDefaultVoiceAsText(enabled)
|
||||
await configService.setExportDefaultVoiceAsText(enabled)
|
||||
showMessage(enabled ? '已开启默认语音转文字' : '已关闭默认语音转文字', true)
|
||||
}}
|
||||
/>
|
||||
<span className="switch-slider" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Excel 列显示</label>
|
||||
<span className="form-hint">控制 Excel 导出的列字段</span>
|
||||
<div className="select-field" ref={exportExcelColumnsDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`select-trigger ${showExportExcelColumnsSelect ? 'open' : ''}`}
|
||||
onClick={() => {
|
||||
setShowExportExcelColumnsSelect(!showExportExcelColumnsSelect)
|
||||
setShowExportFormatSelect(false)
|
||||
setShowExportDateRangeSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="select-value">{exportExcelColumnsLabel}</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{showExportExcelColumnsSelect && (
|
||||
<div className="select-dropdown">
|
||||
{exportExcelColumnOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`select-option ${exportExcelColumnsValue === option.value ? 'active' : ''}`}
|
||||
onClick={async () => {
|
||||
const compact = option.value === 'compact'
|
||||
setExportDefaultExcelCompactColumns(compact)
|
||||
await configService.setExportDefaultExcelCompactColumns(compact)
|
||||
showMessage(compact ? '已启用精简列' : '已启用完整列', true)
|
||||
setShowExportExcelColumnsSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
{option.desc && <span className="option-desc">{option.desc}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const renderCacheTab = () => (
|
||||
@@ -1204,23 +1222,26 @@ function SettingsPage() {
|
||||
<>
|
||||
<p className="update-hint">新版 v{updateInfo.version} 可用</p>
|
||||
{isDownloading ? (
|
||||
<div className="download-progress">
|
||||
<div className="update-progress">
|
||||
<div className="progress-bar">
|
||||
<div className="progress-fill" style={{ width: `${downloadProgress}%` }} />
|
||||
<div className="progress-inner" style={{ width: `${(downloadProgress?.percent || 0)}%` }} />
|
||||
</div>
|
||||
<span>{downloadProgress.toFixed(0)}%</span>
|
||||
<span>{(downloadProgress?.percent || 0).toFixed(0)}%</span>
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn btn-primary" onClick={handleUpdateNow}>
|
||||
<button className="btn btn-primary" onClick={() => setShowUpdateDialog(true)}>
|
||||
<Download size={16} /> 立即更新
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<button className="btn btn-secondary" onClick={handleCheckUpdate} disabled={isCheckingUpdate}>
|
||||
<RefreshCw size={16} className={isCheckingUpdate ? 'spin' : ''} />
|
||||
{isCheckingUpdate ? '检查中...' : '检查更新'}
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<button className="btn btn-secondary" onClick={handleCheckUpdate} disabled={isCheckingUpdate}>
|
||||
<RefreshCw size={16} className={isCheckingUpdate ? 'spin' : ''} />
|
||||
{isCheckingUpdate ? '检查中...' : '检查更新'}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1299,6 +1320,7 @@ function SettingsPage() {
|
||||
{activeTab === 'cache' && renderCacheTab()}
|
||||
{activeTab === 'about' && renderAboutTab()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,70 +10,47 @@
|
||||
}
|
||||
|
||||
.sns-sidebar {
|
||||
width: 300px;
|
||||
width: 320px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-left: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&.closed {
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
transform: translateX(100%);
|
||||
pointer-events: none;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 18px 20px;
|
||||
padding: 0 24px;
|
||||
height: 64px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
/* justify-content: space-between; -- No longer needed as it's just h3 */
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
|
||||
.title-icon {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--hover-bg);
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-y: hidden;
|
||||
/* Changed from auto to hidden to allow inner scrolling of contact list */
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -86,6 +63,7 @@
|
||||
padding: 14px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.04);
|
||||
@@ -172,7 +150,7 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0; // 改为 0 以支持 flex 压缩
|
||||
min-height: 200px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -181,7 +159,7 @@
|
||||
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 0px;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
@@ -258,12 +236,16 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.section-header {
|
||||
padding: 16px 16px 1px 16px;
|
||||
margin-bottom: 12px;
|
||||
/* Increased spacing */
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
@@ -306,6 +288,7 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
@@ -354,6 +337,7 @@
|
||||
overflow-y: auto;
|
||||
padding: 4px 8px;
|
||||
margin: 0 4px 8px 4px;
|
||||
min-height: 0;
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
@@ -524,6 +508,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -553,6 +543,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.sns-content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -739,6 +730,61 @@
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.live-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
left: auto;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(4px);
|
||||
color: white;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
transition: opacity 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.download-btn-overlay {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 2;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
transform: scale(1.1);
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.download-btn-overlay {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.media-error-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -937,4 +983,197 @@
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug Dialog Styles
|
||||
.debug-btn {
|
||||
margin-left: auto;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--hover-bg);
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.debug-dialog {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
|
||||
.debug-dialog-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--hover-bg);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.debug-dialog-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
|
||||
.debug-section {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.debug-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
align-items: flex-start;
|
||||
|
||||
.debug-key {
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
min-width: 140px;
|
||||
font-size: 13px;
|
||||
font-family: 'Consolas', 'Microsoft YaHei', 'SimHei', monospace;
|
||||
}
|
||||
|
||||
.debug-value {
|
||||
flex: 1;
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
font-family: 'Consolas', 'Microsoft YaHei', 'SimHei', monospace;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
padding: 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.media-debug-item {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.media-debug-header {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.live-photo-debug {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--border-color);
|
||||
|
||||
.live-photo-label {
|
||||
font-weight: 500;
|
||||
color: var(--accent-color);
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.json-code {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
overflow-x: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
user-select: all;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.copy-json-btn {
|
||||
margin-top: 12px;
|
||||
padding: 8px 16px;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(var(--accent-color-rgb), 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
|
||||
import { RefreshCw, Heart, Search, Calendar, User, X, Filter, Play, ImageIcon } from 'lucide-react'
|
||||
import { RefreshCw, Heart, Search, Calendar, User, X, Filter, Play, ImageIcon, Zap, Download, ChevronRight } from 'lucide-react'
|
||||
import { Avatar } from '../components/Avatar'
|
||||
import { ImagePreview } from '../components/ImagePreview'
|
||||
import JumpToDateDialog from '../components/JumpToDateDialog'
|
||||
import { LivePhotoIcon } from '../components/LivePhotoIcon'
|
||||
import './SnsPage.scss'
|
||||
|
||||
interface SnsPost {
|
||||
@@ -13,29 +14,64 @@ interface SnsPost {
|
||||
createTime: number
|
||||
contentDesc: string
|
||||
type?: number
|
||||
media: { url: string; thumb: string }[]
|
||||
media: {
|
||||
url: string
|
||||
thumb: string
|
||||
md5?: string
|
||||
token?: string
|
||||
key?: string
|
||||
encIdx?: string
|
||||
livePhoto?: {
|
||||
url: string
|
||||
thumb: string
|
||||
token?: string
|
||||
key?: string
|
||||
encIdx?: string
|
||||
}
|
||||
}[]
|
||||
likes: string[]
|
||||
comments: { id: string; nickname: string; content: string; refCommentId: string; refNickname?: string }[]
|
||||
rawXml?: string // 原始 XML 数据
|
||||
}
|
||||
|
||||
const MediaItem = ({ url, thumb, onPreview }: { url: string, thumb: string, onPreview: () => void }) => {
|
||||
const MediaItem = ({ media, onPreview }: { media: any, onPreview: () => void }) => {
|
||||
const [error, setError] = useState(false);
|
||||
const { url, thumb, livePhoto } = media;
|
||||
const isLive = !!livePhoto;
|
||||
const targetUrl = thumb || url;
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
let downloadUrl = url;
|
||||
let downloadKey = media.key || '';
|
||||
|
||||
if (isLive && media.livePhoto) {
|
||||
downloadUrl = media.livePhoto.url;
|
||||
downloadKey = media.livePhoto.key || '';
|
||||
}
|
||||
|
||||
// TODO: 调用后端下载服务
|
||||
// window.electronAPI.sns.download(downloadUrl, downloadKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`media-item ${error ? 'error' : ''}`}>
|
||||
{!error ? (
|
||||
<img
|
||||
src={thumb || url}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
onClick={onPreview}
|
||||
onError={() => setError(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="media-error-placeholder" onClick={onPreview}>
|
||||
<ImageIcon size={24} style={{ opacity: 0.3 }} />
|
||||
<div className={`media-item ${error ? 'error' : ''}`} onClick={onPreview}>
|
||||
<img
|
||||
src={targetUrl}
|
||||
alt=""
|
||||
referrerPolicy="no-referrer"
|
||||
loading="lazy"
|
||||
onError={() => setError(true)}
|
||||
/>
|
||||
{isLive && (
|
||||
<div className="live-badge">
|
||||
<LivePhotoIcon size={16} className="live-icon" />
|
||||
</div>
|
||||
)}
|
||||
<button className="download-btn-overlay" onClick={handleDownload} title="下载原图">
|
||||
<Download size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -65,6 +101,7 @@ export default function SnsPage() {
|
||||
const [showJumpDialog, setShowJumpDialog] = useState(false)
|
||||
const [jumpTargetDate, setJumpTargetDate] = useState<Date | undefined>(undefined)
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null)
|
||||
const [debugPost, setDebugPost] = useState<SnsPost | null>(null)
|
||||
|
||||
const postsContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -264,7 +301,7 @@ export default function SnsPage() {
|
||||
setHasNewer(false)
|
||||
setSelectedUsernames([])
|
||||
setSearchKeyword('')
|
||||
setJumpTargetDate(null)
|
||||
setJumpTargetDate(undefined)
|
||||
loadContacts()
|
||||
loadPosts({ reset: true })
|
||||
}
|
||||
@@ -347,16 +384,157 @@ export default function SnsPage() {
|
||||
return (
|
||||
<div className="sns-page">
|
||||
<div className="sns-container">
|
||||
{/* 侧边栏:过滤与搜索 */}
|
||||
<main className="sns-main">
|
||||
<div className="sns-header">
|
||||
<div className="header-left">
|
||||
<h2>社交动态</h2>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<button
|
||||
className={`icon-btn sidebar-trigger ${isSidebarOpen ? 'active' : ''}`}
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
title={isSidebarOpen ? "收起筛选" : "打开筛选"}
|
||||
>
|
||||
<Filter size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (jumpTargetDate) setJumpTargetDate(undefined);
|
||||
loadPosts({ reset: true });
|
||||
}}
|
||||
disabled={loading || loadingNewer}
|
||||
className="icon-btn refresh-btn"
|
||||
title="刷新"
|
||||
>
|
||||
<RefreshCw size={18} className={(loading || loadingNewer) ? 'spinning' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sns-content-wrapper">
|
||||
<div className="sns-content custom-scrollbar" onScroll={handleScroll} onWheel={handleWheel} ref={postsContainerRef}>
|
||||
<div className="posts-list">
|
||||
{loadingNewer && (
|
||||
<div className="status-indicator loading-newer">
|
||||
<RefreshCw size={16} className="spinning" />
|
||||
<span>正在检查更新的动态...</span>
|
||||
</div>
|
||||
)}
|
||||
{!loadingNewer && hasNewer && (
|
||||
<div className="status-indicator newer-hint" onClick={() => loadPosts({ direction: 'newer' })}>
|
||||
查看更新的动态
|
||||
</div>
|
||||
)}
|
||||
{posts.map((post, index) => {
|
||||
return (
|
||||
<div key={post.id} className="sns-post-row">
|
||||
<div className="sns-post-wrapper">
|
||||
<div className="sns-post">
|
||||
<div className="post-header">
|
||||
<Avatar
|
||||
src={post.avatarUrl}
|
||||
name={post.nickname}
|
||||
size={44}
|
||||
shape="rounded"
|
||||
/>
|
||||
<div className="post-info">
|
||||
<div className="nickname">{post.nickname}</div>
|
||||
<div className="time">{formatTime(post.createTime)}</div>
|
||||
</div>
|
||||
<button
|
||||
className="debug-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDebugPost(post);
|
||||
}}
|
||||
title="查看原始数据"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="16 18 22 12 16 6"></polyline>
|
||||
<polyline points="8 6 2 12 8 18"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="post-body">
|
||||
{post.contentDesc && <div className="post-text">{post.contentDesc}</div>}
|
||||
|
||||
{post.type === 15 ? (
|
||||
<div className="post-video-placeholder">
|
||||
<Play size={20} />
|
||||
<span>视频动态</span>
|
||||
</div>
|
||||
) : post.media.length > 0 && (
|
||||
<div className={`post-media-grid media-count-${Math.min(post.media.length, 9)}`}>
|
||||
{post.media.map((m, idx) => (
|
||||
<MediaItem key={idx} media={m} onPreview={() => setPreviewImage(m.url)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(post.likes.length > 0 || post.comments.length > 0) && (
|
||||
<div className="post-footer">
|
||||
{post.likes.length > 0 && (
|
||||
<div className="likes-section">
|
||||
<Heart size={14} className="icon" />
|
||||
<span className="likes-list">
|
||||
{post.likes.join('、')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{post.comments.length > 0 && (
|
||||
<div className="comments-section">
|
||||
{post.comments.map((c, idx) => (
|
||||
<div key={idx} className="comment-item">
|
||||
<span className="comment-user">{c.nickname}</span>
|
||||
{c.refNickname && (
|
||||
<>
|
||||
<span className="reply-text">回复</span>
|
||||
<span className="comment-user">{c.refNickname}</span>
|
||||
</>
|
||||
)}
|
||||
<span className="comment-separator">: </span>
|
||||
<span className="comment-content">{c.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{loading && <div className="status-indicator loading-more">
|
||||
<RefreshCw size={16} className="spinning" />
|
||||
<span>正在加载更多...</span>
|
||||
</div>}
|
||||
{!hasMore && posts.length > 0 && <div className="status-indicator no-more">已经到底啦</div>}
|
||||
{!loading && posts.length === 0 && (
|
||||
<div className="no-results">
|
||||
<div className="no-results-icon"><Search size={48} /></div>
|
||||
<p>未找到相关动态</p>
|
||||
{(selectedUsernames.length > 0 || searchKeyword) && (
|
||||
<button onClick={clearFilters} className="reset-inline">
|
||||
重置搜索条件
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 侧边栏:过滤与搜索 (moved to right) */}
|
||||
<aside className={`sns-sidebar ${isSidebarOpen ? 'open' : 'closed'}`}>
|
||||
<div className="sidebar-header">
|
||||
<div className="title-wrapper">
|
||||
<Filter size={18} className="title-icon" />
|
||||
<h3>筛选条件</h3>
|
||||
</div>
|
||||
<button className="toggle-btn" onClick={() => setIsSidebarOpen(false)}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
<h3>筛选条件</h3>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="filter-content custom-scrollbar">
|
||||
@@ -460,136 +638,6 @@ export default function SnsPage() {
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="sns-main">
|
||||
<div className="sns-header">
|
||||
<div className="header-left">
|
||||
{!isSidebarOpen && (
|
||||
<button className="icon-btn sidebar-trigger" onClick={() => setIsSidebarOpen(true)}>
|
||||
<Filter size={20} />
|
||||
</button>
|
||||
)}
|
||||
<h2>社交动态</h2>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (jumpTargetDate) setJumpTargetDate(undefined);
|
||||
loadPosts({ reset: true });
|
||||
}}
|
||||
disabled={loading || loadingNewer}
|
||||
className="icon-btn refresh-btn"
|
||||
>
|
||||
<RefreshCw size={18} className={(loading || loadingNewer) ? 'spinning' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sns-content-wrapper">
|
||||
<div className="sns-content custom-scrollbar" onScroll={handleScroll} onWheel={handleWheel} ref={postsContainerRef}>
|
||||
<div className="posts-list">
|
||||
{loadingNewer && (
|
||||
<div className="status-indicator loading-newer">
|
||||
<RefreshCw size={16} className="spinning" />
|
||||
<span>正在检查更新的动态...</span>
|
||||
</div>
|
||||
)}
|
||||
{!loadingNewer && hasNewer && (
|
||||
<div className="status-indicator newer-hint" onClick={() => loadPosts({ direction: 'newer' })}>
|
||||
查看更新的动态
|
||||
</div>
|
||||
)}
|
||||
{posts.map((post, index) => {
|
||||
return (
|
||||
<div key={post.id} className="sns-post-row">
|
||||
<div className="sns-post-wrapper">
|
||||
<div className="sns-post">
|
||||
<div className="post-header">
|
||||
<Avatar
|
||||
src={post.avatarUrl}
|
||||
name={post.nickname}
|
||||
size={44}
|
||||
shape="rounded"
|
||||
/>
|
||||
<div className="post-info">
|
||||
<div className="nickname">{post.nickname}</div>
|
||||
<div className="time">{formatTime(post.createTime)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="post-body">
|
||||
{post.contentDesc && <div className="post-text">{post.contentDesc}</div>}
|
||||
|
||||
{post.type === 15 ? (
|
||||
<div className="post-video-placeholder">
|
||||
<Play size={20} />
|
||||
<span>视频动态</span>
|
||||
</div>
|
||||
) : post.media.length > 0 && (
|
||||
<div className={`post-media-grid media-count-${Math.min(post.media.length, 9)}`}>
|
||||
{post.media.map((m, idx) => (
|
||||
<MediaItem key={idx} url={m.url} thumb={m.thumb} onPreview={() => setPreviewImage(m.url)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(post.likes.length > 0 || post.comments.length > 0) && (
|
||||
<div className="post-footer">
|
||||
{post.likes.length > 0 && (
|
||||
<div className="likes-section">
|
||||
<Heart size={14} className="icon" />
|
||||
<span className="likes-list">
|
||||
{post.likes.join('、')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{post.comments.length > 0 && (
|
||||
<div className="comments-section">
|
||||
{post.comments.map((c, idx) => (
|
||||
<div key={idx} className="comment-item">
|
||||
<span className="comment-user">{c.nickname}</span>
|
||||
{c.refNickname && (
|
||||
<>
|
||||
<span className="reply-text">回复</span>
|
||||
<span className="comment-user">{c.refNickname}</span>
|
||||
</>
|
||||
)}
|
||||
<span className="comment-separator">: </span>
|
||||
<span className="comment-content">{c.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{loading && <div className="status-indicator loading-more">
|
||||
<RefreshCw size={16} className="spinning" />
|
||||
<span>正在加载更多...</span>
|
||||
</div>}
|
||||
{!hasMore && posts.length > 0 && <div className="status-indicator no-more">已经到底啦</div>}
|
||||
{!loading && posts.length === 0 && (
|
||||
<div className="no-results">
|
||||
<div className="no-results-icon"><Search size={48} /></div>
|
||||
<p>未找到相关动态</p>
|
||||
{(selectedUsernames.length > 0 || searchKeyword) && (
|
||||
<button onClick={clearFilters} className="reset-inline">
|
||||
重置搜索条件
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{previewImage && (
|
||||
<ImagePreview src={previewImage} onClose={() => setPreviewImage(null)} />
|
||||
@@ -605,6 +653,154 @@ export default function SnsPage() {
|
||||
}}
|
||||
currentDate={jumpTargetDate || new Date()}
|
||||
/>
|
||||
|
||||
{/* Debug Info Dialog */}
|
||||
{debugPost && (
|
||||
<div className="modal-overlay" onClick={() => setDebugPost(null)}>
|
||||
<div className="debug-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="debug-dialog-header">
|
||||
<h3>原始数据 - {debugPost.nickname}</h3>
|
||||
<button className="close-btn" onClick={() => setDebugPost(null)}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="debug-dialog-body">
|
||||
|
||||
<div className="debug-section">
|
||||
<h4>ℹ 基本信息</h4>
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">ID:</span>
|
||||
<span className="debug-value">{debugPost.id}</span>
|
||||
</div>
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">用户名:</span>
|
||||
<span className="debug-value">{debugPost.username}</span>
|
||||
</div>
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">昵称:</span>
|
||||
<span className="debug-value">{debugPost.nickname}</span>
|
||||
</div>
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">时间:</span>
|
||||
<span className="debug-value">{new Date(debugPost.createTime * 1000).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">类型:</span>
|
||||
<span className="debug-value">{debugPost.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="debug-section">
|
||||
<h4> 媒体信息 ({debugPost.media.length} 项)</h4>
|
||||
{debugPost.media.map((media, idx) => (
|
||||
<div key={idx} className="media-debug-item">
|
||||
<div className="media-debug-header">媒体 {idx + 1}</div>
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">URL:</span>
|
||||
<span className="debug-value">{media.url}</span>
|
||||
</div>
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">缩略图:</span>
|
||||
<span className="debug-value">{media.thumb}</span>
|
||||
</div>
|
||||
{media.md5 && (
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">MD5:</span>
|
||||
<span className="debug-value">{media.md5}</span>
|
||||
</div>
|
||||
)}
|
||||
{media.token && (
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">Token:</span>
|
||||
<span className="debug-value">{media.token}</span>
|
||||
</div>
|
||||
)}
|
||||
{media.key && (
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">Key (解密密钥):</span>
|
||||
<span className="debug-value">{media.key}</span>
|
||||
</div>
|
||||
)}
|
||||
{media.encIdx && (
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">Enc Index:</span>
|
||||
<span className="debug-value">{media.encIdx}</span>
|
||||
</div>
|
||||
)}
|
||||
{media.livePhoto && (
|
||||
<div className="live-photo-debug">
|
||||
<div className="live-photo-label"> Live Photo 视频部分:</div>
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">视频 URL:</span>
|
||||
<span className="debug-value">{media.livePhoto.url}</span>
|
||||
</div>
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">视频缩略图:</span>
|
||||
<span className="debug-value">{media.livePhoto.thumb}</span>
|
||||
</div>
|
||||
{media.livePhoto.token && (
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">视频 Token:</span>
|
||||
<span className="debug-value">{media.livePhoto.token}</span>
|
||||
</div>
|
||||
)}
|
||||
{media.livePhoto.key && (
|
||||
<div className="debug-item">
|
||||
<span className="debug-key">视频 Key:</span>
|
||||
<span className="debug-value">{media.livePhoto.key}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 原始 XML */}
|
||||
{debugPost.rawXml && (
|
||||
<div className="debug-section">
|
||||
<h4> 原始 XML 数据</h4>
|
||||
<pre className="json-code">{(() => {
|
||||
// XML 缩进格式化
|
||||
let formatted = '';
|
||||
let indent = 0;
|
||||
const tab = ' ';
|
||||
const parts = debugPost.rawXml.split(/(<[^>]+>)/g).filter(p => p.trim());
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part.startsWith('<')) {
|
||||
if (part.trim()) formatted += part;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.startsWith('</')) {
|
||||
indent = Math.max(0, indent - 1);
|
||||
formatted += '\n' + tab.repeat(indent) + part;
|
||||
} else if (part.endsWith('/>')) {
|
||||
formatted += '\n' + tab.repeat(indent) + part;
|
||||
} else {
|
||||
formatted += '\n' + tab.repeat(indent) + part;
|
||||
indent++;
|
||||
}
|
||||
}
|
||||
|
||||
return formatted.trim();
|
||||
})()}</pre>
|
||||
<button
|
||||
className="copy-json-btn"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(debugPost.rawXml || '');
|
||||
alert('已复制 XML 到剪贴板');
|
||||
}}
|
||||
>
|
||||
复制 XML
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user