141 lines
3.6 KiB
JavaScript
141 lines
3.6 KiB
JavaScript
import { ref, watch, onMounted, onBeforeUnmount, computed } from 'vue'
|
|
import { useDirtyState } from './useDirtyState'
|
|
|
|
/**
|
|
* useSectionForm — standard composable for schema-driven admin settings pages.
|
|
*
|
|
* Encapsulates: data loading, dirty tracking, save/reload logic, and loading states.
|
|
*
|
|
* Usage:
|
|
* const {
|
|
* formData, loading, saving, loadError,
|
|
* dirty, load, save, reset,
|
|
* } = useSectionForm({
|
|
* fetch: () => api.getBasicSettings(),
|
|
* submit: (data) => api.saveBasicSettings(data),
|
|
* defaults: { comment_enabled: true, lazy_load: true },
|
|
* })
|
|
*
|
|
* @param {Object} options
|
|
* @param {Function} options.fetch — async function returning current settings
|
|
* @param {Function} options.submit — async function accepting form data to save
|
|
* @param {Object} options.defaults — default values merged under fetched data
|
|
* @param {Function} [options.onLoaded] — callback after data is loaded
|
|
* @param {Function} [options.onSaved] — callback after successful save
|
|
* @param {Function} [options.transform] — transform fetched data before applying
|
|
*/
|
|
export function useSectionForm(options) {
|
|
const {
|
|
fetch: fetchData,
|
|
submit: submitData,
|
|
defaults = {},
|
|
onLoaded,
|
|
onSaved,
|
|
transform,
|
|
} = options
|
|
|
|
const formData = ref({ ...defaults })
|
|
const initialSnapshot = ref('')
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
const loadError = ref(null)
|
|
|
|
const { isDirty, markDirty, markClean, reset: resetDirty, confirmLeave } = useDirtyState()
|
|
|
|
/** Snapshot the current form state for dirty comparison */
|
|
function snapshot() {
|
|
initialSnapshot.value = JSON.stringify(formData.value)
|
|
}
|
|
|
|
/** Check if form data differs from snapshot */
|
|
function checkDirty() {
|
|
const current = JSON.stringify(formData.value)
|
|
if (current !== initialSnapshot.value) {
|
|
markDirty()
|
|
} else {
|
|
resetDirty()
|
|
}
|
|
}
|
|
|
|
/** Deep watch form data for dirty tracking */
|
|
let watcherHandle = null
|
|
|
|
function startWatching() {
|
|
if (watcherHandle) watcherHandle()
|
|
watcherHandle = watch(formData, checkDirty, { deep: true })
|
|
}
|
|
|
|
/** Load settings from API */
|
|
async function load() {
|
|
loading.value = true
|
|
loadError.value = null
|
|
try {
|
|
const data = await fetchData()
|
|
const raw = data?.data ?? data ?? {}
|
|
const merged = { ...defaults, ...(transform ? transform(raw) : raw) }
|
|
formData.value = merged
|
|
snapshot()
|
|
startWatching()
|
|
onLoaded?.(merged)
|
|
} catch (err) {
|
|
loadError.value = err?.message || '加载失败'
|
|
console.error('[useSectionForm] load error:', err)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
/** Save current form data */
|
|
async function save() {
|
|
saving.value = true
|
|
try {
|
|
await submitData(formData.value)
|
|
snapshot()
|
|
markClean()
|
|
onSaved?.(formData.value)
|
|
} catch (err) {
|
|
console.error('[useSectionForm] save error:', err)
|
|
throw err // let caller handle the error display
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
/** Reload form data from API (discards changes) */
|
|
async function reload() {
|
|
if (watcherHandle) watcherHandle()
|
|
resetDirty()
|
|
await load()
|
|
}
|
|
|
|
/** Reset form to defaults */
|
|
function resetToDefaults() {
|
|
if (watcherHandle) watcherHandle()
|
|
formData.value = { ...defaults }
|
|
snapshot()
|
|
startWatching()
|
|
resetDirty()
|
|
}
|
|
|
|
// Auto-load on mount
|
|
onMounted(load)
|
|
|
|
// Cleanup watcher
|
|
onBeforeUnmount(() => {
|
|
if (watcherHandle) watcherHandle()
|
|
})
|
|
|
|
return {
|
|
formData,
|
|
loading,
|
|
saving,
|
|
loadError,
|
|
dirty: isDirty,
|
|
load,
|
|
save,
|
|
reload,
|
|
resetToDefaults,
|
|
confirmLeave,
|
|
}
|
|
}
|