Files
nl-blogs/blog-wot-uniapp/src/subPackages/admin/pages/resource/form.vue

252 lines
8.3 KiB
Vue
Raw Normal View History

2026-07-30 14:14:28 +08:00
<script setup lang="ts">
/**
* 通用资源表单 resourceSchemas.fields 渲染支持 media/switch/select
* JSON 模式仅作高级兜底复杂资源应走专用页
*/
import { computed, reactive, ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { useToast } from '@wot-ui/ui'
import AppNavbar from '@/components/layout/AppNavbar.vue'
import AppPageShell from '@/components/layout/AppPageShell.vue'
import AdminMediaPicker from '../../components/AdminMediaPicker.vue'
import AdminFormField from '../../components/AdminFormField.vue'
import { useAuth } from '@/composables/useAuth'
import { useAppMode } from '@/composables/useAppMode'
import { adminCreate, adminGet, adminUpdate } from '@/api'
import { getResourceSchema, type SchemaField } from '../../config/resourceSchemas'
const toast = useToast()
const { isAuthenticated } = useAuth()
const { canEnterAdmin } = useAppMode()
const resource = ref('')
const mode = ref<'create' | 'edit'>('create')
const id = ref('')
const loading = ref(false)
const submitting = ref(false)
const jsonMode = ref(false)
const form = reactive<Record<string, unknown>>({})
const rawJson = ref('{}')
const schema = computed(() => getResourceSchema(resource.value))
const fields = computed(() => schema.value.fields.filter(f => !f.editOnly || mode.value === 'edit'))
const pageTitle = computed(() => {
const label = schema.value.title || resource.value
return mode.value === 'create' ? `新建${label}` : `编辑${label}`
})
function guardAdmin() {
if (!isAuthenticated() || !canEnterAdmin.value) {
uni.reLaunch({ url: '/pages/mine/index' })
return false
}
return true
}
function initEmpty() {
Object.keys(form).forEach(k => delete form[k])
fields.value.forEach((f) => {
if (f.type === 'switch') form[f.key] = 1
else if (f.type === 'number' || f.type === 'rating') form[f.key] = f.type === 'rating' ? 5 : 0
else form[f.key] = ''
})
rawJson.value = '{}'
}
function fillFromRecord(data: Record<string, unknown>) {
Object.keys(form).forEach(k => delete form[k])
fields.value.forEach((f) => {
const v = data[f.key]
if (f.type === 'switch') form[f.key] = v === true || v === 1 || v === '1' ? 1 : 0
else if (f.type === 'number' || f.type === 'rating') form[f.key] = Number(v ?? 0)
else if (f.key === 'config' && typeof v === 'object') form[f.key] = JSON.stringify(v, null, 2)
else form[f.key] = v ?? ''
})
// 保留未声明字段以便 JSON 模式完整编辑
rawJson.value = JSON.stringify(data, null, 2)
}
async function load() {
if (!resource.value || !guardAdmin()) return
loading.value = true
try {
if (mode.value === 'edit' && id.value) {
const data = await adminGet(resource.value, id.value) as Record<string, unknown>
fillFromRecord(data)
}
else {
initEmpty()
}
}
catch (e: unknown) {
toast.error(e instanceof Error ? e.message : '加载失败')
}
finally {
loading.value = false
}
}
onLoad((q) => {
resource.value = String(q?.resource || '')
mode.value = (q?.mode === 'edit' ? 'edit' : 'create') as 'create' | 'edit'
id.value = String(q?.id || '')
// 无字段的资源强制 JSON
if (!getResourceSchema(resource.value).fields.length) jsonMode.value = true
})
onShow(load)
function buildPayload(): Record<string, unknown> {
if (jsonMode.value) {
try {
return JSON.parse(rawJson.value || '{}') as Record<string, unknown>
}
catch {
throw new Error('JSON 格式不正确')
}
}
const payload: Record<string, unknown> = {}
fields.value.forEach((f: SchemaField) => {
let v = form[f.key]
if (f.type === 'number' || f.type === 'rating') v = Number(v ?? 0)
if (f.type === 'switch') v = v ? 1 : 0
if (f.key === 'config' && typeof v === 'string') {
try { v = JSON.parse(v) }
catch { /* 保持字符串,后端可自行处理 */ }
}
payload[f.key] = v
})
return payload
}
const onSubmit = async () => {
submitting.value = true
try {
const payload = buildPayload()
if (mode.value === 'edit' && id.value) await adminUpdate(resource.value, id.value, payload)
else await adminCreate(resource.value, payload)
toast.success('已保存')
setTimeout(() => uni.navigateBack(), 400)
}
catch (e: unknown) {
toast.error(e instanceof Error ? e.message : '保存失败')
}
finally {
submitting.value = false
}
}
function selectLabel(f: SchemaField) {
const opts = f.options || []
const hit = opts.find(o => String(o.value) === String(form[f.key]))
return hit?.label || '请选择'
}
</script>
<template>
<AppPageShell admin>
<AppNavbar :title="pageTitle" show-back />
<view class="page-pad">
<view class="admin-card form-card">
<template v-if="!jsonMode && fields.length">
<template v-for="f in fields" :key="f.key">
<AdminFormField v-if="f.type === 'text' || f.type === 'number'" :label="f.label">
<wd-input
:model-value="String(form[f.key] ?? '')"
:placeholder="f.placeholder || f.label"
:disabled="f.readonly"
clearable
@update:model-value="(v: string | number) => form[f.key] = v"
/>
</AdminFormField>
<AdminFormField v-else-if="f.type === 'textarea'" :label="f.label">
<wd-textarea
:model-value="String(form[f.key] ?? '')"
:placeholder="f.placeholder || f.label"
@update:model-value="(v: string) => form[f.key] = v"
/>
</AdminFormField>
<AdminMediaPicker
v-else-if="f.type === 'media'"
:model-value="String(form[f.key] ?? '')"
:label="f.label"
@update:model-value="(v: string) => form[f.key] = v"
/>
<view v-else-if="f.type === 'switch'" class="switch-row">
<text>{{ f.label }}</text>
<wd-switch
:model-value="!!form[f.key]"
@change="(v: boolean) => form[f.key] = v ? 1 : 0"
/>
</view>
<view v-else-if="f.type === 'rating'" class="rating-row">
<text class="label">{{ f.label }}</text>
<view class="stars">
<text
v-for="n in 5"
:key="n"
class="star"
:class="{ on: Number(form[f.key]) >= n }"
@click="form[f.key] = n"
>
</text>
</view>
</view>
<wd-picker
v-else-if="f.type === 'select' && f.options"
:columns="[f.options.map(o => ({ label: o.label, value: o.value }))]"
@confirm="({ value }: { value: unknown[] }) => { form[f.key] = (value?.[0] as { value?: unknown })?.value ?? value?.[0] }"
>
<view class="picker-row">
<text class="label">{{ f.label }}</text>
<text class="val">{{ selectLabel(f) }}</text>
</view>
</wd-picker>
</template>
</template>
<template v-else>
<AdminFormField label="JSON">
<wd-textarea v-model="rawJson" placeholder="{}" />
</AdminFormField>
</template>
<view class="actions">
<wd-button size="small" plain @click="jsonMode = !jsonMode">
{{ jsonMode ? '表单模式' : 'JSON 模式' }}
</wd-button>
</view>
<wd-button
block
type="primary"
:loading="submitting"
custom-style="margin-top:24rpx;background:#d4b383;border-color:#d4b383;"
@click="onSubmit"
>
保存
</wd-button>
</view>
<wd-loading v-if="loading" />
</view>
</AppPageShell>
</template>
<style scoped lang="scss">
.page-pad { padding: 24rpx 32rpx 80rpx; }
.actions { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 16rpx; }
.switch-row, .picker-row, .rating-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
color: rgb(var(--art-text));
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.label { font-size: 28rpx; }
.val { font-size: 26rpx; color: rgba(255, 255, 255, 0.65); }
.stars { display: flex; gap: 8rpx; }
.star { font-size: 36rpx; color: rgba(255, 255, 255, 0.25); }
.star.on { color: rgb(var(--art-accent)); }
</style>