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

1083 lines
56 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="editClientPage()">
<!-- ===== 顶部导航栏 ===== -->
<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 cursor-pointer">工作台</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white cursor-pointer">房源</a>
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium cursor-pointer">客源</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white cursor-pointer">营销</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white cursor-pointer">交易</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white cursor-pointer">数据</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white cursor-pointer">人事</a>
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white cursor-pointer">系统</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 cursor-pointer">
<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 cursor-pointer">公客池</a>
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100 cursor-pointer">成交客</a>
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100 cursor-pointer">已删客源</a>
</nav>
</aside>
<!-- ===== 主内容区 ===== -->
<main class="ml-60 pt-14 min-h-screen bg-neutral-50">
<!-- 面包屑 + 标题区 -->
<div class="px-6 pt-6 pb-4">
<nav class="flex items-center gap-1 text-xs text-neutral-500 mb-2" aria-label="面包屑">
<a class="hover:text-neutral-700 cursor-pointer">客源</a>
<span>/</span>
<a class="hover:text-neutral-700 cursor-pointer">客源管理</a>
<span>/</span>
<span class="text-neutral-700">编辑客源</span>
</nav>
<h1 class="text-xl font-semibold text-neutral-800">编辑客源</h1>
</div>
<!-- Tab 导航栏(粘性,紧贴顶部 header 下方) -->
<div class="sticky top-14 z-10 bg-white border-b border-neutral-200">
<nav class="flex gap-0 px-6">
<button
class="px-4 py-3 text-sm font-medium border-b-2 transition-colors focus:outline-none"
:class="activeTab === 'contacts' ? 'border-orange-500 text-orange-500' : 'border-transparent text-neutral-500 hover:text-neutral-700'"
@click="setTab('contacts')"
>联系人</button>
<button
class="px-4 py-3 text-sm font-medium border-b-2 transition-colors focus:outline-none"
:class="activeTab === 'basic' ? 'border-orange-500 text-orange-500' : 'border-transparent text-neutral-500 hover:text-neutral-700'"
@click="setTab('basic')"
>基础信息</button>
<button
class="px-4 py-3 text-sm font-medium border-b-2 transition-colors focus:outline-none"
:class="activeTab === 'requirement' ? 'border-orange-500 text-orange-500' : 'border-transparent text-neutral-500 hover:text-neutral-700'"
@click="setTab('requirement')"
>二手</button>
</nav>
</div>
<!-- 表单主体 -->
<form id="form-edit-client" @submit.prevent="submitForm" class="px-6">
<div class="mx-auto max-w-5xl py-5 space-y-4">
<!-- ===== 联系人 Section ===== -->
<section id="section-contacts" 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>
<div class="flex items-center gap-3">
<span class="text-sm text-neutral-400">查看后可编辑号码</span>
<button
type="button"
x-show="!phoneRevealed"
@click="revealPhone"
class="text-sm text-info-600 hover:underline focus:outline-none"
:class="revealLoading ? 'opacity-60 cursor-wait' : ''"
:disabled="revealLoading"
>
<span x-show="revealLoading" class="inline-flex items-center gap-1">
<svg class="w-3.5 h-3.5 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>
<span x-show="!revealLoading">查看号码</span>
</button>
<span x-show="phoneRevealed" class="text-sm text-neutral-400">已查看</span>
<button
type="button"
x-show="form.contacts.length < 5"
@click="addContact"
class="inline-flex items-center gap-1 text-sm font-medium border border-neutral-300 bg-white text-neutral-700 rounded-md px-3 py-1.5 hover:bg-neutral-50 hover:border-neutral-400"
>
<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 class="space-y-4">
<template x-for="(contact, idx) in form.contacts" :key="contact.id">
<div class="border border-neutral-200 rounded-lg p-5">
<!-- 联系人卡片标题行 -->
<div class="flex items-center justify-between mb-4">
<span class="text-sm font-medium text-neutral-700" x-text="'联系人 ' + (idx + 1)"></span>
<button
type="button"
x-show="idx > 0"
@click="removeContact(idx)"
class="text-sm text-danger-600 hover:underline focus:outline-none"
>删除</button>
</div>
<!-- 字段网格3列 -->
<div class="grid grid-cols-3 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="fk('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="inputCls(fk('contacts', idx, 'name'))"
>
<p class="text-xs text-danger-600" x-show="errors[fk('contacts', idx, 'name')]" x-text="errors[fk('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 class="flex items-center gap-6 rounded-md border px-3 py-2 min-h-[42px]"
:class="groupCls(fk('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[fk('contacts', idx, 'gender')]" x-text="errors[fk('contacts', idx, 'gender')]"></p>
</div>
<!-- 电话1打码/已解码) -->
<div class="space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">
电话1 <span class="text-danger-600">*</span>
</label>
<!-- 打码态 -->
<template x-if="!phoneRevealed && idx === 0">
<div class="flex items-center gap-2">
<div class="flex-1 flex items-center px-3 py-2 rounded-md border border-neutral-200 bg-neutral-50 min-h-[42px]">
<span class="text-sm text-neutral-500 select-none" x-text="contact.phoneMasked"></span>
</div>
<button type="button"
@click="revealPhone"
class="shrink-0 text-sm text-info-600 hover:underline focus:outline-none whitespace-nowrap">
标记无效
</button>
</div>
</template>
<!-- 可编辑态(已解码,或非第一联系人) -->
<template x-if="phoneRevealed || idx > 0">
<div>
<div
class="flex rounded-md border overflow-hidden focus-within:ring-2"
:class="phoneCls(fk('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="'cc-' + code">
<option :value="code" x-text="code"></option>
</template>
</select>
<input
type="tel"
x-model.trim="contact.phone"
placeholder="输入手机号"
class="flex-1 px-3 py-2 text-sm bg-white focus:outline-none"
>
</div>
<div class="flex items-center justify-between mt-1">
<p class="text-xs text-danger-600" x-show="errors[fk('contacts', idx, 'phone')]" x-text="errors[fk('contacts', idx, 'phone')]"></p>
<button type="button" class="ml-auto text-xs text-info-600 hover:underline focus:outline-none">标记无效</button>
</div>
</div>
</template>
</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>
<!-- 电话2 -->
<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="'cc2-' + 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>
<!-- QQ -->
<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 class="col-span-3 space-y-1.5">
<label class="block text-sm font-medium text-neutral-700">备注</label>
<div x-data="{ cnt: contact.remarks ? contact.remarks.length : 0 }">
<textarea
x-model="contact.remarks"
@input="cnt = $event.target.value.length"
maxlength="200"
rows="2"
placeholder="请输入备注信息"
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 resize-none focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
></textarea>
<p class="text-xs text-neutral-400 text-right mt-0.5" x-text="cnt + ' / 200'"></p>
</div>
</div>
</div>
</div>
</template>
</div>
</section>
<!-- ===== 基础信息 Section ===== -->
<section id="section-basic" class="bg-white rounded-lg border border-neutral-200 p-6">
<h2 class="text-base font-semibold text-neutral-800 mb-5">基础信息</h2>
<div class="space-y-5">
<!-- 需求类型(复选框) -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">
需求类型 <span class="text-danger-600">*</span>
</div>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]"
:class="groupCls('requirement_types')">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.requirementTypes" value="second_hand"
class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700">二手</span>
</label>
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.requirementTypes" value="new_house"
class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700">新房</span>
</label>
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.requirementTypes" value="rental"
class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700">租房</span>
</label>
</div>
<p class="text-xs text-danger-600 mt-1" x-show="errors.requirement_types" x-text="errors.requirement_types"></p>
</div>
</div>
<!-- 用途 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">
用途 <span class="text-danger-600">*</span>
</div>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]"
:class="groupCls('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 mt-1" x-show="errors.property_usage" x-text="errors.property_usage"></p>
</div>
</div>
<!-- 等级 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">
等级 <span class="text-danger-600">*</span>
</div>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]"
:class="groupCls('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 mt-1" x-show="errors.grade" x-text="errors.grade"></p>
</div>
</div>
<!-- 来源 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">
来源 <span class="text-danger-600">*</span>
</div>
<div class="flex-1">
<select
data-field-key="source"
x-model="form.source"
class="block w-full max-w-xs px-3 py-2 text-sm rounded-md border bg-white focus:outline-none focus:ring-2"
:class="inputCls('source')"
>
<option value="">请选择</option>
<option value="store_visit">线下丨门店接待</option>
<option value="old_client_referral">老客户转介绍</option>
<option value="online_form">线上丨留资表单</option>
<option value="community_push">社群活动</option>
</select>
<p class="text-xs text-danger-600 mt-1" x-show="errors.source" x-text="errors.source"></p>
</div>
</div>
<!-- 购房目的(多选) -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">购房目的</div>
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border border-neutral-300 px-3 py-2 min-h-[42px] flex-1">
<template x-for="item in buyingPurposeOptions" :key="item.value">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.buyingPurpose" :value="item.value"
class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700" x-text="item.label"></span>
</label>
</template>
</div>
</div>
<!-- 付款方式 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">付款方式</div>
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border border-neutral-300 px-3 py-2 min-h-[42px] flex-1">
<template x-for="item in paymentOptions" :key="item.value">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="form.paymentMethod" :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>
</div>
<!-- 名下房产 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">名下房产</div>
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border border-neutral-300 px-3 py-2 min-h-[42px] flex-1">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="form.propertiesOwned" value="none" 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.propertiesOwned" value="local_none" 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.propertiesOwned" value="local_has" class="w-4 h-4 accent-primary-600">
<span class="text-sm text-neutral-700">本地有房</span>
</label>
</div>
</div>
<!-- 贷款记录 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">贷款记录</div>
<div class="flex items-center gap-6 rounded-md border border-neutral-300 px-3 py-2 min-h-[42px] flex-1">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="radio" x-model="form.hasLoanRecord" value="true" 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.hasLoanRecord" value="false" class="w-4 h-4 accent-primary-600">
<span class="text-sm text-neutral-700"></span>
</label>
</div>
</div>
<!-- 证件类型 + 证件号码 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">证件类型</div>
<div class="flex gap-3 flex-1">
<select x-model="form.idType"
class="w-36 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 class="flex-1">
<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>
<!-- 意向学校 -->
<div class="flex gap-0"
x-data="{ schools: form.schools, addSchool() { this.schools.push(''); form.schools = this.schools; }, removeSchool(i) { this.schools.splice(i, 1); form.schools = this.schools; } }">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">意向学校</div>
<div class="flex-1 space-y-2">
<template x-for="(school, si) in schools" :key="'school-' + si">
<div class="flex items-center gap-2 max-w-sm">
<input type="text"
x-model="schools[si]"
@input="form.schools[si] = $event.target.value"
placeholder="请输入学校名称"
class="flex-1 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(si)" x-show="schools.length > 1"
class="text-sm text-danger-600 hover:underline focus:outline-none">删除</button>
</div>
</template>
<button type="button" @click="addSchool()"
class="flex items-center gap-1 text-sm text-primary-600 hover:text-primary-700 focus:outline-none">
<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>
<!-- 入学时间(月份选择器,使用原生 input[type=month] -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">入学时间</div>
<div class="flex-1">
<input type="month" x-model="form.enrollmentDate"
class="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">
<p class="text-xs text-neutral-400 mt-1">请选择年月</p>
</div>
</div>
</div>
</section>
<!-- ===== 二手需求 Section ===== -->
<section id="section-requirement" class="bg-white rounded-lg border border-neutral-200 p-6">
<h2 class="text-base font-semibold text-neutral-800 mb-5">二手</h2>
<div class="space-y-5">
<!-- 总价 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">
总价 <span class="text-danger-600">*</span>
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<input type="number" x-model="form.budgetMin" min="0" step="0.01" placeholder="最小值"
class="w-28 px-3 py-2 text-sm rounded-md border placeholder:text-neutral-400 focus:outline-none focus:ring-2"
:class="inputCls('budget')">
<span class="text-neutral-400">-</span>
<input type="number" x-model="form.budgetMax" min="0" step="0.01" placeholder="最大值"
class="w-28 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">
<span class="text-sm text-neutral-500">万元</span>
</div>
<p class="text-xs text-danger-600 mt-1" x-show="errors.budget" x-text="errors.budget"></p>
</div>
</div>
<!-- 面积 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">
面积 <span class="text-danger-600">*</span>
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<input type="number" x-model="form.areaMin" min="0" step="0.01" placeholder="最小值"
class="w-28 px-3 py-2 text-sm rounded-md border placeholder:text-neutral-400 focus:outline-none focus:ring-2"
:class="inputCls('area')">
<span class="text-neutral-400">-</span>
<input type="number" x-model="form.areaMax" min="0" step="0.01" placeholder="最大值"
class="w-28 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">
<span class="text-sm text-neutral-500"></span>
</div>
<p class="text-xs text-danger-600 mt-1" x-show="errors.area" x-text="errors.area"></p>
</div>
</div>
<!-- 居室 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">
居室 <span class="text-danger-600">*</span>
</div>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border px-3 py-2 min-h-[42px]"
:class="groupCls('bedroom_counts')">
<template x-for="item in bedroomOptions" :key="item.value">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.bedroomCounts" :value="item.value"
class="w-4 h-4 rounded 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 mt-1" x-show="errors.bedroom_counts" x-text="errors.bedroom_counts"></p>
</div>
</div>
<!-- 楼层 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">楼层</div>
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border border-neutral-300 px-3 py-2 min-h-[42px] flex-1">
<template x-for="item in floorOptions" :key="item.value">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.floorPreferences" :value="item.value"
class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700" x-text="item.label"></span>
</label>
</template>
</div>
</div>
<!-- 朝向 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">朝向</div>
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border border-neutral-300 px-3 py-2 min-h-[42px] flex-1">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.orientations" value="east" class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700"></span>
</label>
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.orientations" value="south" class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700"></span>
</label>
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.orientations" value="west" class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700">西</span>
</label>
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.orientations" value="north" class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700"></span>
</label>
</div>
</div>
<!-- 装修 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">装修</div>
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border border-neutral-300 px-3 py-2 min-h-[42px] flex-1">
<template x-for="item in decorationOptions" :key="item.value">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.decorations" :value="item.value"
class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700" x-text="item.label"></span>
</label>
</template>
</div>
</div>
<!-- 楼龄 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">楼龄</div>
<div class="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-md border border-neutral-300 px-3 py-2 min-h-[42px] flex-1">
<template x-for="item in buildingAgeOptions" :key="item.value">
<label class="flex items-center gap-1.5 cursor-pointer">
<input type="checkbox" x-model="form.buildingAgeRanges" :value="item.value"
class="w-4 h-4 rounded accent-primary-600">
<span class="text-sm text-neutral-700" x-text="item.label"></span>
</label>
</template>
</div>
</div>
<!-- 意向商圈(原生 select multiple -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">意向商圈</div>
<div class="flex-1 max-w-xs">
<select multiple
x-model="form.intentBusinessAreaIds"
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"
size="4">
<option value="ba-001">中心商圈</option>
<option value="ba-002">东城商圈</option>
<option value="ba-003">南海商圈</option>
<option value="ba-004">西区商圈</option>
<option value="ba-005">北部新城</option>
<option value="ba-006">科技园区</option>
</select>
<p class="text-xs text-neutral-400 mt-1">按住 Ctrl/Cmd 可多选</p>
</div>
</div>
<!-- 意向小区 -->
<div class="flex gap-0"
x-data="{ complexes: form.complexes, addComplex() { this.complexes.push(''); form.complexes = this.complexes; }, removeComplex(i) { this.complexes.splice(i, 1); form.complexes = this.complexes; } }">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">意向小区</div>
<div class="flex-1 space-y-2">
<template x-for="(complex, ci) in complexes" :key="'complex-' + ci">
<div class="flex items-center gap-2 max-w-sm">
<input type="text"
x-model="complexes[ci]"
@input="form.complexes[ci] = $event.target.value"
placeholder="请输入小区名称"
class="flex-1 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="removeComplex(ci)" x-show="complexes.length > 1"
class="text-sm text-danger-600 hover:underline focus:outline-none">删除</button>
</div>
</template>
<button type="button" @click="addComplex()"
class="flex items-center gap-1 text-sm text-primary-600 hover:text-primary-700 focus:outline-none">
<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 class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">交通</div>
<div class="flex-1 max-w-lg" x-data="{ cnt: form.transportation ? form.transportation.length : 0 }">
<input type="text" x-model="form.transportation"
@input="cnt = $event.target.value.length; form.transportation = $event.target.value"
maxlength="50"
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">
<span class="text-xs text-neutral-400 text-right block mt-0.5" x-text="cnt + ' / 50'"></span>
</div>
</div>
<!-- 备注 -->
<div class="flex gap-0">
<div class="w-28 shrink-0 pt-2 text-sm font-medium text-neutral-700">备注</div>
<div class="flex-1 max-w-lg" x-data="{ cnt: form.requirementNotes ? form.requirementNotes.length : 0 }">
<textarea x-model="form.requirementNotes"
@input="cnt = $event.target.value.length; form.requirementNotes = $event.target.value"
maxlength="200"
rows="3"
placeholder="请输入需求备注"
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 resize-none focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"></textarea>
<span class="text-xs text-neutral-400 text-right block mt-0.5" x-text="cnt + ' / 200'"></span>
</div>
</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>
</div>
</form>
</main>
<!-- ===== Toast 提示 ===== -->
<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 shrink-0 mt-0.5"
:class="toast.type === 'success' ? 'text-success-600' : 'text-danger-600'"
fill="none" stroke="currentColor" stroke-width="2.2" viewBox="0 0 24 24">
<template x-if="toast.type === 'success'">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/>
</template>
<template x-if="toast.type !== 'success'">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"/>
</template>
</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" x-text="toast.subText"></p>
</div>
</div>
</div>
<script>
function editClientPage() {
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(暂不关注)' }
],
buyingPurposeOptions: [
{ value: 'rigid', label: '刚需' },
{ value: 'investment', label: '投资' },
{ value: 'school_district',label: '学区' },
{ value: 'upgrade', label: '改善' },
{ value: 'commercial', label: '商用' },
{ value: 'other', label: '其他' }
],
paymentOptions: [
{ value: 'full', label: '全额' },
{ value: 'mortgage', label: '商业贷款' },
{ value: 'mortgage_fund', label: '商业贷款+公积金' },
{ value: 'fund', label: '公积金' }
],
bedroomOptions: [
{ value: 1, label: '1居' },
{ value: 2, label: '2居' },
{ value: 3, label: '3居' },
{ value: 4, label: '4居' },
{ value: 5, label: '5居及以上' }
],
floorOptions: [
{ value: 'no_first', label: '不要一层' },
{ value: 'low', label: '低楼层' },
{ value: 'mid', label: '中楼层' },
{ value: 'high', label: '高楼层' },
{ value: 'no_top', label: '不要顶层' }
],
decorationOptions: [
{ value: 'rough', label: '毛坯' },
{ value: 'clear', label: '清水' },
{ value: 'simple', label: '简装' },
{ value: 'mid', label: '中装' },
{ value: 'fine', label: '精装' },
{ value: 'luxury', label: '豪装' }
],
buildingAgeOptions: [
{ value: 'within_5y', label: '5年以内' },
{ value: '5_10y', label: '5-10年' },
{ value: '10_15y', label: '10-15年' },
{ value: '15_20y', label: '15-20年' },
{ value: 'over_20y', label: '20年以上' }
],
/* -------- 交互状态 -------- */
activeTab: 'contacts',
phoneRevealed: false,
revealLoading: false,
submitting: false,
redirectHint: '',
errors: {},
toast: { show: false, message: '', subText: '', type: 'success' },
/* -------- 表单数据(含占位回显值) -------- */
form: {
contacts: [],
requirementTypes: ['second_hand'],
propertyUsage: 'residential',
grade: 'C',
source: 'store_visit',
buyingPurpose: ['rigid', 'school_district'],
paymentMethod: 'mortgage',
propertiesOwned: 'none',
hasLoanRecord: 'false',
idType: 'id_card',
idNumber: '',
schools: ['实验小学'],
enrollmentDate: '2027-09',
budgetMin: 150,
budgetMax: 280,
areaMin: 80,
areaMax: 130,
bedroomCounts: [2, 3],
floorPreferences: ['mid', 'high'],
orientations: ['south'],
decorations: ['fine'],
buildingAgeRanges: ['within_5y', '5_10y'],
intentBusinessAreaIds: ['ba-001', 'ba-002'],
complexes: ['碧桂园翡翠湾', '万科金色家园'],
transportation: '需要地铁沿线步行15分钟内',
requirementNotes: '楼层中高层为主,南向采光要求好,暂不考虑临街房源。'
},
/* -------- 初始化 -------- */
init() {
this.form.contacts = [
{
id: 'c-001',
name: '张明峰',
gender: 'male',
phoneMasked: '137****8921',
phoneCountryCode: '+86',
phone: '13712348921',
phone2CountryCode: '+86',
phone2: '',
wechat: 'zhangmf_2024',
qq: '',
remarks: '白天方便通话,晚上不接陌生来电'
},
{
id: 'c-002',
name: '李晓梅',
gender: 'female',
phoneMasked: '139****6543',
phoneCountryCode: '+86',
phone: '13956786543',
phone2CountryCode: '+86',
phone2: '',
wechat: '',
qq: '345678901',
remarks: ''
}
];
// Intersection ObserverTab 随滚动自动高亮
this.$nextTick(() => {
const sections = ['contacts', 'basic', 'requirement'];
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.id.replace('section-', '');
if (sections.includes(id)) this.activeTab = id;
}
});
}, { rootMargin: '-30% 0px -50% 0px' });
sections.forEach(id => {
const el = document.getElementById('section-' + id);
if (el) observer.observe(el);
});
});
},
/* -------- Tab 切换 -------- */
setTab(tab) {
this.activeTab = tab;
const el = document.getElementById('section-' + tab);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
},
/* -------- 字段 key 辅助 -------- */
fk(group, idx, field) {
return group + '_' + idx + '_' + field;
},
inputCls(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';
},
groupCls(key) {
return this.errors[key]
? 'border-danger-600 ring-2 ring-danger-600/20'
: 'border-neutral-300';
},
phoneCls(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({
id: 'c-new-' + Date.now(),
name: '', gender: '',
phoneMasked: '', phoneCountryCode: '+86', phone: '',
phone2CountryCode: '+86', phone2: '',
wechat: '', qq: '', remarks: ''
});
},
removeContact(idx) {
if (idx === 0 || this.form.contacts.length <= 1) return;
this.form.contacts.splice(idx, 1);
},
/* -------- 查看号码 -------- */
revealPhone() {
if (this.revealLoading || this.phoneRevealed) return;
this.revealLoading = true;
setTimeout(() => {
this.phoneRevealed = true;
this.revealLoading = false;
this.showToast('已解码', '号码已显示,本次查看已留痕', 'success');
}, 800);
},
/* -------- 表单校验 -------- */
validateForm() {
this.errors = {};
let firstKey = '';
this.form.contacts.forEach((c, idx) => {
const nameKey = this.fk('contacts', idx, 'name');
const genderKey = this.fk('contacts', idx, 'gender');
const phoneKey = this.fk('contacts', idx, 'phone');
if (!c.name) {
this.errors[nameKey] = '姓名不能为空';
if (!firstKey) firstKey = nameKey;
}
if (!c.gender) {
this.errors[genderKey] = '请选择称呼';
if (!firstKey) firstKey = genderKey;
}
if ((this.phoneRevealed || idx > 0) && !c.phone) {
this.errors[phoneKey] = '请输入手机号';
if (!firstKey) firstKey = phoneKey;
}
});
if (!this.form.requirementTypes || this.form.requirementTypes.length === 0) {
this.errors.requirement_types = '请至少选择一种需求类型';
if (!firstKey) firstKey = 'requirement_types';
}
if (!this.form.propertyUsage) {
this.errors.property_usage = '请选择用途';
if (!firstKey) firstKey = 'property_usage';
}
if (!this.form.grade) {
this.errors.grade = '请选择等级';
if (!firstKey) firstKey = 'grade';
}
if (!this.form.source) {
this.errors.source = '请选择客户来源';
if (!firstKey) firstKey = 'source';
}
if (!this.form.budgetMin && !this.form.budgetMax) {
this.errors.budget = '请填写总价区间';
if (!firstKey) firstKey = 'budget';
}
if (!this.form.areaMin && !this.form.areaMax) {
this.errors.area = '请填写面积区间';
if (!firstKey) firstKey = 'area';
}
if (!this.form.bedroomCounts || this.form.bedroomCounts.length === 0) {
this.errors.bedroom_counts = '请至少选择一种居室要求';
if (!firstKey) firstKey = 'bedroom_counts';
}
if (firstKey) {
this.$nextTick(() => {
const el = document.querySelector('[data-field-key="' + firstKey + '"]');
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
return false;
}
return true;
},
/* -------- 提交 -------- */
submitForm() {
if (this.submitting) return;
if (!this.validateForm()) return;
this.submitting = true;
this.redirectHint = '';
setTimeout(() => {
this.submitting = false;
this.showToast('保存成功', '客源信息已更新', 'success');
this.redirectHint = '模拟跳转中:即将返回客源详情页 /clients/CL20260426001/';
}, 1200);
},
/* -------- 取消 -------- */
cancelForm() {
this.redirectHint = '已取消编辑,模拟返回客源详情页 /clients/CL20260426001/';
},
/* -------- Toast -------- */
showToast(message, subText = '', type = 'success') {
this.toast = { show: true, message, subText, type };
setTimeout(() => { this.toast.show = false; }, 2500);
}
};
}
</script>
</body>
</html>