ปรับ look and feel

This commit is contained in:
Suphonchai Phoonsawat 2026-08-20 21:34:45 +07:00
parent 0148f48204
commit b47667f918
15 changed files with 915 additions and 523 deletions

View file

@ -134,83 +134,99 @@ onMounted(load)
<template>
<AuthGate>
<section class="mx-auto max-w-4xl space-y-8">
<div v-if="loading" class="flex items-center gap-3 py-10 text-sm text-slate-500 dark:text-slate-400">
<span
class="h-4 w-4 animate-spin rounded-full border-2 border-slate-300 border-t-slate-600 dark:border-slate-600 dark:border-t-slate-300"
/>
<div v-if="loading" class="flex items-center gap-3 py-10 text-sm text-muted">
<span class="spinner" />
กำลังโหลด…
</div>
<AlertBox v-else-if="loadError" type="error">{{ loadError }}</AlertBox>
<template v-else-if="detail">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 class="text-2xl font-bold">{{ detail.name }}</h1>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0">
<h1 class="text-page-title">{{ detail.name }}</h1>
<p class="mt-1 text-xs text-muted">
สร้าง {{ formatDateTime(detail.createdAt) }} · อัปเดต {{ formatDateTime(detail.updatedAt) }}
</p>
</div>
<NuxtLink
to="/templates"
class="rounded-md border border-slate-300 px-3 py-1.5 text-sm hover:bg-slate-100 dark:border-slate-600 dark:hover:bg-slate-800"
>
← รายการ template
</NuxtLink>
<div class="flex shrink-0 flex-wrap items-center gap-2">
<NuxtLink to="/templates" class="btn btn-secondary btn-sm">← รายการ template</NuxtLink>
</div>
</div>
<!-- 1) metadata -->
<form class="space-y-4" @submit.prevent="save">
<h2 class="text-lg font-semibold">ข้อมูล template</h2>
<form class="max-w-xl space-y-4" @submit.prevent="save">
<h2 class="text-section-title">ข้อมูล template</h2>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label for="edit-name" class="mb-1 block text-sm font-medium">ชื่อ</label>
<input
id="edit-name"
v-model="name"
type="text"
required
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-800"
/>
<label for="edit-name" class="label">ชื่อ</label>
<input id="edit-name" v-model="name" type="text" required class="input" />
</div>
<div class="flex items-end gap-2 pb-1">
<input
id="edit-active"
v-model="isActive"
type="checkbox"
class="h-4 w-4 rounded border-slate-300 dark:border-slate-600 dark:bg-slate-800"
/>
<input id="edit-active" v-model="isActive" type="checkbox" class="h-4 w-4 accent-primary" />
<label for="edit-active" class="text-sm">
active
<span class="text-xs text-slate-500 dark:text-slate-400">(เอาออก = สร้างเอกสารใหม่ไม่ได้ แต่เอกสารเดิมยังใช้ได้)</span>
<span class="text-xs text-muted">(เอาออก = สร้างเอกสารใหม่ไม่ได้ แต่เอกสารเดิมยังใช้ได้)</span>
</label>
</div>
</div>
<div>
<label for="edit-desc" class="mb-1 block text-sm font-medium">คำอธิบาย</label>
<textarea
id="edit-desc"
v-model="description"
rows="2"
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-600 dark:bg-slate-800"
/>
<label for="edit-desc" class="label">คำอธิบาย</label>
<textarea id="edit-desc" v-model="description" rows="2" class="input" />
</div>
</form>
<!-- 2) field definitions -->
<div class="space-y-4">
<div>
<h2 class="text-lg font-semibold">Field definitions</h2>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
<h2 class="text-section-title">Field definitions</h2>
<p class="mt-1 text-xs text-muted">
<span class="font-mono">key</span> มาจาก content control tag ในไฟล์ — เพิ่ม/ลบ key ต้องแทนที่ไฟล์ด้านล่าง
</p>
</div>
<div class="overflow-x-auto rounded-lg border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-800">
<!-- มือถือ: card ต่อ field (v-model ชุดเดียวกับตาราง) -->
<ul v-if="fields.length" class="space-y-3 md:hidden">
<li v-for="f in fields" :key="f.key" class="card space-y-3 p-4">
<p class="font-mono text-xs text-muted">{{ f.key }}</p>
<div>
<label :for="`m-label-${f.key}`" class="label">label</label>
<input :id="`m-label-${f.key}`" v-model="f.label" type="text" :aria-label="`label ของ ${f.key}`" class="input" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label :for="`m-type-${f.key}`" class="label">type</label>
<select :id="`m-type-${f.key}`" v-model="f.type" :aria-label="`type ของ ${f.key}`" class="input">
<option v-for="t in FIELD_TYPES" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<div>
<label :for="`m-order-${f.key}`" class="label">ลำดับ</label>
<input
:id="`m-order-${f.key}`"
v-model.number="f.sortOrder"
type="number"
:aria-label="`ลำดับของ ${f.key}`"
class="input"
/>
</div>
</div>
<label class="flex items-center gap-2 text-sm">
<input v-model="f.isRequired" type="checkbox" :aria-label="`required ของ ${f.key}`" class="h-4 w-4 accent-primary" />
required
</label>
</li>
</ul>
<AlertBox v-else-if="fields.length === 0" type="info">
ไฟล์นี้ไม่มี content control เลย — แทนที่ไฟล์ด้วยไฟล์ที่มี content controls
</AlertBox>
<!-- desktop: ตารางแก้ค่า -->
<div v-if="fields.length" class="table-card hidden md:block">
<table class="w-full text-sm">
<thead class="border-b border-slate-200 text-left text-xs uppercase text-slate-500 dark:border-slate-700 dark:text-slate-400">
<thead class="table-head">
<tr>
<th class="px-4 py-3">key</th>
<th class="px-4 py-3">label</th>
@ -219,7 +235,7 @@ onMounted(load)
<th class="px-4 py-3">ลำดับ</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
<tbody class="divide-y divide-line">
<tr v-for="(f, i) in fields" :key="f.key">
<td class="px-4 py-2 font-mono text-xs">{{ f.key }}</td>
<td class="px-4 py-2">
@ -227,15 +243,11 @@ onMounted(load)
v-model="f.label"
type="text"
:aria-label="`label ของ ${f.key}`"
class="w-full min-w-40 rounded-md border border-slate-300 bg-white px-2 py-1 text-sm dark:border-slate-600 dark:bg-slate-800"
class="input min-w-40 px-2 py-1"
/>
</td>
<td class="px-4 py-2">
<select
v-model="f.type"
:aria-label="`type ของ ${f.key}`"
class="rounded-md border border-slate-300 bg-white px-2 py-1 text-sm dark:border-slate-600 dark:bg-slate-800"
>
<select v-model="f.type" :aria-label="`type ของ ${f.key}`" class="input px-2 py-1">
<option v-for="t in FIELD_TYPES" :key="t" :value="t">{{ t }}</option>
</select>
</td>
@ -244,7 +256,7 @@ onMounted(load)
v-model="f.isRequired"
type="checkbox"
:aria-label="`required ของ ${f.key}`"
class="h-4 w-4 rounded border-slate-300 dark:border-slate-600 dark:bg-slate-800"
class="h-4 w-4 accent-primary"
/>
</td>
<td class="px-4 py-2">
@ -252,29 +264,19 @@ onMounted(load)
v-model.number="f.sortOrder"
type="number"
:aria-label="`ลำดับของ ${f.key}`"
class="w-20 rounded-md border border-slate-300 bg-white px-2 py-1 text-sm dark:border-slate-600 dark:bg-slate-800"
class="input w-20 px-2 py-1"
/>
</td>
</tr>
<tr v-if="fields.length === 0">
<td colspan="5" class="px-4 py-6 text-center text-sm text-slate-500 dark:text-slate-400">
ไฟล์นี้ไม่มี content control เลย — แทนที่ไฟล์ด้วยไฟล์ที่มี content controls
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex flex-wrap items-center gap-3">
<button
type="button"
:disabled="saving"
class="rounded-md bg-sky-600 px-4 py-2 text-sm font-medium text-white hover:bg-sky-700 disabled:opacity-50"
@click="save"
>
<button type="button" :disabled="saving" class="btn btn-primary" @click="save">
{{ saving ? 'กำลังบันทึก…' : 'บันทึกทั้งหมด' }}
</button>
<span v-if="savedAt && !saveError" class="text-xs text-emerald-600 dark:text-emerald-400">
<span v-if="savedAt && !saveError" class="text-xs text-success-strong">
บันทึกแล้วเมื่อ {{ savedAt }}
</span>
</div>
@ -282,27 +284,27 @@ onMounted(load)
</div>
<!-- 3) replace file -->
<div class="space-y-3 border-t border-slate-200 pt-6 dark:border-slate-700">
<div class="space-y-3 border-t border-line pt-6">
<div>
<h2 class="text-lg font-semibold">แทนที่ไฟล์ template</h2>
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
<h2 class="text-section-title">แทนที่ไฟล์ template</h2>
<p class="mt-1 text-xs text-muted">
ระบบจะ detect tags ใหม่จากไฟล์ — field definition ของ tag ที่หายไปจะถูกลบ, tag ใหม่จะเพิ่มแบบ default
(เอกสารที่สร้างไปแล้วไม่กระทบ)
</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<input
ref="replaceInput"
type="file"
accept=".docx"
class="rounded-md border border-slate-300 bg-white px-3 py-2 text-sm file:mr-3 file:rounded file:border-0 file:bg-slate-100 file:px-3 file:py-1 file:text-sm dark:border-slate-600 dark:bg-slate-800 dark:file:bg-slate-700"
class="input sm:max-w-md file:mr-3 file:rounded-pill file:border-0 file:bg-surface-muted file:px-3 file:py-1 file:text-sm file:font-medium"
@change="onReplaceFileChange"
/>
<button
type="button"
:disabled="replacing || !replaceFile"
class="rounded-md border border-slate-300 px-4 py-2 text-sm hover:bg-slate-100 disabled:opacity-50 dark:border-slate-600 dark:hover:bg-slate-800"
class="btn btn-secondary w-full sm:w-auto"
@click="submitReplaceFile"
>
{{ replacing ? 'กำลังอัปโหลด…' : 'อัปโหลดไฟล์ใหม่' }}