Files
nexus/Project/fonrey/UI_DESIGN/新增客源_UI.html
2026-04-26 12:49:46 +08:00

644 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280">
<title>Fonrey 新增客源 · 静态原型</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
50: '#F0FDFA',
100: '#CCFBF1',
200: '#99F6E4',
500: '#14B8A6',
600: '#0F766E',
700: '#115E59',
800: '#134E4A'
},
neutral: {
50: '#F8FAFC',
100: '#F1F5F9',
200: '#E2E8F0',
300: '#CBD5E1',
400: '#94A3B8',
500: '#64748B',
600: '#475569',
700: '#334155',
800: '#1E293B',
900: '#0F172A'
},
success: { 50: '#F0FDF4', 600: '#16A34A' },
warning: { 50: '#FFFBEB', 600: '#D97706' },
danger: { 50: '#FEF2F2', 600: '#DC2626' },
info: { 50: '#EFF6FF', 600: '#2563EB' }
},
boxShadow: {
xs: '0 1px 2px rgba(15,23,42,0.04)'
},
fontFamily: {
sans: ['Inter', 'PingFang SC', 'Microsoft YaHei', 'sans-serif']
}
}
}
}
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
html { scroll-behavior: smooth; }
[x-cloak] { display: none !important; }
.tabular-nums { font-variant-numeric: tabular-nums; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
</style>
</head>
<body class="bg-neutral-50 text-sm text-neutral-700 antialiased" x-data="createClientPage()">
<header class="fixed top-0 left-0 right-0 h-14 z-20 bg-primary-800 flex items-center justify-between">
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
<span class="text-base font-semibold text-white">Fonrey</span>
</div>
<nav class="flex items-center gap-1 flex-1 px-2" aria-label="主导航">
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">工作台</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">房源</a>
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium">客源</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">营销</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">交易</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">数据</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">人事</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">系统</a>
</nav>
<div class="flex items-center gap-1 px-4 shrink-0">
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="消息">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
</button>
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold"></div>
<span class="text-sm font-medium text-primary-100">魏深</span>
</div>
</div>
</header>
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-neutral-200 bg-white">
<nav class="p-3 space-y-0.5">
<div class="px-2 pt-2 pb-1 text-xs font-medium text-neutral-500 uppercase tracking-wide">客源管理</div>
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md bg-primary-50 text-primary-700 font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/></svg>
私客列表
</a>
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">公客池</a>
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">成交客</a>
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">已删客源</a>
</nav>
</aside>
<main class="ml-60 pt-[72px] min-h-screen bg-neutral-50 px-6 py-5">
<div class="mx-auto max-w-5xl">
<div class="mb-6">
<nav class="flex items-center gap-1 text-xs text-neutral-500 mb-2" aria-label="面包屑">
<a href="/clients/" class="hover:text-neutral-700">客源</a>
<span>/</span>
<span class="text-neutral-700">录入私客</span>
</nav>
<h1 class="text-xl font-semibold text-neutral-800">录入私客</h1>
</div>
<form id="create-client-form" @submit.prevent="submitForm" class="space-y-4">
<section class="bg-white rounded-lg border border-neutral-200 p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-neutral-800">联系人</h2>
<span class="text-xs text-neutral-500">最多添加 5 位联系人</span>
</div>
<div class="space-y-5">
<template x-for="(contact, idx) in form.contacts" :key="contact.id">
<div class="border border-neutral-200 rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-neutral-700" x-text="'联系人' + (idx + 1)"></h3>
<button
type="button"
x-show="idx > 0"
@click="removeContact(idx)"
class="text-xs text-danger-600 hover:text-danger-600 hover:underline"
>删除</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">
姓名 <span class="text-danger-600">*</span>
</label>
<input
type="text"
x-model.trim="contact.name"
:data-field-key="fieldKey('contacts', idx, 'name')"
placeholder="请输入"
class="block w-full px-3 py-2 text-sm rounded-md border placeholder:text-neutral-400 focus:outline-none focus:ring-2"
:class="inputClass(fieldKey('contacts', idx, 'name'))"
>
<p class="text-xs text-danger-600" x-show="errors[fieldKey('contacts', idx, 'name')]" x-text="errors[fieldKey('contacts', idx, 'name')]"></p>
</div>
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">
性别 <span class="text-danger-600">*</span>
</label>
<div :data-field-key="fieldKey('contacts', idx, 'gender')" class="flex items-center gap-6 rounded-md border px-3 py-2 min-h-[42px]"
:class="groupClass(fieldKey('contacts', idx, 'gender'))">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="contact.gender" value="male" class="w-4 h-4 accent-primary-600">
<span class="text-sm text-neutral-700">先生</span>
</label>
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="contact.gender" value="female" class="w-4 h-4 accent-primary-600">
<span class="text-sm text-neutral-700">女士</span>
</label>
</div>
<p class="text-xs text-danger-600" x-show="errors[fieldKey('contacts', idx, 'gender')]" x-text="errors[fieldKey('contacts', idx, 'gender')]"></p>
</div>
<div class="md:col-span-2 space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">
电话1 <span class="text-danger-600">*</span>
</label>
<div
:data-field-key="fieldKey('contacts', idx, 'phone')"
class="flex rounded-md border overflow-hidden focus-within:ring-2"
:class="phoneWrapClass(fieldKey('contacts', idx, 'phone'))"
>
<select x-model="contact.phoneCountryCode" class="w-24 px-2 py-2 text-sm bg-neutral-50 border-r border-neutral-200 text-neutral-700 focus:outline-none">
<template x-for="code in countryCodes" :key="code">
<option :value="code" x-text="code"></option>
</template>
</select>
<input
type="tel"
x-model.trim="contact.phone"
@blur="idx === 0 ? checkDuplicatePhone() : null"
placeholder="输入手机号"
class="flex-1 px-3 py-2 text-sm bg-white focus:outline-none"
>
</div>
<p class="text-xs text-danger-600" x-show="errors[fieldKey('contacts', idx, 'phone')]" x-text="errors[fieldKey('contacts', idx, 'phone')]"></p>
<template x-if="idx === 0">
<div class="mt-1 min-h-[20px]">
<template x-if="checkingDuplicate">
<div class="animate-pulse h-4 bg-neutral-200 rounded w-56"></div>
</template>
<p
x-show="duplicateHint"
x-text="duplicateHint"
class="text-xs text-warning-600"
></p>
</div>
</template>
</div>
</div>
<div class="mt-3">
<button
type="button"
@click="contact.expanded = !contact.expanded"
class="flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700"
>
<span>电话2、微信、QQ等</span>
<svg class="w-4 h-4 transition-transform" :class="contact.expanded ? 'rotate-180' : ''" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="contact.expanded" x-transition class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">电话2</label>
<div class="flex rounded-md border border-neutral-300 overflow-hidden focus-within:border-primary-600 focus-within:ring-2 focus-within:ring-primary-600/20">
<select x-model="contact.phone2CountryCode" class="w-24 px-2 py-2 text-sm bg-neutral-50 border-r border-neutral-200 text-neutral-700 focus:outline-none">
<template x-for="code in countryCodes" :key="'p2-' + code">
<option :value="code" x-text="code"></option>
</template>
</select>
<input type="tel" x-model.trim="contact.phone2" placeholder="输入手机号" class="flex-1 px-3 py-2 text-sm bg-white focus:outline-none">
</div>
</div>
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">微信</label>
<input type="text" x-model.trim="contact.wechat" placeholder="请输入" class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
</div>
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">QQ</label>
<input type="text" x-model.trim="contact.qq" placeholder="请输入" class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
</div>
</div>
</div>
</div>
</template>
<button
type="button"
x-show="form.contacts.length < 5"
@click="addContact"
class="flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
新增联系人
</button>
</div>
</section>
<section class="bg-white rounded-lg border border-neutral-200 p-6">
<h2 class="text-base font-semibold text-neutral-800 mb-4">基础信息</h2>
<div class="space-y-4">
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">状态 <span class="text-danger-600">*</span></label>
<div data-field-key="status" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('status')">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="form.status" value="buying" class="w-4 h-4 accent-primary-600">
<span class="text-sm text-neutral-700">求购</span>
</label>
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="form.status" value="renting" class="w-4 h-4 accent-primary-600">
<span class="text-sm text-neutral-700">求租</span>
</label>
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="form.status" value="buy_or_rent" class="w-4 h-4 accent-primary-600">
<span class="text-sm text-neutral-700">租购</span>
</label>
</div>
<p class="text-xs text-danger-600" x-show="errors.status" x-text="errors.status"></p>
</div>
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">用途 <span class="text-danger-600">*</span></label>
<div data-field-key="property_usage" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('property_usage')">
<template x-for="item in usageOptions" :key="item.value">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="form.propertyUsage" :value="item.value" class="w-4 h-4 accent-primary-600">
<span class="text-sm text-neutral-700" x-text="item.label"></span>
</label>
</template>
</div>
<p class="text-xs text-danger-600" x-show="errors.property_usage" x-text="errors.property_usage"></p>
</div>
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">等级 <span class="text-danger-600">*</span></label>
<div data-field-key="grade" class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]" :class="groupClass('grade')">
<template x-for="item in gradeOptions" :key="item.value">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="form.grade" :value="item.value" class="w-4 h-4 accent-primary-600">
<span class="text-sm text-neutral-700" x-text="item.label"></span>
</label>
</template>
</div>
<p class="text-xs text-danger-600" x-show="errors.grade" x-text="errors.grade"></p>
</div>
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">来源 <span class="text-danger-600">*</span></label>
<select
data-field-key="source"
x-model="form.source"
:disabled="sourceLoading"
class="block w-full max-w-xs px-3 py-2 text-sm rounded-md border bg-white focus:outline-none focus:ring-2 disabled:bg-neutral-100 disabled:text-neutral-400"
:class="inputClass('source')"
>
<option value="" x-text="sourceLoading ? '加载中...' : '请选择'"></option>
<template x-for="item in sourceOptions" :key="item.value">
<option :value="item.value" x-text="item.label"></option>
</template>
</select>
<p class="text-xs text-danger-600" x-show="errors.source" x-text="errors.source"></p>
</div>
<div>
<button type="button" @click="infoExpanded = !infoExpanded" class="flex items-center gap-1 text-sm text-neutral-500 hover:text-neutral-700">
<span>证件类型、证件号码、意向学校等</span>
<svg class="w-4 h-4 transition-transform" :class="infoExpanded ? 'rotate-180' : ''" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m6 9 6 6 6-6"/></svg>
</button>
<div x-show="infoExpanded" x-transition class="mt-3 space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">证件类型</label>
<select x-model="form.idType" class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 bg-white focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
<option value="">请选择</option>
<option value="id_card">身份证</option>
<option value="passport">护照</option>
<option value="hk_macao">港澳通行证</option>
<option value="other">其他</option>
</select>
</div>
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">证件号码</label>
<input type="text" x-model.trim="form.idNumber" placeholder="请输入证件号码" class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
</div>
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-neutral-700">意向学校</label>
<template x-for="(school, sIdx) in form.schools" :key="'school-' + sIdx">
<div class="flex items-center gap-2 max-w-md">
<input type="text" x-model.trim="form.schools[sIdx]" placeholder="请输入学校名称" class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20">
<button type="button" @click="removeSchool(sIdx)" x-show="form.schools.length > 1" class="px-2 py-2 text-xs text-danger-600 hover:bg-danger-50 rounded-md">删除</button>
</div>
</template>
<button type="button" @click="addSchool" class="flex items-center gap-1.5 text-sm text-primary-600 hover:text-primary-700">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
添加学校
</button>
</div>
</div>
</div>
</div>
</section>
<section class="bg-white rounded-lg border border-neutral-200 p-6">
<h2 class="text-base font-semibold text-neutral-800 mb-4">相关员工</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">首录人</label>
<input type="text" value="魏深 - 都市港湾店一组" disabled class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-200 bg-neutral-100 text-neutral-500 cursor-not-allowed">
<input type="hidden" name="first_recorder_id" x-model="form.firstRecorderId">
</div>
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">归属人</label>
<input type="text" value="魏深 - 都市港湾店一组" disabled class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-200 bg-neutral-100 text-neutral-500 cursor-not-allowed">
<input type="hidden" name="owner_id" x-model="form.ownerId">
</div>
</div>
</section>
<div class="flex items-center gap-3 mt-2 pb-8">
<button
type="submit"
:disabled="submitting"
class="inline-flex items-center gap-1.5 px-6 py-2 text-sm font-medium bg-primary-600 text-white rounded-md hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-70 disabled:cursor-wait"
>
<svg x-show="submitting" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-opacity=".25" stroke-width="4"></circle>
<path d="M22 12a10 10 0 0 1-10 10" stroke="currentColor" stroke-width="4"></path>
</svg>
<span x-text="submitting ? '保存中...' : '确定'"></span>
</button>
<button
type="button"
@click="cancelForm"
:disabled="submitting"
class="inline-flex items-center px-4 py-2 text-sm font-medium bg-white border border-neutral-300 text-neutral-700 rounded-md hover:bg-neutral-50 hover:border-neutral-400 disabled:opacity-60"
>
取消
</button>
<p x-show="redirectHint" x-text="redirectHint" class="text-xs text-success-600"></p>
</div>
</form>
</div>
</main>
<div x-show="toast.show" x-transition.opacity class="fixed bottom-6 right-6 z-[70]">
<div class="w-80 bg-white rounded-lg shadow-lg border border-neutral-200 flex items-start gap-3 p-3">
<svg class="w-5 h-5 text-success-600 shrink-0 mt-0.5" fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
<div class="flex-1 text-sm">
<p class="font-medium text-neutral-800" x-text="toast.message"></p>
<p class="text-xs text-neutral-500">客源信息已保存</p>
</div>
</div>
</div>
<script>
function createClientPage() {
return {
countryCodes: ['+86', '+852', '+853', '+886', '+1'],
usageOptions: [
{ value: 'residential', label: '住宅' },
{ value: 'villa', label: '别墅' },
{ value: 'commercial_residential', label: '商住' },
{ value: 'shop', label: '商铺' },
{ value: 'office', label: '写字楼' },
{ value: 'other', label: '其他' }
],
gradeOptions: [
{ value: 'A_urgent', label: 'A(急迫)' },
{ value: 'A', label: 'A' },
{ value: 'B', label: 'B(较强)' },
{ value: 'C', label: 'C(一般)' },
{ value: 'D', label: 'D(较弱)' },
{ value: 'E', label: 'E(暂不关注)' }
],
sourceLoading: true,
sourceOptions: [],
infoExpanded: false,
checkingDuplicate: false,
duplicateHint: '',
submitting: false,
redirectHint: '',
errors: {},
toast: {
show: false,
message: ''
},
form: {
contacts: [],
status: '',
propertyUsage: 'residential',
grade: '',
source: '',
idType: '',
idNumber: '',
schools: [''],
firstRecorderId: 'staff-1001',
ownerId: 'staff-1001'
},
init() {
this.form.contacts = [this.newContact()];
this.simulateSourceLoading();
},
newContact() {
return {
id: 'c-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
name: '',
gender: '',
phoneCountryCode: '+86',
phone: '',
expanded: false,
phone2CountryCode: '+86',
phone2: '',
wechat: '',
qq: ''
};
},
fieldKey(group, idx, field) {
return group + '_' + idx + '_' + field;
},
inputClass(key) {
return this.errors[key]
? 'border-danger-600 ring-danger-600/20 focus:border-danger-600 focus:ring-danger-600/20'
: 'border-neutral-300 focus:border-primary-600 focus:ring-primary-600/20';
},
groupClass(key) {
return this.errors[key]
? 'border-danger-600 ring-2 ring-danger-600/20'
: 'border-neutral-300';
},
phoneWrapClass(key) {
return this.errors[key]
? 'border-danger-600 ring-danger-600/20 focus-within:border-danger-600 focus-within:ring-danger-600/20'
: 'border-neutral-300 focus-within:border-primary-600 focus-within:ring-primary-600/20';
},
addContact() {
if (this.form.contacts.length >= 5) return;
this.form.contacts.push(this.newContact());
},
removeContact(idx) {
if (idx === 0) return;
this.form.contacts.splice(idx, 1);
},
addSchool() {
this.form.schools.push('');
},
removeSchool(idx) {
if (this.form.schools.length <= 1) return;
this.form.schools.splice(idx, 1);
},
simulateSourceLoading() {
setTimeout(() => {
this.sourceOptions = [
{ value: 'store_visit', label: '线下丨门店接待' },
{ value: 'old_client_referral', label: '老客户转介绍' },
{ value: 'online_form', label: '线上丨留资表单' },
{ value: 'community_push', label: '社群活动' }
];
this.sourceLoading = false;
}, 900);
},
validatePhone(phone, countryCode) {
if (!phone) return false;
if (countryCode === '+86') return /^1\d{10}$/.test(phone);
return /^[0-9]{5,20}$/.test(phone);
},
validateForm() {
this.errors = {};
let firstErrorKey = '';
this.form.contacts.forEach((contact, idx) => {
const nameKey = this.fieldKey('contacts', idx, 'name');
const genderKey = this.fieldKey('contacts', idx, 'gender');
const phoneKey = this.fieldKey('contacts', idx, 'phone');
if (!contact.name) {
this.errors[nameKey] = '姓名不能为空';
if (!firstErrorKey) firstErrorKey = nameKey;
}
if (!contact.gender) {
this.errors[genderKey] = '请选择性别';
if (!firstErrorKey) firstErrorKey = genderKey;
}
if (!contact.phone) {
this.errors[phoneKey] = '请输入手机号';
if (!firstErrorKey) firstErrorKey = phoneKey;
} else if (!this.validatePhone(contact.phone, contact.phoneCountryCode)) {
this.errors[phoneKey] = contact.phoneCountryCode === '+86' ? '请输入有效的手机号码' : '请输入有效的电话号码';
if (!firstErrorKey) firstErrorKey = phoneKey;
}
});
if (!this.form.status) {
this.errors.status = '请选择客源状态';
if (!firstErrorKey) firstErrorKey = 'status';
}
if (!this.form.propertyUsage) {
this.errors.property_usage = '请选择用途';
if (!firstErrorKey) firstErrorKey = 'property_usage';
}
if (!this.form.grade) {
this.errors.grade = '请选择客源等级';
if (!firstErrorKey) firstErrorKey = 'grade';
}
if (!this.form.source) {
this.errors.source = '请选择客户来源';
if (!firstErrorKey) firstErrorKey = 'source';
}
if (firstErrorKey) {
this.$nextTick(() => {
const target = document.querySelector('[data-field-key="' + firstErrorKey + '"]');
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
if (typeof target.focus === 'function') target.focus({ preventScroll: true });
}
});
return false;
}
return true;
},
checkDuplicatePhone() {
const c = this.form.contacts[0];
this.duplicateHint = '';
if (!c.phone || !this.validatePhone(c.phone, c.phoneCountryCode)) return;
this.checkingDuplicate = true;
setTimeout(() => {
const duplicatedPhones = ['13800138000', '13911112222', '13700009999'];
if (c.phoneCountryCode === '+86' && duplicatedPhones.includes(c.phone)) {
this.duplicateHint = '存在重复客源:张三(可前往详情查看),可继续提交但建议先核对。';
}
this.checkingDuplicate = false;
}, 600);
},
showToast(message) {
this.toast.message = message;
this.toast.show = true;
setTimeout(() => {
this.toast.show = false;
}, 2200);
},
submitForm() {
if (this.submitting) return;
if (!this.validateForm()) return;
this.submitting = true;
this.redirectHint = '';
setTimeout(() => {
this.submitting = false;
this.showToast('保存成功');
this.redirectHint = '模拟跳转中:即将进入客源详情页 /clients/CL20260426001/';
}, 1200);
},
cancelForm() {
this.redirectHint = '已取消录入,模拟返回客源列表 /clients/';
}
};
}
</script>
</body>
</html>