644 lines
32 KiB
HTML
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>
|