ux(ui): readable persona form instead of raw JSON + step-by-step guides for non-IT users

- GroupEdit: replace raw JSON persona dump with a labeled, sectioned PersonaForm
  (ข้อมูลพื้นฐาน / ข้อมูลลูกค้า / การขาย / ช่องทาง) shown in a modal; admin edits by field.
- New reusable components/PersonaForm.vue.
- GroupBuilder + Personas: add friendly step-by-step guidance cards ('วิธีสร้าง',
  'วิธีฝึก') so admins/trainees know what to do at each step.
- Rebuild dist.
This commit is contained in:
Macky
2026-08-08 10:24:53 +07:00
parent 55759a3a50
commit 39d1aa04e4
25 changed files with 273 additions and 83 deletions

View File

@@ -0,0 +1,129 @@
<template>
<div class="persona-form">
<h3>{{ persona.name || 'New persona' }}</h3>
<p class="muted">{{ persona.profession }} · {{ persona.personality }}</p>
<!-- Section: อมลพนฐาน -->
<div class="sec">
<h4>👤 อมลพนฐาน</h4>
<div class="row">
<div class="f"><label></label><input v-model="d.name" /></div>
<div class="f"><label>ระดบความยาก (1-5)</label><input v-model.number="d.difficulty" type="number" min="1" max="5" /></div>
<div class="f"><label>ระดบลกค</label>
<select v-model="d.tier"><option value="A">A — พร้อมตัดสินใจซื้อ</option><option value="B">B — ยังไม่แน่ใจ</option><option value="C">C ไม่สนใจแต่มีปัญหา</option></select>
</div>
</div>
<div class="row">
<div class="f"><label>อาช</label><input v-model="d.profession" /></div>
<div class="f"><label>วงอาย</label><input v-model="d.age_group" /></div>
<div class="f"><label>นท</label><input v-model="d.location" /></div>
</div>
</div>
<!-- Section: อมลลกค (การขาย) -->
<div class="sec">
<h4>💼 อมลลกค</h4>
<div class="row">
<div class="f"><label>รายได/สถานะการเง</label><input v-model="d.income" /></div>
<div class="f"><label>งบประมาณ</label><input v-model="d.budget" /></div>
<div class="f"><label>ไลฟสไตล</label><input v-model="d.lifestyle" /></div>
</div>
<label>หล</label>
<textarea v-model="d.background"></textarea>
<label>คล / ดค</label>
<textarea v-model="d.personality"></textarea>
<label>สไตลการสอสาร</label>
<textarea v-model="d.communication_style"></textarea>
</div>
<!-- Section: การขาย -->
<div class="sec">
<h4>🎯 การขาย</h4>
<label>เปาหมาย</label><textarea v-model="d.goal"></textarea>
<label>กรอบเวลาในการตดสนใจ</label><input v-model="d.decision_timeline" />
<label>Pain (ญหา) แบบ 1 อบรรท</label>
<textarea v-model="painsText" placeholder="เช่น ต้นทุนสูงเกินไป&#10;ระบบล้าสมัย"></textarea>
<label>อโตแย (Objections) 1 อบรรท</label>
<textarea v-model="objectionsText"></textarea>
<label>งทใชอรอง (เช วนลด, ฟรดต)</label>
<textarea v-model="negLeversText"></textarea>
<label>องทางสำหรบลกคาเปดบทสนทนา (opener)</label>
<textarea v-model="d.opener"></textarea>
</div>
<!-- Section: ช่องทาง/โหมด -->
<div class="sec">
<h4>📡 องทาง & โหมด</h4>
<div class="row">
<div class="f"><label>องทาง</label>
<select v-model="d.channel"><option value="line">LINE</option><option value="facebook">Facebook</option></select>
</div>
<div class="f"><label>ใครเรมคยกอน</label>
<select v-model="d.initiation_mode"><option value="customer">ลูกค้าทักก่อน</option><option value="seller">เราต้องเริ่มขาย (เชิงรุก)</option></select>
</div>
</div>
<label class="muted" v-if="d.special">พิเศษ: {{ d.special }}</label>
</div>
<div class="row actions">
<button class="primary" @click="save" :disabled="busy">{{ busy ? 'กำลังบันทึก…' : '💾 บันทึกบุคคลต้นแบบ' }}</button>
<button @click="$emit('cancel')">ปิด</button>
</div>
</div>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
const props = defineProps({ persona: { type: Object, required: true } })
const emit = defineEmits(['save', 'cancel'])
const d = reactive({
name: '', difficulty: 1, tier: 'B', profession: '', age_group: '', location: '',
income: '', budget: '', lifestyle: '', background: '', personality: '',
communication_style: '', goal: '', decision_timeline: '', opener: '', channel: 'line',
initiation_mode: 'customer', special: '',
})
const painsText = ref('')
const objectionsText = ref('')
const negLeversText = ref('')
const busy = ref(false)
function toLines(a) { return Array.isArray(a) ? a.join('\n') : String(a || '') }
function fromLines(s) { return s.split('\n').map(x => x.trim()).filter(Boolean) }
watch(() => props.persona, (p) => {
Object.assign(d, {
name: p?.name || '', difficulty: p?.difficulty ?? 1, tier: p?.tier || 'B',
profession: p?.profession || '', age_group: p?.age_group || '', location: p?.location || '',
income: p?.income || '', budget: p?.budget || '', lifestyle: p?.lifestyle || '',
background: p?.background || '', personality: p?.personality || '',
communication_style: p?.communication_style || '', goal: p?.goal || '',
decision_timeline: p?.decision_timeline || '', opener: p?.opener || '',
channel: p?.channel || 'line', initiation_mode: p?.initiation_mode || 'customer',
special: p?.special || '',
})
painsText.value = toLines(p?.pains)
objectionsText.value = toLines(p?.objections)
negLeversText.value = toLines(p?.negotiation_levers)
}, { immediate: true })
function save() {
emit('save', {
...d,
pains: fromLines(painsText.value),
objections: fromLines(objectionsText.value),
negotiation_levers: fromLines(negLeversText.value),
})
}
</script>
<style scoped>
.sec { margin-top: 20px; }
.sec h4 { margin: 0 0 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
.row { display: flex; gap: 12px; flex-wrap: wrap; }
.f { flex: 1; min-width: 140px; }
label { font-size: 13px; color: var(--muted); display: block; margin: 10px 0 4px; }
input, select, textarea { width: 100%; }
.actions { margin-top: 20px; }
</style>