refactor: wechat页per-webhook独立保存 + monitor页用useDirtyTracker统一section保存
- wechat页: 每推送组底部独立保存按钮(仅修改后显示),移除全局保存按钮 - monitor页: 用 useDirtyTracker 替代 originalConfig+hasSectionChanged 模式
This commit is contained in:
parent
991b7b85c3
commit
459e19cbb8
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue