/* Copyright 2024-2026 ReplyPier Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import { computed, ref, type Ref } from 'vue' import { toast } from 'vue-sonner' import { faqService } from '@/services/faq' import type { HelpCenterSettings, HelpCenterSettingsUpdate } from '@/types/faq' export type SaveState = 'idle' | 'saving' | 'saved' | 'error' const AUTOSAVE_DEBOUNCE_MS = 800 const MAX_LOGO_BYTES = 2 * 1024 * 1024 const MAX_FAVICON_BYTES = 1 * 1024 * 1024 /** Appearance/settings editing with debounced autosave — the design has no * Save button ("changes apply instantly to your published help center"). */ export function useHelpCenterSettings(settings: Ref) { const saveState = ref('idle') let debounceTimer: ReturnType | null = null let saveInFlight = false let trailingSave: HelpCenterSettingsUpdate | null = null let debouncedPayload: HelpCenterSettingsUpdate | null = null const brandColor = computed(() => settings.value?.brand_color || '#4338CA') function applyResponse(updated: HelpCenterSettings): void { const pending = { ...(trailingSave || {}), ...(debouncedPayload || {}) } settings.value = { ...updated, ...pending } } async function save(payload: HelpCenterSettingsUpdate): Promise { if (saveInFlight) { trailingSave = { ...(trailingSave || {}), ...payload } return } saveInFlight = true saveState.value = 'saving' try { applyResponse(await faqService.updateSettings(payload)) saveState.value = 'saved' } catch (error: any) { saveState.value = 'error' toast.error(error.message) } finally { saveInFlight = false if (trailingSave) { const queued = trailingSave trailingSave = null void save(queued) } } } /** Debounced path for text inputs (links, CTA, labels). */ function queueSave(payload: HelpCenterSettingsUpdate): void { debouncedPayload = { ...(debouncedPayload || {}), ...payload } if (debounceTimer) clearTimeout(debounceTimer) debounceTimer = setTimeout(() => { debounceTimer = null const queued = debouncedPayload debouncedPayload = null if (queued) void save(queued) }, AUTOSAVE_DEBOUNCE_MS) } /** Immediate path for discrete actions (swatch click, toggles, selects). */ async function saveNow(payload: HelpCenterSettingsUpdate): Promise { if (debounceTimer) clearTimeout(debounceTimer) debounceTimer = null const combined = { ...(debouncedPayload || {}), ...payload } debouncedPayload = null await save(combined) } async function uploadLogo(file: File): Promise { if (file.size > MAX_LOGO_BYTES) { toast.error('Logo 不能超过 2 MB') return } saveState.value = 'saving' try { applyResponse(await faqService.uploadLogo(file)) saveState.value = 'saved' } catch (error: any) { saveState.value = 'error' toast.error(error.message) } } async function removeLogo(): Promise { saveState.value = 'saving' try { applyResponse(await faqService.removeLogo()) saveState.value = 'saved' } catch (error: any) { saveState.value = 'error' toast.error(error.message) } } async function uploadFavicon(file: File): Promise { if (file.size > MAX_FAVICON_BYTES) { toast.error('网站图标不能超过 1 MB') return } saveState.value = 'saving' try { applyResponse(await faqService.uploadFavicon(file)) saveState.value = 'saved' } catch (error: any) { saveState.value = 'error' toast.error(error.message) } } async function removeFavicon(): Promise { saveState.value = 'saving' try { applyResponse(await faqService.removeFavicon()) saveState.value = 'saved' } catch (error: any) { saveState.value = 'error' toast.error(error.message) } } // Domain lifecycle (explicit Verify button — never autosaved). const domainBusy = ref(false) async function setDomain(domain: string): Promise { domainBusy.value = true try { const result = await faqService.setDomain(domain) if (settings.value) settings.value = { ...settings.value, domain: result } } catch (error: any) { toast.error(error.message) } finally { domainBusy.value = false } } async function verifyDomain(): Promise { domainBusy.value = true try { const result = await faqService.verifyDomain() if (settings.value) settings.value = { ...settings.value, domain: result } if (result.domain_status === 'verified') { toast.success('域名已验证') } else { toast.info('记录尚未生效,DNS 变更可能需要几分钟才能传播') } } catch (error: any) { toast.error(error.message) } finally { domainBusy.value = false } } async function removeDomain(): Promise { domainBusy.value = true try { const result = await faqService.removeDomain() if (settings.value) settings.value = { ...settings.value, domain: result } } catch (error: any) { toast.error(error.message) } finally { domainBusy.value = false } } return { saveState, brandColor, queueSave, saveNow, uploadLogo, removeLogo, uploadFavicon, removeFavicon, domainBusy, setDomain, verifyDomain, removeDomain, } }