1119 lines
56 KiB
HTML
1119 lines
56 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;
|
||
}
|
||
|
||
.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> |