Files
nexus/Project/fonrey/UI_DESIGN/楼盘列表_UI.html
2026-04-28 16:39:52 +08:00

513 lines
24 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' }
}
}
}
}
</script>
<style>
:root {
--bg-page: #F8FAFC;
--bg-card: #FFFFFF;
--bg-subtle: #F1F5F9;
--text-primary: #0F172A;
--text-secondary: #64748B;
--border: #E2E8F0;
}
html { scroll-behavior: smooth; }
body {
background: var(--bg-page);
color: var(--text-primary);
transition: background-color .2s ease, color .2s ease;
}
[x-cloak] { display: none !important; }
.tabular-nums { font-variant-numeric: tabular-nums; }
.bg-surface { background: var(--bg-card); }
.bg-subtle { background: var(--bg-subtle); }
.border-surface { border-color: var(--border); }
.text-surface { color: var(--text-primary); }
.text-muted { color: var(--text-secondary); }
.module-tab {
color: #64748B;
border-bottom: 2px solid transparent;
}
.module-tab.active {
color: #0F766E;
border-bottom-color: #0F766E;
font-weight: 600;
}
.chip {
border: 1px solid #E2E8F0;
background: #FFFFFF;
color: #64748B;
}
.chip.active {
border-color: #0F766E;
color: #0F766E;
background: #F0FDFA;
font-weight: 600;
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
</style>
</head>
<body class="text-sm antialiased" x-data="complexListPage()" x-init="init()">
<!-- Top Bar -->
<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 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>
<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>
<!-- Side Bar -->
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-surface bg-surface overflow-y-auto">
<nav class="p-3 space-y-0.5">
<div class="px-2 pt-2 pb-1 text-xs font-medium text-muted 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">楼盘管理</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>
<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 -->
<main class="ml-60 pt-[72px] min-h-screen px-6 py-5">
<div class="mx-auto max-w-[1680px] space-y-4">
<!-- Breadcrumb + title -->
<section class="bg-surface border border-surface rounded-lg p-4">
<div class="flex items-start justify-between gap-4">
<div>
<nav class="flex items-center gap-1 text-xs text-muted mb-2" aria-label="面包屑">
<a href="#" class="hover:text-neutral-700">房源</a>
<span>/</span>
<a href="#" class="hover:text-neutral-700">小区</a>
<span>/</span>
<span class="text-surface">楼盘管理系统-楼盘管理</span>
</nav>
<h1 class="text-xl font-semibold text-surface">楼盘管理</h1>
</div>
</div>
</section>
<!-- Module Tabs -->
<section class="bg-surface border border-surface rounded-lg px-4">
<nav class="flex items-center gap-6 overflow-x-auto" aria-label="楼盘管理模块导航">
<button class="module-tab py-3 whitespace-nowrap active">楼盘</button>
<button class="module-tab py-3 whitespace-nowrap">区域管理</button>
<button class="module-tab py-3 whitespace-nowrap">学校管理</button>
<button class="module-tab py-3 whitespace-nowrap">应用标准数据</button>
</nav>
</section>
<!-- Completeness metrics -->
<section class="bg-info-50 border border-info-600/20 rounded-lg p-3">
<div class="flex items-center justify-between gap-3 mb-2">
<h2 class="text-sm font-semibold text-info-600">数据完整度指标</h2>
<button class="text-xs px-2 py-1 rounded border border-info-600/30 text-info-600 bg-white hover:bg-info-50" @click="notify('已触发重新计算(原型)')">重新计算</button>
</div>
<div class="grid grid-cols-7 gap-2 text-xs">
<template x-for="item in metrics" :key="item.name">
<div class="bg-white rounded-md border border-info-600/10 p-2">
<p class="text-muted" x-text="item.name"></p>
<p class="font-semibold tabular-nums mt-0.5" x-text="item.value"></p>
</div>
</template>
</div>
</section>
<!-- Search + Filter -->
<section class="bg-surface border border-surface rounded-lg p-4 space-y-3">
<div class="flex items-center gap-2">
<input x-model.trim="filters.keyword" type="text" placeholder="楼盘名/别名/拼音/详细地址" class="w-[380px] px-3 py-2 rounded-md border border-surface bg-white focus:outline-none focus:ring-2 focus:ring-primary-600/40" />
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="applyFilters()">查询</button>
<button class="px-3 py-2 rounded-md border border-surface hover:bg-neutral-50" @click="resetFilters()">清除</button>
</div>
<div class="flex items-center gap-2 flex-wrap">
<span class="text-xs text-muted">区域:</span>
<template x-for="item in regionOptions" :key="item">
<button class="chip px-2.5 py-1 rounded-md text-xs" :class="{ 'active': filters.region === item }" @click="filters.region = item" x-text="item"></button>
</template>
</div>
<div class="flex items-center gap-2 flex-wrap">
<span class="text-xs text-muted">用途:</span>
<template x-for="item in usageOptions" :key="item">
<button class="chip px-2.5 py-1 rounded-md text-xs" :class="{ 'active': filters.usage === item }" @click="filters.usage = item" x-text="item"></button>
</template>
</div>
<div class="grid grid-cols-7 gap-2">
<template x-for="cfg in extraFilters" :key="cfg.key">
<select x-model="filters[cfg.key]" class="px-2.5 py-2 rounded-md border border-surface bg-white text-xs focus:outline-none focus:ring-2 focus:ring-primary-600/40">
<option value="" x-text="cfg.label + ':不限'"></option>
<template x-for="opt in cfg.options" :key="opt">
<option :value="opt" x-text="opt"></option>
</template>
</select>
</template>
</div>
</section>
<!-- Action bar -->
<section class="bg-surface border border-surface rounded-lg p-3 flex items-center justify-between gap-3">
<div class="flex items-center gap-2 flex-wrap">
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('新增楼盘(原型入口)')">+ 新增楼盘</button>
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedCount===0" :class="selectedCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'">批量新增楼栋</button>
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedCount===0" :class="selectedCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'">批改区域商圈</button>
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedCount===0" :class="selectedCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'">删除</button>
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedCount===0" :class="selectedCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'">合并楼盘</button>
</div>
<div class="text-xs text-muted">
<span x-show="selectedCount===0">未选中楼盘</span>
<span x-show="selectedCount>0" x-text="'已选 ' + selectedCount + ' 条'"></span>
</div>
</section>
<!-- Table -->
<section class="bg-surface border border-surface rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="bg-subtle border-b border-surface">
<tr class="text-left">
<th scope="col" class="px-3 py-2 w-10">
<input type="checkbox" class="rounded border-surface" :checked="allOnPageSelected" @click.prevent="toggleSelectPage(!allOnPageSelected)" aria-label="全选当前页" />
</th>
<th scope="col" class="px-3 py-2">楼盘名称</th>
<th scope="col" class="px-3 py-2">楼盘类型</th>
<th scope="col" class="px-3 py-2">详细地址</th>
<th scope="col" class="px-3 py-2">城区商圈</th>
<th scope="col" class="px-3 py-2">当月挂牌均价(元/㎡)</th>
<th scope="col" class="px-3 py-2">楼栋数</th>
<th scope="col" class="px-3 py-2">产品数</th>
<th scope="col" class="px-3 py-2">房源数</th>
<th scope="col" class="px-3 py-2">操作</th>
</tr>
</thead>
<tbody>
<template x-if="paginatedRows.length === 0">
<tr>
<td colspan="10" class="px-4 py-12 text-center">
<p class="text-base font-medium text-surface">暂无匹配楼盘</p>
<p class="text-xs text-muted mt-1">请尝试调整筛选条件</p>
<button class="mt-3 px-3 py-1.5 rounded-md border border-surface hover:bg-neutral-50" @click="resetFilters()">清除筛选</button>
</td>
</tr>
</template>
<template x-for="row in paginatedRows" :key="row.id">
<tr class="border-b border-surface hover:bg-neutral-50/50">
<td class="px-3 py-2 align-top">
<input type="checkbox" class="rounded border-surface" :checked="isSelected(row.id)" @click.prevent="toggleSelect(row.id, !isSelected(row.id))" aria-label="选择楼盘" />
</td>
<td class="px-3 py-2 align-top">
<button class="text-left text-info-600 hover:underline font-medium" @click="openDetail(row)" x-text="row.name"></button>
<div class="mt-1 flex items-center gap-1 flex-wrap">
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-info-50 text-info-600">信息</span>
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-warning-50 text-warning-600">标准楼盘</span>
<span class="text-[10px] px-1.5 py-0.5 rounded-full bg-danger-50 text-danger-600">标准房号</span>
</div>
</td>
<td class="px-3 py-2 align-top" x-text="row.usage"></td>
<td class="px-3 py-2 align-top" x-text="row.address"></td>
<td class="px-3 py-2 align-top" x-text="row.region + '-' + row.biz"></td>
<td class="px-3 py-2 align-top tabular-nums" x-text="row.avgPrice"></td>
<td class="px-3 py-2 align-top tabular-nums" x-text="row.buildingCount"></td>
<td class="px-3 py-2 align-top tabular-nums" x-text="row.productCount"></td>
<td class="px-3 py-2 align-top">
<span class="text-xs text-muted">出售</span>
<span class="tabular-nums" x-text="row.saleCount"></span>
<span class="text-xs text-muted">/出租</span>
<span class="tabular-nums" x-text="row.rentCount"></span>
<span class="text-xs text-muted">/共</span>
<span class="tabular-nums" x-text="row.saleCount + row.rentCount"></span>
</td>
<td class="px-3 py-2 align-top">
<div class="flex items-center gap-2">
<button class="text-info-600 hover:underline text-xs" @click="notify('编辑楼盘(原型)')">编辑</button>
<button class="text-danger-600 hover:underline text-xs" @click="notify('删除楼盘(原型)')">删除</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</section>
<!-- Pagination -->
<section class="bg-surface border border-surface rounded-lg px-4 py-3 flex items-center justify-between">
<p class="text-xs text-muted" x-text="'共 ' + filteredRows.length + ' 条'" aria-live="polite"></p>
<div class="flex items-center gap-2 text-xs">
<button class="px-2 py-1 rounded border border-surface" :disabled="currentPage===1" :class="currentPage===1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="goPrev()">上一页</button>
<span class="tabular-nums" x-text="currentPage + ' / ' + totalPages"></span>
<button class="px-2 py-1 rounded border border-surface" :disabled="currentPage===totalPages || totalPages===0" :class="(currentPage===totalPages || totalPages===0) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="goNext()">下一页</button>
<span class="ml-2">20条/页</span>
<span class="ml-2">跳至</span>
<input type="number" min="1" :max="totalPages || 1" x-model.number="jumpPage" class="w-16 px-2 py-1 rounded border border-surface" />
<button class="px-2 py-1 rounded border border-surface hover:bg-neutral-50" @click="goJump()">确定</button>
</div>
</section>
</div>
</main>
<!-- Toast -->
<div x-show="toast.show" x-cloak class="fixed bottom-5 right-5 z-50 px-4 py-2 rounded-md text-sm text-white shadow-lg bg-success-600" x-text="toast.message"></div>
<script>
function complexListPage() {
return {
rows: [],
filters: {
keyword: '',
region: '不限',
usage: '不限',
fixedStatus: '',
completion: '',
complexType: '',
hasProperty: '',
buildingType: '',
ownership: '',
hasCoord: ''
},
regionOptions: ['不限', '静安', '闵行', '普陀', '松江', '长宁', '嘉定'],
usageOptions: ['不限', '住宅', '别墅', '商住', '商业', '写字楼', '其他'],
extraFilters: [
{ key: 'fixedStatus', label: '固定情况', options: ['已固定', '未固定'] },
{ key: 'completion', label: '完善情况', options: ['高', '中', '低'] },
{ key: 'complexType', label: '楼盘类型', options: ['标准', '非标'] },
{ key: 'hasProperty', label: '有无房源', options: ['有', '无'] },
{ key: 'buildingType', label: '楼栋类型', options: ['板楼', '塔楼', '板塔结合'] },
{ key: 'ownership', label: '权属关系', options: ['商品房', '房改房', '经济适用房'] },
{ key: 'hasCoord', label: '有无坐标', options: ['有坐标', '无坐标'] }
],
metrics: [
{ name: '楼盘关联率', value: '47.61%' },
{ name: '楼栋及单元完整率', value: '100.38%' },
{ name: '房号匹配率', value: '100%' },
{ name: '处置率', value: '12.05%' },
{ name: '入住人结构数据', value: '58 / 3000' },
{ name: '有效结构数量', value: '523 / 523' },
{ name: '房源对标', value: '1.83%' }
],
pageSize: 20,
currentPage: 1,
jumpPage: 1,
selectedIds: [],
toast: { show: false, message: '' },
init() {
this.rows = this.mockRows();
},
mockRows() {
const seeds = [
['都市港湾', '住宅', '嘉定', '丰庄', '上海 嘉定 海波路1000弄'],
['阳光威尼斯四期', '别墅', '普陀', '真光', '上海 普陀 金鼎路1600弄'],
['嘉城名都', '住宅', '嘉定', '江桥', '上海 嘉定 嘉城路188弄'],
['中海臻如府', '商住', '普陀', '真如', '上海 普陀 真如路88弄'],
['凯旋华庭', '写字楼', '长宁', '中山公园', '上海 长宁 凯旋路888号'],
['虹桥商务中心', '商业', '闵行', '虹桥', '上海 闵行 申长路699号'],
['静安云邸', '住宅', '静安', '大宁', '上海 静安 万荣路66弄'],
['松江壹号院', '别墅', '松江', '大学城', '上海 松江 文汇路188号']
];
const list = [];
for (let i = 1; i <= 46; i++) {
const s = seeds[(i - 1) % seeds.length];
list.push({
id: i,
name: `${s[0]}${i}`,
usage: s[1],
region: s[2],
biz: s[3],
address: s[4],
avgPrice: (32000 + i * 137).toFixed(2),
buildingCount: (6 + (i % 16)),
productCount: (50 + (i % 90)),
saleCount: (i % 12),
rentCount: (i % 8)
});
}
return list;
},
applyFilters() {
this.currentPage = 1;
this.jumpPage = 1;
this.selectedIds = [];
},
resetFilters() {
this.filters = {
keyword: '',
region: '不限',
usage: '不限',
fixedStatus: '',
completion: '',
complexType: '',
hasProperty: '',
buildingType: '',
ownership: '',
hasCoord: ''
};
this.currentPage = 1;
this.jumpPage = 1;
this.selectedIds = [];
},
get filteredRows() {
const keyword = (this.filters.keyword || '').toLowerCase();
return this.rows.filter((r) => {
const hitKeyword = !keyword || [r.name, r.address, `${r.region}${r.biz}`].some(v => String(v).toLowerCase().includes(keyword));
const hitRegion = this.filters.region === '不限' || r.region === this.filters.region;
const hitUsage = this.filters.usage === '不限' || r.usage === this.filters.usage;
return hitKeyword && hitRegion && hitUsage;
});
},
get totalPages() {
return Math.max(1, Math.ceil(this.filteredRows.length / this.pageSize));
},
get paginatedRows() {
const page = Math.min(this.currentPage, this.totalPages);
const start = (page - 1) * this.pageSize;
return this.filteredRows.slice(start, start + this.pageSize);
},
get selectedCount() {
return this.selectedIds.length;
},
isSelected(id) {
return this.selectedIds.includes(id);
},
toggleSelect(id, checked) {
if (checked) {
if (!this.selectedIds.includes(id)) this.selectedIds.push(id);
} else {
this.selectedIds = this.selectedIds.filter(x => x !== id);
}
},
get allOnPageSelected() {
if (this.paginatedRows.length === 0) return false;
return this.paginatedRows.every(r => this.selectedIds.includes(r.id));
},
toggleSelectPage(checked) {
const pageIds = this.paginatedRows.map(r => r.id);
if (checked) {
const set = new Set([...this.selectedIds, ...pageIds]);
this.selectedIds = [...set];
} else {
this.selectedIds = this.selectedIds.filter(id => !pageIds.includes(id));
}
},
goPrev() {
if (this.currentPage > 1) this.currentPage -= 1;
this.jumpPage = this.currentPage;
this.selectedIds = [];
},
goNext() {
if (this.currentPage < this.totalPages) this.currentPage += 1;
this.jumpPage = this.currentPage;
this.selectedIds = [];
},
goJump() {
let p = Number(this.jumpPage || 1);
if (!Number.isFinite(p)) p = 1;
p = Math.max(1, Math.min(this.totalPages, Math.floor(p)));
this.currentPage = p;
this.jumpPage = p;
this.selectedIds = [];
},
openDetail(row) {
window.location.hash = `complex/${row.id}`;
this.notify(`进入楼盘详情(原型):${row.name}`);
},
notify(message) {
this.toast = { show: true, message };
window.setTimeout(() => { this.toast.show = false; }, 1800);
}
}
}
</script>
</body>
</html>