Files
nexus/Project/fonrey/UI_DESIGN/区域管理_UI.html
2026-04-29 15:43:49 +08:00

1119 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' }
}
}
}
}
</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;
}
.sub-tab {
color: #64748B;
border-bottom: 2px solid transparent;
}
.sub-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;
}
.action-link {
font-size: 12px;
color: #2563EB;
}
.action-link:hover { text-decoration: underline; }
.table-head-sort::after {
content: '↕';
font-size: 11px;
margin-left: 4px;
color: #94A3B8;
}
::-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="regionManagePage()" x-init="init()" @keydown.escape.window="closeAllModals()">
<!-- 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">
<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>
</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">楼盘</button>
<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>
</nav>
</section>
<!-- Sub Tabs -->
<section class="bg-surface border border-surface rounded-lg px-4">
<nav class="flex items-center gap-6" aria-label="区域管理子导航">
<button class="sub-tab py-3" :class="{ 'active': activeSubTab==='district' }" @click="switchSubTab('district')">城区管理</button>
<button class="sub-tab py-3" :class="{ 'active': activeSubTab==='biz' }" @click="switchSubTab('biz')">商圈管理</button>
</nav>
</section>
<!-- District Panel -->
<template x-if="activeSubTab==='district'">
<section class="space-y-4">
<!-- Filters -->
<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="districtFilters.keyword" type="text" placeholder="城区名称" class="w-[320px] px-3 py-2 rounded-md border border-surface bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40" />
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="applyDistrictFilters()">查询</button>
<button class="px-3 py-2 rounded-md border border-surface hover:bg-neutral-50" @click="resetDistrictFilters()">重置</button>
</div>
<div class="flex items-center gap-2 flex-wrap">
<span class="text-xs text-muted">有无坐标:</span>
<template x-for="option in coordOptions" :key="'district-' + option">
<button class="chip px-2.5 py-1 rounded-md text-xs" :class="{ 'active': districtFilters.coord === option }" @click="districtFilters.coord = option" x-text="option"></button>
</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 border border-surface" :disabled="selectedDistrictCount===0" :class="selectedDistrictCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="notify('已触发合并城区(原型)')">合并城区</button>
<span class="text-xs text-muted" x-text="'已选本页 ' + selectedDistrictCount + ' 条'"></span>
</div>
<button data-test="btn-add-district" class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="openDistrictModal('add')">新增城区</button>
</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="districtAllOnPageSelected" @click.prevent="toggleDistrictPageSelect(!districtAllOnPageSelected)" aria-label="全选当前页城区" />
</th>
<th scope="col" class="px-3 py-2">城区名称</th>
<th scope="col" class="px-3 py-2 table-head-sort">商圈数量</th>
<th scope="col" class="px-3 py-2 table-head-sort">楼盘数量</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="districtPaginatedRows.length === 0">
<tr>
<td colspan="6" 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>
</td>
</tr>
</template>
<template x-for="row in districtPaginatedRows" :key="'district-row-' + 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="isDistrictSelected(row.id)" @click.prevent="toggleDistrictSelect(row.id, !isDistrictSelected(row.id))" aria-label="选择城区" />
</td>
<td class="px-3 py-2 align-top" x-text="row.name"></td>
<td class="px-3 py-2 align-top tabular-nums" x-text="row.bizCount"></td>
<td class="px-3 py-2 align-top tabular-nums" x-text="row.complexCount"></td>
<td class="px-3 py-2 align-top tabular-nums text-xs" :class="row.coord ? '' : 'text-muted'" x-text="row.coord || '—'"></td>
<td class="px-3 py-2 align-top">
<div class="flex items-center gap-3">
<button class="action-link" @click="openDistrictModal('edit', row)">修改</button>
<button class="action-link" @click="openCoordModal('district', row)">设置坐标</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="'共 ' + districtFilteredRows.length + ' 条'"></p>
<div class="flex items-center gap-2 text-xs">
<button class="px-2 py-1 rounded border border-surface" :disabled="districtPage===1" :class="districtPage===1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="districtPrevPage()">上一页</button>
<span class="tabular-nums" x-text="districtPage + ' / ' + districtTotalPages"></span>
<button class="px-2 py-1 rounded border border-surface" :disabled="districtPage===districtTotalPages" :class="districtPage===districtTotalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="districtNextPage()">下一页</button>
<span class="ml-2">20条/页</span>
<span class="ml-1">跳至</span>
<input type="number" min="1" :max="districtTotalPages" x-model.number="districtJumpPage" 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="districtGoPage()">确定</button>
</div>
</section>
</section>
</template>
<!-- Biz Panel -->
<template x-if="activeSubTab==='biz'">
<section class="space-y-4">
<!-- Filters -->
<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="bizFilters.keyword" type="text" placeholder="商圈名称" class="w-[320px] px-3 py-2 rounded-md border border-surface bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40" />
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="applyBizFilters()">查询</button>
<button class="px-3 py-2 rounded-md border border-surface hover:bg-neutral-50" @click="resetBizFilters()">重置</button>
</div>
<div class="flex items-start gap-2">
<span class="text-xs text-muted leading-7 shrink-0">区域:</span>
<div class="flex items-center gap-2 flex-wrap">
<template x-for="option in regionOptions" :key="'biz-region-' + option">
<button class="chip px-2.5 py-1 rounded-md text-xs" :class="{ 'active': bizFilters.region === option }" @click="bizFilters.region = option" x-text="option"></button>
</template>
</div>
</div>
<div class="flex items-center gap-2 flex-wrap">
<span class="text-xs text-muted">有无坐标:</span>
<template x-for="option in coordOptions" :key="'biz-coord-' + option">
<button class="chip px-2.5 py-1 rounded-md text-xs" :class="{ 'active': bizFilters.coord === option }" @click="bizFilters.coord = option" x-text="option"></button>
</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 border border-surface" :disabled="selectedBizCount===0" :class="selectedBizCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="notify('已触发合并商圈(原型)')">合并商圈</button>
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedBizCount===0" :class="selectedBizCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="notify('已触发转移商圈(原型)')">转移商圈</button>
<span class="text-xs text-muted" x-text="'已选本页 ' + selectedBizCount + ' 条'"></span>
</div>
<button data-test="btn-add-biz" class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="openBizModal('add')">新增商圈</button>
</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="bizAllOnPageSelected" @click.prevent="toggleBizPageSelect(!bizAllOnPageSelected)" 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 table-head-sort">楼盘数量</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="bizPaginatedRows.length === 0">
<tr>
<td colspan="6" 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>
</td>
</tr>
</template>
<template x-for="row in bizPaginatedRows" :key="'biz-row-' + 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="isBizSelected(row.id)" @click.prevent="toggleBizSelect(row.id, !isBizSelected(row.id))" aria-label="选择商圈" />
</td>
<td class="px-3 py-2 align-top" x-text="row.district"></td>
<td class="px-3 py-2 align-top">
<span class="inline-flex items-center gap-1">
<span x-show="row.isStandard" class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] bg-info-50 text-info-600">标准</span>
<span x-text="row.name"></span>
</span>
</td>
<td class="px-3 py-2 align-top tabular-nums" x-text="row.complexCount"></td>
<td class="px-3 py-2 align-top tabular-nums text-xs" :class="row.coord ? '' : 'text-muted'" x-text="row.coord || '—'"></td>
<td class="px-3 py-2 align-top">
<div class="flex items-center gap-3">
<button class="action-link" @click="openBizModal('edit', row)">修改</button>
<button class="action-link" @click="openRelationModal(row)">查看关联关系</button>
<button class="action-link" @click="openCoordModal('biz', row)">设置坐标</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="'共 ' + bizFilteredRows.length + ' 条'"></p>
<div class="flex items-center gap-2 text-xs">
<button class="px-2 py-1 rounded border border-surface" :disabled="bizPage===1" :class="bizPage===1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="bizPrevPage()">上一页</button>
<span class="tabular-nums" x-text="bizPage + ' / ' + bizTotalPages"></span>
<button class="px-2 py-1 rounded border border-surface" :disabled="bizPage===bizTotalPages" :class="bizPage===bizTotalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="bizNextPage()">下一页</button>
<span class="ml-2">20条/页</span>
<span class="ml-1">跳至</span>
<input type="number" min="1" :max="bizTotalPages" x-model.number="bizJumpPage" 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="bizGoPage()">确定</button>
</div>
</section>
</section>
</template>
</div>
</main>
<!-- District Add/Edit Modal -->
<div x-show="districtModal.open" x-cloak class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-neutral-900/40" @click="closeDistrictModal()"></div>
<div class="relative w-[460px] bg-white rounded-xl shadow-lg border border-surface">
<div class="px-5 py-3 border-b border-surface flex items-center justify-between">
<h3 class="text-base font-semibold" x-text="districtModal.mode==='add' ? '新增城区' : '修改城区'"></h3>
<button class="p-1 rounded hover:bg-neutral-100" @click="closeDistrictModal()" aria-label="关闭">
<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="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="px-5 py-4 space-y-4">
<div>
<label class="block text-sm mb-1"><span class="text-danger-600 mr-0.5">*</span>城区名称</label>
<input data-test="input-district-name" x-model.trim="districtModal.form.name" type="text" class="w-full px-3 py-2 rounded-md border border-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40" placeholder="请输入城区名称" />
<p class="mt-1 text-xs text-danger-600" x-show="districtModal.errors.name" x-text="districtModal.errors.name"></p>
</div>
<div>
<label class="block text-sm mb-1">城区简称</label>
<input x-model.trim="districtModal.form.shortName" type="text" class="w-full px-3 py-2 rounded-md border border-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40" placeholder="请输入城区简称(选填)" />
</div>
</div>
<div class="px-5 py-3 border-t border-surface flex items-center justify-end gap-2">
<button class="px-3 py-1.5 rounded-md border border-surface hover:bg-neutral-50" @click="closeDistrictModal()">取消</button>
<button data-test="btn-save-district" class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveDistrict()" x-text="districtModal.mode==='add' ? '确认新增' : '确认修改'"></button>
</div>
</div>
</div>
<!-- Biz Add/Edit Modal -->
<div x-show="bizModal.open" x-cloak class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-neutral-900/40" @click="closeBizModal()"></div>
<div class="relative w-[500px] bg-white rounded-xl shadow-lg border border-surface">
<div class="px-5 py-3 border-b border-surface flex items-center justify-between">
<h3 class="text-base font-semibold" x-text="bizModal.mode==='add' ? '新增商圈' : '修改商圈'"></h3>
<button class="p-1 rounded hover:bg-neutral-100" @click="closeBizModal()" aria-label="关闭">
<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="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="px-5 py-4 space-y-4">
<div>
<label class="block text-sm mb-1"><span class="text-danger-600 mr-0.5">*</span>所属城区</label>
<select x-model="bizModal.form.district" class="w-full px-3 py-2 rounded-md border border-surface bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40">
<option value="">请选择所属城区</option>
<template x-for="name in allDistrictNames" :key="'district-option-' + name">
<option :value="name" x-text="name"></option>
</template>
</select>
<p class="mt-1 text-xs text-danger-600" x-show="bizModal.errors.district" x-text="bizModal.errors.district"></p>
</div>
<div>
<label class="block text-sm mb-1"><span class="text-danger-600 mr-0.5">*</span>商圈名称</label>
<input data-test="input-biz-name" x-model.trim="bizModal.form.name" type="text" class="w-full px-3 py-2 rounded-md border border-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40" placeholder="请输入商圈名称" />
<p class="mt-1 text-xs text-danger-600" x-show="bizModal.errors.name" x-text="bizModal.errors.name"></p>
</div>
</div>
<div class="px-5 py-3 border-t border-surface flex items-center justify-end gap-2">
<button class="px-3 py-1.5 rounded-md border border-surface hover:bg-neutral-50" @click="closeBizModal()">取消</button>
<button data-test="btn-save-biz" class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveBiz()" x-text="bizModal.mode==='add' ? '确认新增' : '确认修改'"></button>
</div>
</div>
</div>
<!-- Coordinate Modal -->
<div x-show="coordModal.open" x-cloak class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-neutral-900/40" @click="closeCoordModal()"></div>
<div class="relative w-[500px] bg-white rounded-xl shadow-lg border border-surface">
<div class="px-5 py-3 border-b border-surface flex items-center justify-between">
<h3 class="text-base font-semibold" x-text="coordModal.title"></h3>
<button class="p-1 rounded hover:bg-neutral-100" @click="closeCoordModal()" aria-label="关闭">
<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="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="px-5 py-4 space-y-4">
<div>
<label class="block text-sm mb-1">纬度</label>
<input x-model.trim="coordModal.form.lat" type="text" class="w-full px-3 py-2 rounded-md border border-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40" placeholder="例如31.264564" />
</div>
<div>
<label class="block text-sm mb-1">经度</label>
<input x-model.trim="coordModal.form.lng" type="text" class="w-full px-3 py-2 rounded-md border border-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40" placeholder="例如121.376238" />
</div>
<p class="text-xs text-danger-600" x-show="coordModal.errors.coord" x-text="coordModal.errors.coord"></p>
</div>
<div class="px-5 py-3 border-t border-surface flex items-center justify-end gap-2">
<button class="px-3 py-1.5 rounded-md border border-surface hover:bg-neutral-50" @click="closeCoordModal()">取消</button>
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveCoord()">确认保存</button>
</div>
</div>
</div>
<!-- Relation Modal -->
<div x-show="relationModal.open" x-cloak class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-neutral-900/40" @click="closeRelationModal()"></div>
<div class="relative w-[1160px] max-h-[86vh] overflow-hidden bg-white rounded-xl shadow-lg border border-surface flex flex-col">
<div class="px-5 py-3 border-b border-surface flex items-center justify-between">
<h3 class="text-base font-semibold">查看关联情况</h3>
<button class="p-1 rounded hover:bg-neutral-100" @click="closeRelationModal()" aria-label="关闭">
<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="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<div class="p-5 space-y-4 overflow-auto">
<div class="grid grid-cols-12 gap-3 items-end">
<div class="col-span-4">
<label class="block text-xs text-muted mb-1">标准区域</label>
<select x-model="relationModal.filters.standardRegion" class="w-full px-3 py-2 rounded-md border border-surface bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40">
<option value="">请选择</option>
<template x-for="option in relationStandardOptions" :key="'std-' + option">
<option :value="option" x-text="option"></option>
</template>
</select>
</div>
<div class="col-span-4">
<label class="block text-xs text-muted mb-1">本地区域</label>
<select x-model="relationModal.filters.localRegion" class="w-full px-3 py-2 rounded-md border border-surface bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40">
<option value="">请选择</option>
<template x-for="option in relationLocalOptions" :key="'local-' + option">
<option :value="option" x-text="option"></option>
</template>
</select>
</div>
<div class="col-span-4 flex items-center gap-2">
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="relationModal.tick += 1">查询</button>
<button class="px-3 py-2 rounded-md border border-surface hover:bg-neutral-50" @click="resetRelationFilters()">重置</button>
</div>
</div>
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-2">
<button class="px-3 py-1.5 rounded-md border border-surface" :disabled="selectedRelationCount===0" :class="selectedRelationCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="notify('已触发批量修改(原型)')">批量修改</button>
<span class="text-xs text-muted" x-text="'已选本页 ' + selectedRelationCount + ' 条'"></span>
</div>
<span class="text-xs text-muted" x-text="relationModal.currentBiz ? ('当前商圈:' + relationModal.currentBiz) : ''"></span>
</div>
<div class="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="relationAllOnPageSelected" @click.prevent="toggleRelationPageSelect(!relationAllOnPageSelected)" 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>
</tr>
</thead>
<tbody>
<template x-if="relationFilteredRows.length===0">
<tr><td colspan="7" class="px-4 py-10 text-center text-muted">暂无匹配数据</td></tr>
</template>
<template x-for="row in relationFilteredRows" :key="'relation-row-' + row.id">
<tr class="border-b border-surface hover:bg-neutral-50/50">
<td class="px-3 py-2"><input type="checkbox" class="rounded border-surface" :checked="isRelationSelected(row.id)" @click.prevent="toggleRelationSelect(row.id, !isRelationSelected(row.id))" aria-label="选择关联项" /></td>
<td class="px-3 py-2" x-text="row.stdCity"></td>
<td class="px-3 py-2" x-text="row.stdDistrict"></td>
<td class="px-3 py-2" x-text="row.stdBiz"></td>
<td class="px-3 py-2" x-text="row.localBiz"></td>
<td class="px-3 py-2" x-text="row.localDistrict"></td>
<td class="px-3 py-2"><button class="action-link" @click="notify('已打开关联变更(原型)')">变更</button></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
<div class="px-5 py-3 border-t border-surface flex items-center justify-between text-xs">
<p class="text-muted" x-text="'共 ' + relationFilteredRows.length + ' 条'"></p>
<button class="px-3 py-1.5 rounded-md border border-surface hover:bg-neutral-50" @click="closeRelationModal()">关闭</button>
</div>
</div>
</div>
<!-- Toast -->
<div x-show="toast.show" x-cloak class="fixed bottom-5 right-5 z-[70] px-4 py-2 rounded-md text-sm text-white shadow-lg" :class="toast.type==='success' ? 'bg-success-600' : 'bg-danger-600'" x-text="toast.message"></div>
<script>
function regionManagePage() {
return {
activeSubTab: 'district',
coordOptions: ['不限', '有坐标', '无坐标'],
regionOptions: ['不限', '宝山', '崇明', '奉贤', '黄浦', '虹口', '嘉定', '静安', '金山', '卢湾', '闵行', '南汇', '普陀', '浦东', '青浦', '松江', '上海周边', '徐汇', '杨浦', '长宁', '闸北'],
districtRows: [],
bizRows: [],
districtFilters: { keyword: '', coord: '不限' },
bizFilters: { keyword: '', region: '不限', coord: '不限' },
districtPage: 1,
bizPage: 1,
districtJumpPage: 1,
bizJumpPage: 1,
pageSize: 20,
selectedDistrictIds: [],
selectedBizIds: [],
districtModal: {
open: false,
mode: 'add',
form: { id: null, name: '', shortName: '' },
errors: {}
},
bizModal: {
open: false,
mode: 'add',
form: { id: null, district: '', name: '' },
errors: {}
},
coordModal: {
open: false,
type: 'district',
targetId: null,
title: '设置坐标',
form: { lat: '', lng: '' },
errors: {}
},
relationModal: {
open: false,
currentBiz: '',
filters: { standardRegion: '', localRegion: '' },
tick: 0
},
relationRows: [],
selectedRelationIds: [],
relationStandardOptions: ['上海-徐汇-徐家汇', '上海-徐汇-龙华', '上海-上海周边-南通', '上海-闵行-虹桥'],
relationLocalOptions: ['上海周边 南通', '上海周边 苏州', '徐汇 徐家汇', '徐汇 龙华', '闵行 虹桥'],
toast: { show: false, message: '', type: 'success' },
init() {
this.districtRows = this.mockDistrictRows();
this.bizRows = this.mockBizRows();
this.relationRows = this.mockRelationRows();
},
mockDistrictRows() {
return [
{ id: 1, name: '闵行', shortName: '闵', bizCount: 18, complexCount: 172, coord: '121.381709 , 31.112813' },
{ id: 2, name: '长宁', shortName: '长', bizCount: 12, complexCount: 144, coord: '121.424624 , 31.220367' },
{ id: 3, name: '卢湾', shortName: '卢', bizCount: 5, complexCount: 63, coord: '121.468127 , 31.218558' },
{ id: 4, name: '黄浦', shortName: '黄', bizCount: 9, complexCount: 103, coord: '121.484420 , 31.231661' },
{ id: 5, name: '杨浦', shortName: '杨', bizCount: 14, complexCount: 126, coord: '121.526000 , 31.259188' },
{ id: 6, name: '嘉定', shortName: '嘉', bizCount: 11, complexCount: 118, coord: '121.265276 , 31.374724' },
{ id: 7, name: '普陀', shortName: '普', bizCount: 10, complexCount: 96, coord: '121.392499 , 31.241701' },
{ id: 8, name: '奉贤', shortName: '奉', bizCount: 4, complexCount: 28, coord: '' },
{ id: 9, name: '崇明', shortName: '崇', bizCount: 3, complexCount: 15, coord: '' },
{ id: 10, name: '松江', shortName: '松', bizCount: 16, complexCount: 139, coord: '121.228929 , 31.032243' },
{ id: 11, name: '徐汇', shortName: '徐', bizCount: 22, complexCount: 208, coord: '121.437520 , 31.179973' },
{ id: 12, name: '静安', shortName: '静', bizCount: 13, complexCount: 112, coord: '121.447348 , 31.227901' },
{ id: 13, name: '虹口', shortName: '虹', bizCount: 8, complexCount: 75, coord: '121.505133 , 31.264600' },
{ id: 14, name: '浦东', shortName: '浦', bizCount: 26, complexCount: 320, coord: '121.567706 , 31.245944' },
{ id: 15, name: '闸北', shortName: '闸', bizCount: 6, complexCount: 44, coord: '' },
{ id: 16, name: '宝山', shortName: '宝', bizCount: 9, complexCount: 67, coord: '121.489934 , 31.405457' },
{ id: 17, name: '青浦', shortName: '青', bizCount: 7, complexCount: 52, coord: '' },
{ id: 18, name: '南汇', shortName: '南', bizCount: 2, complexCount: 11, coord: '' },
{ id: 19, name: '金山', shortName: '金', bizCount: 4, complexCount: 20, coord: '' },
{ id: 20, name: '上海周边', shortName: '周边', bizCount: 7, complexCount: 38, coord: '121.473700 , 31.230400' },
{ id: 21, name: '新城区样例', shortName: '新', bizCount: 0, complexCount: 0, coord: '' }
];
},
mockBizRows() {
return [
{ id: 101, district: '上海周边', name: '南通', isStandard: true, complexCount: 0, coord: '121.662033 , 31.799047' },
{ id: 102, district: '上海周边', name: '嘉兴', isStandard: true, complexCount: 0, coord: '120.755486 , 30.746129' },
{ id: 103, district: '上海周边', name: '昆山', isStandard: true, complexCount: 0, coord: '120.980000 , 31.380000' },
{ id: 104, district: '上海周边', name: '苏州', isStandard: true, complexCount: 1, coord: '120.619585 , 31.299379' },
{ id: 105, district: '上海周边', name: '慈溪', isStandard: true, complexCount: 0, coord: '' },
{ id: 106, district: '上海周边', name: '湖州', isStandard: true, complexCount: 0, coord: '' },
{ id: 107, district: '徐汇', name: '徐家汇', isStandard: true, complexCount: 36, coord: '121.436770 , 31.188350' },
{ id: 108, district: '徐汇', name: '龙华', isStandard: true, complexCount: 29, coord: '121.453737 , 31.170235' },
{ id: 109, district: '徐汇', name: '湖南路', isStandard: true, complexCount: 18, coord: '' },
{ id: 110, district: '徐汇', name: '上海南站', isStandard: true, complexCount: 14, coord: '121.430300 , 31.157300' },
{ id: 111, district: '徐汇', name: '万体馆', isStandard: true, complexCount: 12, coord: '' },
{ id: 112, district: '徐汇', name: '华东理工', isStandard: true, complexCount: 10, coord: '' },
{ id: 113, district: '闵行', name: '虹桥', isStandard: true, complexCount: 20, coord: '121.326996 , 31.200000' },
{ id: 114, district: '闵行', name: '古美', isStandard: true, complexCount: 16, coord: '' },
{ id: 115, district: '普陀', name: '真如', isStandard: true, complexCount: 11, coord: '' },
{ id: 116, district: '普陀', name: '真光', isStandard: true, complexCount: 21, coord: '121.392400 , 31.240600' },
{ id: 117, district: '嘉定', name: '江桥', isStandard: true, complexCount: 19, coord: '' },
{ id: 118, district: '嘉定', name: '丰庄', isStandard: true, complexCount: 8, coord: '' },
{ id: 119, district: '浦东', name: '川沙', isStandard: true, complexCount: 26, coord: '121.700000 , 31.190000' },
{ id: 120, district: '浦东', name: '三林', isStandard: true, complexCount: 34, coord: '' },
{ id: 121, district: '静安', name: '大宁', isStandard: true, complexCount: 13, coord: '' },
{ id: 122, district: '长宁', name: '中山公园', isStandard: true, complexCount: 17, coord: '' },
{ id: 123, district: '黄浦', name: '人民广场', isStandard: true, complexCount: 9, coord: '' },
{ id: 124, district: '宝山', name: '淞南', isStandard: true, complexCount: 7, coord: '' }
];
},
mockRelationRows() {
return [
{ id: 1, stdCity: '上海', stdDistrict: '上海周边', stdBiz: '南通', localBiz: '南通', localDistrict: '上海周边' },
{ id: 2, stdCity: '上海', stdDistrict: '上海周边', stdBiz: '苏州', localBiz: '苏州', localDistrict: '上海周边' },
{ id: 3, stdCity: '上海', stdDistrict: '徐汇', stdBiz: '徐家汇', localBiz: '徐家汇', localDistrict: '徐汇' },
{ id: 4, stdCity: '上海', stdDistrict: '徐汇', stdBiz: '龙华', localBiz: '龙华', localDistrict: '徐汇' }
];
},
switchSubTab(key) {
this.activeSubTab = key;
if (key === 'district') {
this.selectedBizIds = [];
} else {
this.selectedDistrictIds = [];
}
},
// ---------- District ----------
get districtFilteredRows() {
const keyword = (this.districtFilters.keyword || '').toLowerCase();
return this.districtRows.filter((row) => {
const hitKeyword = !keyword || row.name.toLowerCase().includes(keyword);
const hitCoord = this.districtFilters.coord === '不限'
|| (this.districtFilters.coord === '有坐标' && !!row.coord)
|| (this.districtFilters.coord === '无坐标' && !row.coord);
return hitKeyword && hitCoord;
});
},
get districtTotalPages() {
return Math.max(1, Math.ceil(this.districtFilteredRows.length / this.pageSize));
},
get districtPaginatedRows() {
const page = Math.min(this.districtPage, this.districtTotalPages);
const start = (page - 1) * this.pageSize;
return this.districtFilteredRows.slice(start, start + this.pageSize);
},
get selectedDistrictCount() {
const ids = this.districtPaginatedRows.map(r => r.id);
return this.selectedDistrictIds.filter(id => ids.includes(id)).length;
},
isDistrictSelected(id) {
return this.selectedDistrictIds.includes(id);
},
toggleDistrictSelect(id, checked) {
if (checked) {
if (!this.selectedDistrictIds.includes(id)) this.selectedDistrictIds.push(id);
} else {
this.selectedDistrictIds = this.selectedDistrictIds.filter(x => x !== id);
}
},
get districtAllOnPageSelected() {
if (this.districtPaginatedRows.length === 0) return false;
return this.districtPaginatedRows.every(r => this.selectedDistrictIds.includes(r.id));
},
toggleDistrictPageSelect(checked) {
const ids = this.districtPaginatedRows.map(r => r.id);
if (checked) {
this.selectedDistrictIds = [...new Set([...this.selectedDistrictIds, ...ids])];
} else {
this.selectedDistrictIds = this.selectedDistrictIds.filter(id => !ids.includes(id));
}
},
applyDistrictFilters() {
this.districtPage = 1;
this.districtJumpPage = 1;
this.selectedDistrictIds = [];
},
resetDistrictFilters() {
this.districtFilters = { keyword: '', coord: '不限' };
this.districtPage = 1;
this.districtJumpPage = 1;
this.selectedDistrictIds = [];
},
districtPrevPage() {
if (this.districtPage > 1) this.districtPage -= 1;
this.districtJumpPage = this.districtPage;
this.selectedDistrictIds = [];
},
districtNextPage() {
if (this.districtPage < this.districtTotalPages) this.districtPage += 1;
this.districtJumpPage = this.districtPage;
this.selectedDistrictIds = [];
},
districtGoPage() {
let p = Number(this.districtJumpPage || 1);
if (!Number.isFinite(p)) p = 1;
p = Math.max(1, Math.min(this.districtTotalPages, Math.floor(p)));
this.districtPage = p;
this.districtJumpPage = p;
this.selectedDistrictIds = [];
},
// ---------- Biz ----------
get bizFilteredRows() {
const keyword = (this.bizFilters.keyword || '').toLowerCase();
return this.bizRows.filter((row) => {
const hitKeyword = !keyword || row.name.toLowerCase().includes(keyword);
const hitRegion = this.bizFilters.region === '不限' || row.district === this.bizFilters.region;
const hitCoord = this.bizFilters.coord === '不限'
|| (this.bizFilters.coord === '有坐标' && !!row.coord)
|| (this.bizFilters.coord === '无坐标' && !row.coord);
return hitKeyword && hitRegion && hitCoord;
});
},
get bizTotalPages() {
return Math.max(1, Math.ceil(this.bizFilteredRows.length / this.pageSize));
},
get bizPaginatedRows() {
const page = Math.min(this.bizPage, this.bizTotalPages);
const start = (page - 1) * this.pageSize;
return this.bizFilteredRows.slice(start, start + this.pageSize);
},
get selectedBizCount() {
const ids = this.bizPaginatedRows.map(r => r.id);
return this.selectedBizIds.filter(id => ids.includes(id)).length;
},
isBizSelected(id) {
return this.selectedBizIds.includes(id);
},
toggleBizSelect(id, checked) {
if (checked) {
if (!this.selectedBizIds.includes(id)) this.selectedBizIds.push(id);
} else {
this.selectedBizIds = this.selectedBizIds.filter(x => x !== id);
}
},
get bizAllOnPageSelected() {
if (this.bizPaginatedRows.length === 0) return false;
return this.bizPaginatedRows.every(r => this.selectedBizIds.includes(r.id));
},
toggleBizPageSelect(checked) {
const ids = this.bizPaginatedRows.map(r => r.id);
if (checked) {
this.selectedBizIds = [...new Set([...this.selectedBizIds, ...ids])];
} else {
this.selectedBizIds = this.selectedBizIds.filter(id => !ids.includes(id));
}
},
applyBizFilters() {
this.bizPage = 1;
this.bizJumpPage = 1;
this.selectedBizIds = [];
},
resetBizFilters() {
this.bizFilters = { keyword: '', region: '不限', coord: '不限' };
this.bizPage = 1;
this.bizJumpPage = 1;
this.selectedBizIds = [];
},
bizPrevPage() {
if (this.bizPage > 1) this.bizPage -= 1;
this.bizJumpPage = this.bizPage;
this.selectedBizIds = [];
},
bizNextPage() {
if (this.bizPage < this.bizTotalPages) this.bizPage += 1;
this.bizJumpPage = this.bizPage;
this.selectedBizIds = [];
},
bizGoPage() {
let p = Number(this.bizJumpPage || 1);
if (!Number.isFinite(p)) p = 1;
p = Math.max(1, Math.min(this.bizTotalPages, Math.floor(p)));
this.bizPage = p;
this.bizJumpPage = p;
this.selectedBizIds = [];
},
get allDistrictNames() {
return [...new Set(this.districtRows.map(r => r.name))];
},
// ---------- Modals ----------
openDistrictModal(mode, row = null) {
this.districtModal.open = true;
this.districtModal.mode = mode;
this.districtModal.errors = {};
this.districtModal.form = row
? { id: row.id, name: row.name, shortName: row.shortName || '' }
: { id: null, name: '', shortName: '' };
},
closeDistrictModal() {
this.districtModal.open = false;
this.districtModal.errors = {};
},
saveDistrict() {
this.districtModal.errors = {};
if (!this.districtModal.form.name) {
this.districtModal.errors.name = '请输入城区名称';
return;
}
if (this.districtModal.mode === 'add') {
const nextId = Math.max(...this.districtRows.map(r => r.id)) + 1;
this.districtRows.unshift({
id: nextId,
name: this.districtModal.form.name,
shortName: this.districtModal.form.shortName,
bizCount: 0,
complexCount: 0,
coord: ''
});
this.notify('新增城区成功(原型)');
} else {
this.districtRows = this.districtRows.map((row) => row.id === this.districtModal.form.id
? { ...row, name: this.districtModal.form.name, shortName: this.districtModal.form.shortName }
: row);
this.bizRows = this.bizRows.map((row) => row.district === this.findDistrictNameById(this.districtModal.form.id)
? { ...row, district: this.districtModal.form.name }
: row);
this.notify('修改城区成功(原型)');
}
this.closeDistrictModal();
},
findDistrictNameById(id) {
const found = this.districtRows.find(r => r.id === id);
return found ? found.name : '';
},
openBizModal(mode, row = null) {
this.bizModal.open = true;
this.bizModal.mode = mode;
this.bizModal.errors = {};
this.bizModal.form = row
? { id: row.id, district: row.district, name: row.name }
: { id: null, district: '', name: '' };
},
closeBizModal() {
this.bizModal.open = false;
this.bizModal.errors = {};
},
saveBiz() {
this.bizModal.errors = {};
if (!this.bizModal.form.district) {
this.bizModal.errors.district = '请选择所属城区';
}
if (!this.bizModal.form.name) {
this.bizModal.errors.name = '请输入商圈名称';
}
if (Object.keys(this.bizModal.errors).length > 0) return;
if (this.bizModal.mode === 'add') {
const nextId = Math.max(...this.bizRows.map(r => r.id)) + 1;
this.bizRows.unshift({
id: nextId,
district: this.bizModal.form.district,
name: this.bizModal.form.name,
isStandard: true,
complexCount: 0,
coord: ''
});
this.notify('新增商圈成功(原型)');
} else {
this.bizRows = this.bizRows.map((row) => row.id === this.bizModal.form.id
? { ...row, district: this.bizModal.form.district, name: this.bizModal.form.name }
: row);
this.notify('修改商圈成功(原型)');
}
this.closeBizModal();
},
openCoordModal(type, row) {
let lat = '';
let lng = '';
if (row.coord && row.coord.includes(',')) {
const parts = row.coord.split(',').map(v => v.trim());
if (parts.length >= 2) {
lng = parts[0];
lat = parts[1];
}
}
this.coordModal = {
open: true,
type,
targetId: row.id,
title: `设置坐标 - ${type === 'district' ? '城区:' : '商圈:'}${row.name}`,
form: { lat, lng },
errors: {}
};
},
closeCoordModal() {
this.coordModal.open = false;
this.coordModal.errors = {};
},
saveCoord() {
this.coordModal.errors = {};
if (!this.coordModal.form.lat || !this.coordModal.form.lng) {
this.coordModal.errors.coord = '请完整填写经纬度';
return;
}
const coordText = `${this.coordModal.form.lng} , ${this.coordModal.form.lat}`;
if (this.coordModal.type === 'district') {
this.districtRows = this.districtRows.map((row) => row.id === this.coordModal.targetId
? { ...row, coord: coordText }
: row);
} else {
this.bizRows = this.bizRows.map((row) => row.id === this.coordModal.targetId
? { ...row, coord: coordText }
: row);
}
this.closeCoordModal();
this.notify('坐标保存成功(原型)');
},
openRelationModal(row) {
this.relationModal.open = true;
this.relationModal.currentBiz = row.name;
this.relationModal.filters = {
standardRegion: '',
localRegion: row.district + ' ' + row.name
};
this.selectedRelationIds = [];
},
closeRelationModal() {
this.relationModal.open = false;
this.selectedRelationIds = [];
},
resetRelationFilters() {
this.relationModal.filters = { standardRegion: '', localRegion: '' };
this.selectedRelationIds = [];
},
get relationFilteredRows() {
const std = this.relationModal.filters.standardRegion;
const local = this.relationModal.filters.localRegion;
return this.relationRows.filter((row) => {
const stdKey = `${row.stdCity}-${row.stdDistrict}-${row.stdBiz}`;
const localKey = `${row.localDistrict} ${row.localBiz}`;
const hitStd = !std || stdKey === std;
const hitLocal = !local || localKey === local;
const hitCurrentBiz = !this.relationModal.currentBiz || row.localBiz === this.relationModal.currentBiz || !local;
return hitStd && hitLocal && hitCurrentBiz;
});
},
isRelationSelected(id) {
return this.selectedRelationIds.includes(id);
},
toggleRelationSelect(id, checked) {
if (checked) {
if (!this.selectedRelationIds.includes(id)) this.selectedRelationIds.push(id);
} else {
this.selectedRelationIds = this.selectedRelationIds.filter(x => x !== id);
}
},
get relationAllOnPageSelected() {
if (this.relationFilteredRows.length === 0) return false;
return this.relationFilteredRows.every(r => this.selectedRelationIds.includes(r.id));
},
toggleRelationPageSelect(checked) {
const ids = this.relationFilteredRows.map(r => r.id);
if (checked) {
this.selectedRelationIds = [...new Set([...this.selectedRelationIds, ...ids])];
} else {
this.selectedRelationIds = this.selectedRelationIds.filter(id => !ids.includes(id));
}
},
get selectedRelationCount() {
return this.selectedRelationIds.length;
},
closeAllModals() {
this.closeDistrictModal();
this.closeBizModal();
this.closeCoordModal();
this.closeRelationModal();
},
notify(message, type = 'success') {
this.toast = { show: true, message, type };
window.setTimeout(() => {
this.toast.show = false;
}, 1800);
}
}
}
</script>
</body>
</html>