feat(new-frontend): add theme and templates

This commit is contained in:
2026-05-04 15:54:29 +08:00
parent a6a395b743
commit 3b362500f0
17 changed files with 1585 additions and 92 deletions
@@ -1,8 +1,14 @@
<script setup lang="ts">
import { Eye, Plus, Save, Trash2 } from 'lucide-vue-next'
import { onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { templateApi, type Template, type TemplatePreview } from '@/api'
import StateBlock from '@/components/StateBlock.vue'
import TemplateConfigEditor from '@/components/templates/TemplateConfigEditor.vue'
import {
buildTemplatePreviewPayload,
parseTemplateFieldConfig,
validateFieldConfig,
} from '@/components/templates/template-config'
import {
alertClass,
buttonBase,
@@ -10,7 +16,6 @@ import {
cardClass,
inputClass,
sectionHeaderClass,
textareaClass,
toneClass,
} from '@/components/ui'
import { extractErrorMessage, formatDateTime, stringifyJson } from '@/utils/format'
@@ -21,6 +26,7 @@ const message = ref('')
const templates = ref<Template[]>([])
const editingId = ref<number | 'new' | null>(null)
const preview = ref<TemplatePreview | null>(null)
const editorValid = ref(true)
const form = reactive({
name: '',
description: '',
@@ -29,6 +35,12 @@ const form = reactive({
is_active: true,
})
const localPreviewPayload = computed(() => {
const result = parseTemplateFieldConfig(form.field_config)
if (!result.ok) return null
return buildTemplatePreviewPayload(result.config)
})
async function load() {
loading.value = true
error.value = ''
@@ -68,7 +80,10 @@ async function save() {
error.value = ''
message.value = ''
try {
JSON.parse(form.field_config)
const parsed = parseTemplateFieldConfig(form.field_config)
if (!parsed.ok) throw new Error(parsed.message || '字段配置无效')
const validation = validateFieldConfig(parsed.config)
if (!validation.ok) throw new Error(validation.message || '字段配置无效')
const payload = {
name: form.name,
description: form.description || null,
@@ -185,27 +200,28 @@ onMounted(load)
:class="[cardClass, 'grid gap-4 overflow-hidden']"
@submit.prevent="save"
>
<div class="border-b border-zinc-200 bg-zinc-50/70 px-5 py-4">
<div
class="border-b border-zinc-200 bg-zinc-50/70 px-5 py-4 dark:border-zinc-800 dark:bg-zinc-950/50"
>
<h2 class="font-semibold">{{ editingId === 'new' ? '新建模板' : '编辑模板' }}</h2>
<p class="mt-1 text-sm text-zinc-500">字段配置必须是合法 JSON</p>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
使用结构化字段编辑器维护配置必要时切换 JSON 修复
</p>
</div>
<div class="grid gap-4 p-5">
<label class="grid gap-2">
<span class="text-xs font-semibold text-zinc-500">名称</span>
<span class="text-xs font-semibold text-zinc-500 dark:text-zinc-400">名称</span>
<input v-model="form.name" :class="inputClass" required />
</label>
<label class="grid gap-2">
<span class="text-xs font-semibold text-zinc-500">描述</span>
<span class="text-xs font-semibold text-zinc-500 dark:text-zinc-400">描述</span>
<input v-model="form.description" :class="inputClass" />
</label>
<label class="flex items-center gap-2 text-sm">
<input v-model="form.is_active" type="checkbox" />
启用模板
</label>
<label class="grid gap-2">
<span class="text-xs font-semibold text-zinc-500">字段配置 JSON</span>
<textarea v-model="form.field_config" :class="textareaClass" class="min-h-64" />
</label>
<TemplateConfigEditor v-model="form.field_config" @valid="editorValid = $event" />
<div v-if="error" :class="alertClass.danger">
{{ error }}
</div>
@@ -213,7 +229,11 @@ onMounted(load)
{{ message }}
</div>
<div class="flex gap-2">
<button :class="[buttonBase, buttonTone.primary]" type="submit">
<button
:class="[buttonBase, buttonTone.primary]"
:disabled="!editorValid"
type="submit"
>
<Save class="size-4" />
保存
</button>
@@ -235,6 +255,14 @@ onMounted(load)
>{{ stringifyJson(preview.preview_payload) }}</pre
>
</section>
<section v-else-if="editingId && localPreviewPayload" :class="[cardClass, 'p-5']">
<h2 class="font-semibold">当前配置预览</h2>
<pre
class="mt-3 max-h-96 overflow-auto rounded-md bg-zinc-950 p-3 text-xs leading-5 text-zinc-100"
>{{ stringifyJson(localPreviewPayload) }}</pre
>
</section>
</aside>
</div>
</template>