refactor: wechat页per-webhook独立保存 + monitor页用useDirtyTracker统一section保存

- wechat页: 每推送组底部独立保存按钮(仅修改后显示),移除全局保存按钮
- monitor页: 用 useDirtyTracker 替代 originalConfig+hasSectionChanged 模式
This commit is contained in:
gitadmin 2026-07-13 16:41:07 +08:00
parent 991b7b85c3
commit 459e19cbb8
2 changed files with 33 additions and 28 deletions

View File

@ -2,6 +2,7 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { Card } from '@/components/ui' import { Card } from '@/components/ui'
import { Search, RefreshCw } from 'lucide-react' import { Search, RefreshCw } from 'lucide-react'
import { useDirtyTracker } from '@shared/ui/hooks/useDirtyTracker'
interface MonitorStatus { interface MonitorStatus {
enabled: boolean enabled: boolean
@ -53,8 +54,8 @@ type SectionKey = 'mail' | 'filter' | 'monitor'
export default function MonitorSettingsPage() { export default function MonitorSettingsPage() {
const [status, setStatus] = useState<MonitorStatus | null>(null) const [status, setStatus] = useState<MonitorStatus | null>(null)
const [config, setConfig] = useState<MonitorConfig | null>(null) const [config, setConfig] = useState<MonitorConfig | null>(null)
const [originalConfig, setOriginalConfig] = useState<MonitorConfig | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const { isDirty, markClean } = useDirtyTracker()
const [saving, setSaving] = useState<SectionKey | null>(null) const [saving, setSaving] = useState<SectionKey | null>(null)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [newKeyword, setNewKeyword] = useState('') const [newKeyword, setNewKeyword] = useState('')
@ -212,7 +213,9 @@ export default function MonitorSettingsPage() {
]).then(([s, c, scan, history]) => { ]).then(([s, c, scan, history]) => {
setStatus(s) setStatus(s)
setConfig(c) setConfig(c)
setOriginalConfig(JSON.parse(JSON.stringify(c))) markClean('mail', { mail: c.mail })
markClean('filter', { filter: c.filter })
markClean('monitor', { monitor: c.monitor })
setScanState(scan) setScanState(scan)
setScanHistory(history) setScanHistory(history)
setLoading(false) setLoading(false)
@ -361,7 +364,7 @@ export default function MonitorSettingsPage() {
const data = await res.json() const data = await res.json()
setSaving(null) setSaving(null)
if (data.success) { if (data.success) {
setOriginalConfig(prev => prev ? { ...prev, [section]: JSON.parse(JSON.stringify(config[section])) } : prev) markClean(section, { [section]: config[section] })
setSectionMessage(prev => ({ ...prev, [section]: { type: 'success', text: '已保存' } })) setSectionMessage(prev => ({ ...prev, [section]: { type: 'success', text: '已保存' } }))
} else { } else {
setSectionMessage(prev => ({ ...prev, [section]: { type: 'error', text: data.error } })) setSectionMessage(prev => ({ ...prev, [section]: { type: 'error', text: data.error } }))
@ -380,16 +383,12 @@ export default function MonitorSettingsPage() {
setConfig({ ...config, filter: { ...config.filter, subject_keywords: config.filter.subject_keywords.filter((_, i) => i !== index) } }) setConfig({ ...config, filter: { ...config.filter, subject_keywords: config.filter.subject_keywords.filter((_, i) => i !== index) } })
} }
function hasSectionChanged(original: MonitorConfig | null, current: MonitorConfig | null, section: SectionKey): boolean {
if (!original || !current) return false
return JSON.stringify(original[section]) !== JSON.stringify(current[section])
}
const inputClass = (path: string, changed = false) => const inputClass = (path: string, changed = false) =>
`w-full px-3 py-2 border rounded-lg text-sm transition-colors ${changed ? 'border-amber-400 bg-amber-50 dark:bg-amber-900/20 dark:border-amber-600' : 'border-slate-300 dark:border-slate-600'}` `w-full px-3 py-2 border rounded-lg text-sm transition-colors ${changed ? 'border-amber-400 bg-amber-50 dark:bg-amber-900/20 dark:border-amber-600' : 'border-slate-300 dark:border-slate-600'}`
const SectionSaveButton = ({ section }: { section: SectionKey }) => { const SectionSaveButton = ({ section }: { section: SectionKey }) => {
const changed = hasSectionChanged(originalConfig, config, section) if (!config) return null
const changed = isDirty(section, { [section]: config[section] })
if (!changed) return null if (!changed) return null
return ( return (
<div className="mt-4 flex items-center gap-3"> <div className="mt-4 flex items-center gap-3">

View File

@ -2,6 +2,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Card } from '@/components/ui' import { Card } from '@/components/ui'
import { Plus, Trash2, ChevronDown, ChevronUp } from 'lucide-react' import { Plus, Trash2, ChevronDown, ChevronUp } from 'lucide-react'
import { useDirtyTracker } from '@shared/ui/hooks/useDirtyTracker'
interface WebhookConfig { interface WebhookConfig {
id: string id: string
@ -23,7 +24,8 @@ function generateId() {
export default function WechatSettingsPage() { export default function WechatSettingsPage() {
const [config, setConfig] = useState<{ wechat: { webhooks: WebhookConfig[] } } | null>(null) const [config, setConfig] = useState<{ wechat: { webhooks: WebhookConfig[] } } | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false) const { isDirty, markClean, removeItem } = useDirtyTracker()
const [savingId, setSavingId] = useState<string | null>(null)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [expandedWebhooks, setExpandedWebhooks] = useState<Set<string>>(new Set()) const [expandedWebhooks, setExpandedWebhooks] = useState<Set<string>>(new Set())
const [testingWebhook, setTestingWebhook] = useState<string | null>(null) const [testingWebhook, setTestingWebhook] = useState<string | null>(null)
@ -40,25 +42,28 @@ export default function WechatSettingsPage() {
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, []) }, [])
// 保存 // 保存单个推送组
const handleSave = async () => { const handleSaveSingle = async (id: string) => {
if (!config) return if (!config) return
setSaving(true) const wh = config.wechat.webhooks.find(w => w.id === id)
if (!wh) return
setSavingId(id)
setMessage(null) setMessage(null)
try { try {
const res = await fetch('/api/monitor/settings', { const res = await fetch('/api/monitor/settings', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ wechat: config.wechat }), body: JSON.stringify({ wechat: { webhooks: [wh] } }),
}) })
const data = await res.json() const data = await res.json()
if (!res.ok) throw new Error(data.error || '保存失败') if (!res.ok) throw new Error(data.error || '保存失败')
setMessage({ type: 'success', text: '保存成功' }) markClean(id, wh)
setMessage({ type: 'success', text: `${wh.name}」保存成功` })
setTimeout(() => setMessage(null), 3000) setTimeout(() => setMessage(null), 3000)
} catch (err) { } catch (err) {
setMessage({ type: 'error', text: '保存失败: ' + (err instanceof Error ? err.message : String(err)) }) setMessage({ type: 'error', text: `${wh.name}」保存失败: ${err instanceof Error ? err.message : String(err)}` })
} finally { } finally {
setSaving(false) setSavingId(null)
} }
} }
@ -99,6 +104,7 @@ export default function WechatSettingsPage() {
if (!config) return if (!config) return
if (config.wechat.webhooks.length <= 1) return if (config.wechat.webhooks.length <= 1) return
setConfig({ wechat: { ...config.wechat, webhooks: config.wechat.webhooks.filter(w => w.id !== id) } }) setConfig({ wechat: { ...config.wechat, webhooks: config.wechat.webhooks.filter(w => w.id !== id) } })
removeItem(id)
} }
// 更新 webhook 基本字段 // 更新 webhook 基本字段
@ -265,22 +271,22 @@ export default function WechatSettingsPage() {
</div> </div>
</div> </div>
</div> </div>
{isDirty(wh.id, wh) && (
<div className="flex justify-end pt-2 border-t border-slate-100 dark:border-slate-700/50">
<button
onClick={() => handleSaveSingle(wh.id)}
disabled={savingId === wh.id}
className="px-3 py-1.5 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 text-xs font-medium"
>
{savingId === wh.id ? '保存中...' : '保存'}
</button>
</div>
)}
</div> </div>
)} )}
</div> </div>
))} ))}
</div> </div>
{/* 保存按钮 */}
<div className="mt-4 flex items-center gap-3">
<button
onClick={handleSave}
disabled={saving}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 text-sm font-medium"
>
{saving ? '保存中...' : '保存设置'}
</button>
</div>
</Card> </Card>
</div> </div>
) )