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:
129
frontend/src/components/PersonaForm.vue
Normal file
129
frontend/src/components/PersonaForm.vue
Normal 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="เช่น ต้นทุนสูงเกินไป ระบบล้าสมัย"></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>
|
||||
Reference in New Issue
Block a user