chore: sync local project changes

This commit is contained in:
Shen Wei
2026-04-28 16:39:21 +08:00
parent 365caa800a
commit e4cf7f8485
27 changed files with 13691 additions and 1317 deletions

View File

@@ -55,6 +55,11 @@
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
.chip { border:1px solid #E2E8F0; background:#FFFFFF; color:#64748B; }
.chip.active { border-color:#0F766E; color:#0F766E; background:#F0FDFA; }
.subtab { color:#64748B; border-bottom:2px solid transparent; }
.subtab.active { color:#0F766E; border-bottom-color:#0F766E; font-weight:600; }
</style>
</head>
<body class="bg-neutral-50 text-sm text-neutral-700 antialiased" x-data="createClientPage()">
@@ -108,8 +113,78 @@
<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">
<form id="create-client-form" @submit.prevent="submitForm" class="space-y-4 pb-28">
<section class="bg-white rounded-lg border border-neutral-200 p-4">
<div>
<div class="text-xs text-neutral-500 mb-2">需求状态</div>
<div class="flex items-center gap-2" id="demandStatusGroup" data-field-key="status">
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" :class="{ 'active': form.status==='buying' }" @click="form.status='buying'">求购</button>
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" :class="{ 'active': form.status==='renting' }" @click="form.status='renting'">求租</button>
<button type="button" class="chip px-3 py-1.5 rounded-md text-sm" :class="{ 'active': form.status==='buy_or_rent' }" @click="form.status='buy_or_rent'">租购</button>
</div>
<p class="text-xs text-danger-600 mt-1" x-show="errors.status" x-text="errors.status"></p>
</div>
</section>
<nav class="bg-white rounded-lg border border-neutral-200 px-4 sticky top-[74px] z-10">
<div class="flex items-center gap-5 overflow-x-auto text-sm">
<a href="#section-client-info" class="subtab py-3" :class="{ 'active': activeSection==='client-info' }">房源信息</a>
<a href="#section-contact" class="subtab py-3" :class="{ 'active': activeSection==='contact' }">联系人</a>
<a href="#section-basic" class="subtab py-3" :class="{ 'active': activeSection==='basic' }">基础信息</a>
<a href="#section-staff" class="subtab py-3" :class="{ 'active': activeSection==='staff' }">相关员工</a>
</div>
</nav>
<section id="section-client-info" class="bg-white rounded-lg border border-neutral-200 p-6" data-section-key="client-info">
<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="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>
</section>
<section id="section-contact" class="bg-white rounded-lg border border-neutral-200 p-6" data-section-key="contact">
<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>
@@ -250,72 +325,10 @@
</div>
</section>
<section class="bg-white rounded-lg border border-neutral-200 p-6">
<section id="section-basic" class="bg-white rounded-lg border border-neutral-200 p-6" data-section-key="basic">
<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>
@@ -358,7 +371,7 @@
</div>
</section>
<section class="bg-white rounded-lg border border-neutral-200 p-6">
<section id="section-staff" class="bg-white rounded-lg border border-neutral-200 p-6" data-section-key="staff">
<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">
@@ -374,27 +387,39 @@
</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 class="fixed bottom-4 left-[calc(15rem+1.5rem)] right-6 z-30">
<div class="mx-auto max-w-5xl bg-white rounded-lg border border-neutral-200 shadow-xs p-4">
<div class="flex items-center justify-end gap-3">
<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>
<button
type="button"
@click="submitAndContinue"
: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>
<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>
</div>
<p x-show="redirectHint" x-text="redirectHint" class="text-xs text-success-600 text-right mt-2"></p>
</div>
</div>
</form>
</div>
@@ -419,7 +444,6 @@
{ value: 'villa', label: '别墅' },
{ value: 'commercial_residential', label: '商住' },
{ value: 'shop', label: '商铺' },
{ value: 'office', label: '写字楼' },
{ value: 'other', label: '其他' }
],
gradeOptions: [
@@ -435,6 +459,7 @@
infoExpanded: false,
checkingDuplicate: false,
duplicateHint: '',
activeSection: 'client-info',
submitting: false,
redirectHint: '',
errors: {},
@@ -458,6 +483,7 @@
init() {
this.form.contacts = [this.newContact()];
this.simulateSourceLoading();
this.bindSectionObserver();
},
newContact() {
@@ -516,6 +542,24 @@
this.form.schools.splice(idx, 1);
},
setPropertyUsage(type) {
this.form.propertyUsage = type;
},
bindSectionObserver() {
this.$nextTick(() => {
const sections = Array.from(document.querySelectorAll('[data-section-key]'));
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.activeSection = entry.target.getAttribute('data-section-key');
}
});
}, { rootMargin: '-35% 0px -55% 0px', threshold: 0 });
sections.forEach((s) => observer.observe(s));
});
},
simulateSourceLoading() {
setTimeout(() => {
this.sourceOptions = [
@@ -619,7 +663,7 @@
}, 2200);
},
submitForm() {
submitForm(continueMode = false) {
if (this.submitting) return;
if (!this.validateForm()) return;
@@ -629,10 +673,28 @@
setTimeout(() => {
this.submitting = false;
this.showToast('保存成功');
this.redirectHint = '模拟跳转中:即将进入客源详情页 /clients/CL20260426001/';
if (continueMode) {
this.form.contacts = [this.newContact()];
this.form.status = '';
this.form.grade = '';
this.form.source = '';
this.form.idType = '';
this.form.idNumber = '';
this.form.schools = [''];
this.infoExpanded = false;
this.errors = {};
this.redirectHint = '已保存当前客源,表单已重置,可继续新增。';
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
this.redirectHint = '模拟跳转中:即将进入客源详情页 /clients/CL20260426001/';
}
}, 1200);
},
submitAndContinue() {
this.submitForm(true);
},
cancelForm() {
this.redirectHint = '已取消录入,模拟返回客源列表 /clients/';
}