|
|
@@ -2,56 +2,193 @@
|
|
|
<div class="sidebar" :class="{ 'sidebar--hidden': !visible }">
|
|
|
<div class="sidebar-hide-btn" @click="toggle" />
|
|
|
|
|
|
- <img draggable="false" class="sidebar-logo" src="@/assets/images/logo.png" alt="logo" />
|
|
|
+ <img
|
|
|
+ draggable="false"
|
|
|
+ class="sidebar-logo"
|
|
|
+ src="@/assets/images/logo.png"
|
|
|
+ alt="logo"
|
|
|
+ />
|
|
|
|
|
|
<div class="sidebar-search">
|
|
|
- <input name="search" placeholder="请搜索建筑名称" />
|
|
|
+ <input
|
|
|
+ v-model="searchKeyword"
|
|
|
+ name="search"
|
|
|
+ placeholder="请搜索建筑名称"
|
|
|
+ />
|
|
|
<i class="sidebar-search-icon" />
|
|
|
</div>
|
|
|
|
|
|
- <el-tabs v-model="activeTab" class="sidebar-tabs" stretch @tab-change="handleTabChange">
|
|
|
+ <el-tabs
|
|
|
+ v-model="activeTab"
|
|
|
+ class="sidebar-tabs"
|
|
|
+ stretch
|
|
|
+ @tab-change="handleTabChange"
|
|
|
+ >
|
|
|
<el-tab-pane label="区域划分" name="area">
|
|
|
<el-scrollbar>
|
|
|
- <el-collapse v-for="item in 10" :key="item" class="sidebar-collapse">
|
|
|
- <el-collapse-item title="区域1" name="area1">
|
|
|
+ <div v-if="!areaGroups.length" class="sidebar-empty">
|
|
|
+ {{ searchKeyword ? "无匹配结果" : "暂无数据" }}
|
|
|
+ </div>
|
|
|
+ <el-collapse v-else class="sidebar-collapse">
|
|
|
+ <el-collapse-item
|
|
|
+ v-for="(group, idx) in areaGroups"
|
|
|
+ :key="'area-' + group.key"
|
|
|
+ :name="'area-' + group.key"
|
|
|
+ >
|
|
|
+ <template #title>
|
|
|
+ <span class="collapse-header">
|
|
|
+ <span class="tag">{{ group.items.length }}</span>
|
|
|
+ <span class="collapse-title">{{ group.key }}</span>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <template #icon="{ isActive }">
|
|
|
+ <span class="icon" :class="{ 'icon--active': isActive }" />
|
|
|
+ </template>
|
|
|
+ <ul>
|
|
|
+ <li
|
|
|
+ v-for="item in group.items"
|
|
|
+ :key="item.id"
|
|
|
+ :class="{ active: isItemActive(item) }"
|
|
|
+ @click="handleItemClick(item)"
|
|
|
+ >
|
|
|
+ ·{{ item.name }}
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ </el-collapse-item>
|
|
|
+ </el-collapse>
|
|
|
+ </el-scrollbar>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="类型划分" name="type">
|
|
|
+ <el-scrollbar>
|
|
|
+ <div v-if="!typeGroups.length" class="sidebar-empty">
|
|
|
+ {{ searchKeyword ? "无匹配结果" : "暂无数据" }}
|
|
|
+ </div>
|
|
|
+ <el-collapse v-else class="sidebar-collapse">
|
|
|
+ <el-collapse-item
|
|
|
+ v-for="(group, idx) in typeGroups"
|
|
|
+ :key="'type-' + group.key"
|
|
|
+ :name="'type-' + group.key"
|
|
|
+ >
|
|
|
+ <template #title>
|
|
|
+ <span class="collapse-header">
|
|
|
+ <span class="tag">{{ group.items.length }}</span>
|
|
|
+ <span class="collapse-title">{{ group.key }}</span>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
<template #icon="{ isActive }">
|
|
|
- <span class="tag">20</span>
|
|
|
<span class="icon" :class="{ 'icon--active': isActive }" />
|
|
|
</template>
|
|
|
-
|
|
|
<ul>
|
|
|
- <li>·建筑1</li>
|
|
|
- <li>·建筑2</li>
|
|
|
- <li>·建筑3</li>
|
|
|
+ <li
|
|
|
+ v-for="item in group.items"
|
|
|
+ :key="item.id"
|
|
|
+ :class="{ active: isItemActive(item) }"
|
|
|
+ @click="handleItemClick(item)"
|
|
|
+ >
|
|
|
+ ·{{ item.name }}
|
|
|
+ </li>
|
|
|
</ul>
|
|
|
</el-collapse-item>
|
|
|
</el-collapse>
|
|
|
</el-scrollbar>
|
|
|
</el-tab-pane>
|
|
|
- <el-tab-pane label="类型划分" name="type" />
|
|
|
</el-tabs>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref } from 'vue'
|
|
|
-import { storeToRefs } from 'pinia'
|
|
|
-import { useSidebarStore } from '@/stores/sidebar'
|
|
|
+import { ref, computed, onMounted } from "vue";
|
|
|
+import { useRouter, useRoute } from "vue-router";
|
|
|
+import { storeToRefs } from "pinia";
|
|
|
+import { useSidebarStore } from "@/stores/sidebar";
|
|
|
+
|
|
|
+const emit = defineEmits(["select"]);
|
|
|
+const router = useRouter();
|
|
|
+const route = useRoute();
|
|
|
+const { visible } = storeToRefs(useSidebarStore());
|
|
|
+const { toggle } = useSidebarStore();
|
|
|
+
|
|
|
+const searchKeyword = ref("");
|
|
|
+const activeTab = ref("area");
|
|
|
+const rawData = ref([]);
|
|
|
|
|
|
-const { visible } = storeToRefs(useSidebarStore())
|
|
|
-const { toggle } = useSidebarStore()
|
|
|
+onMounted(async () => {
|
|
|
+ try {
|
|
|
+ const res = await fetch("/data.json");
|
|
|
+ rawData.value = await res.json();
|
|
|
+ } catch (e) {
|
|
|
+ console.error("加载 data.json 失败:", e);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+/** 根据搜索关键词过滤后的数据 */
|
|
|
+const filteredData = computed(() => {
|
|
|
+ const list = rawData.value || [];
|
|
|
+ const kw = (searchKeyword.value || "").trim().toLowerCase();
|
|
|
+ if (!kw) return list;
|
|
|
+ return list.filter((item) => (item.name || "").toLowerCase().includes(kw));
|
|
|
+});
|
|
|
|
|
|
-const search = ref('')
|
|
|
-const activeTab = ref('area')
|
|
|
+/** 按 area 分组,保持数据顺序 */
|
|
|
+const areaGroups = computed(() => {
|
|
|
+ const map = new Map();
|
|
|
+ for (const item of filteredData.value) {
|
|
|
+ const key = item.area || "未分类";
|
|
|
+ if (!map.has(key)) map.set(key, []);
|
|
|
+ map.get(key).push(item);
|
|
|
+ }
|
|
|
+ return Array.from(map.entries()).map(([key, items]) => ({ key, items }));
|
|
|
+});
|
|
|
+
|
|
|
+/** 按 type 分组,保持数据顺序 */
|
|
|
+const typeGroups = computed(() => {
|
|
|
+ const map = new Map();
|
|
|
+ for (const item of filteredData.value) {
|
|
|
+ const key = item.type || "未分类";
|
|
|
+ if (!map.has(key)) map.set(key, []);
|
|
|
+ map.get(key).push(item);
|
|
|
+ }
|
|
|
+ return Array.from(map.entries()).map(([key, items]) => ({ key, items }));
|
|
|
+});
|
|
|
+
|
|
|
+/** 当前选中的 item id(图文模式用 id,地图模式用 name 匹配) */
|
|
|
+const activeItemId = computed(() => {
|
|
|
+ if (route.name === "Picture" && route.query.id) return route.query.id;
|
|
|
+ if (route.name === "MapView" && route.query.name) {
|
|
|
+ const found = rawData.value.find(
|
|
|
+ (i) => (i.name || "").trim() === String(route.query.name).trim(),
|
|
|
+ );
|
|
|
+ return found?.id;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+});
|
|
|
+
|
|
|
+const isItemActive = (item) => item?.id === activeItemId.value;
|
|
|
|
|
|
const handleTabChange = (tab) => {
|
|
|
- console.log(tab)
|
|
|
-}
|
|
|
+ // 可在此处理 tab 切换逻辑
|
|
|
+};
|
|
|
+
|
|
|
+const handleItemClick = (item) => {
|
|
|
+ emit("select", item);
|
|
|
+ // 图文模式页:点击 item 则显示该 item
|
|
|
+ if (route.name === "Picture") {
|
|
|
+ router.replace({ name: "Picture", query: { id: item.id } });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (item.scene) {
|
|
|
+ window.open(item.scene.trim(), "_blank");
|
|
|
+ } else if (item.lng && item.lat) {
|
|
|
+ router.push({
|
|
|
+ name: "MapView",
|
|
|
+ query: { lng: item.lng, lat: item.lat, name: item.name },
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
-
|
|
|
-@use '@/assets/utils.scss';
|
|
|
+@use "@/assets/utils.scss";
|
|
|
|
|
|
.sidebar {
|
|
|
position: absolute;
|
|
|
@@ -62,7 +199,8 @@ const handleTabChange = (tab) => {
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
width: utils.vh-calc(300);
|
|
|
- background: linear-gradient(#D9BF9D, #CFAC7D);
|
|
|
+ background: linear-gradient(rgba(217, 191, 157, 1), rgba(207, 172, 125, 0.8));
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
transition: transform 0.3s ease;
|
|
|
|
|
|
&--hidden {
|
|
|
@@ -74,16 +212,16 @@ const handleTabChange = (tab) => {
|
|
|
}
|
|
|
|
|
|
:deep(.sidebar-tabs) {
|
|
|
- --el-text-color-primary: #4E1A00;
|
|
|
+ --el-text-color-primary: #4e1a00;
|
|
|
--el-font-size-base: utils.vh-calc(20);
|
|
|
- --el-border-color-light: #98382E;
|
|
|
+ --el-border-color-light: #98382e;
|
|
|
margin: utils.vh-calc(25) 0;
|
|
|
width: 100%;
|
|
|
height: 0;
|
|
|
flex: 1;
|
|
|
|
|
|
.el-tabs__item.is-active {
|
|
|
- font-family: 'SourceHanSerifSC-Bold';
|
|
|
+ font-family: "SourceHanSerifSC-Bold";
|
|
|
}
|
|
|
.el-tabs__nav-wrap::after {
|
|
|
height: 1px;
|
|
|
@@ -105,10 +243,17 @@ const handleTabChange = (tab) => {
|
|
|
height: 100%;
|
|
|
}
|
|
|
}
|
|
|
+ .sidebar-empty {
|
|
|
+ padding: utils.vh-calc(30);
|
|
|
+ text-align: center;
|
|
|
+ color: #4e1a00;
|
|
|
+ font-size: utils.vh-calc(16);
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
:deep(.sidebar-collapse) {
|
|
|
--el-fill-color-blank: transparent;
|
|
|
--el-collapse-border-color: transparent;
|
|
|
- --el-collapse-header-text-color: #4E1A00;
|
|
|
+ --el-collapse-header-text-color: #4e1a00;
|
|
|
|
|
|
.el-collapse-item__header {
|
|
|
padding-left: utils.vh-calc(5);
|
|
|
@@ -121,21 +266,37 @@ const handleTabChange = (tab) => {
|
|
|
padding-bottom: 0;
|
|
|
}
|
|
|
.tag {
|
|
|
- width: utils.vh-calc(28);
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-width: utils.vh-calc(28);
|
|
|
height: utils.vh-calc(18);
|
|
|
- line-height: utils.vh-calc(18);
|
|
|
- text-align: center;
|
|
|
- color: #F8D561;
|
|
|
+ padding: 0 utils.vh-calc(6);
|
|
|
+ color: #f8d561;
|
|
|
font-size: utils.vh-calc(14);
|
|
|
border-radius: 100px;
|
|
|
- background: #98382E;
|
|
|
+ background: #98382e;
|
|
|
+ }
|
|
|
+ .collapse-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+ .collapse-title {
|
|
|
+ margin-left: utils.vh-calc(8);
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
.icon {
|
|
|
margin-left: utils.vh-calc(12);
|
|
|
width: utils.vh-calc(18);
|
|
|
height: utils.vh-calc(10);
|
|
|
transition: transform 0.3s ease;
|
|
|
- background: url('@/assets/images/icon-xia.png') no-repeat center / contain;
|
|
|
+ background: url("@/assets/images/icon-xia.png") no-repeat center / contain;
|
|
|
|
|
|
&--active {
|
|
|
transform: rotate(180deg);
|
|
|
@@ -148,11 +309,15 @@ const handleTabChange = (tab) => {
|
|
|
height: utils.vh-calc(40);
|
|
|
line-height: utils.vh-calc(40);
|
|
|
font-size: utils.vh-calc(16);
|
|
|
- color: #4E1A00;
|
|
|
+ color: #4e1a00;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover {
|
|
|
- color: #98382E;
|
|
|
+ color: #98382e;
|
|
|
+ }
|
|
|
+ &.active {
|
|
|
+ color: #98382e;
|
|
|
+ font-family: "SourceHanSerifSC-Bold";
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -164,7 +329,7 @@ const handleTabChange = (tab) => {
|
|
|
align-items: center;
|
|
|
width: utils.vh-calc(270);
|
|
|
height: utils.vh-calc(44);
|
|
|
- background: url('./images/search-bg.png') no-repeat center / contain;
|
|
|
+ background: url("./images/search-bg.png") no-repeat center / contain;
|
|
|
|
|
|
input {
|
|
|
flex: 1;
|
|
|
@@ -173,11 +338,11 @@ const handleTabChange = (tab) => {
|
|
|
height: 100%;
|
|
|
background: transparent;
|
|
|
border: none;
|
|
|
- color: #F7E8D4;
|
|
|
+ color: #f7e8d4;
|
|
|
font-size: utils.vh-calc(18);
|
|
|
|
|
|
&::placeholder {
|
|
|
- color: #F7E8D4;
|
|
|
+ color: #f7e8d4;
|
|
|
}
|
|
|
&:focus {
|
|
|
outline: none;
|
|
|
@@ -193,11 +358,11 @@ const handleTabChange = (tab) => {
|
|
|
cursor: pointer;
|
|
|
|
|
|
&::before {
|
|
|
- content: '';
|
|
|
+ content: "";
|
|
|
display: block;
|
|
|
width: utils.vh-calc(24);
|
|
|
height: utils.vh-calc(22);
|
|
|
- background: url('./images/search-icon.png') no-repeat center / contain;
|
|
|
+ background: url("./images/search-icon.png") no-repeat center / contain;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -214,18 +379,19 @@ const handleTabChange = (tab) => {
|
|
|
width: utils.vh-calc(18);
|
|
|
height: utils.vh-calc(70);
|
|
|
cursor: pointer;
|
|
|
- background: url('./images/sidebar-hide-bg.png') no-repeat center / contain;
|
|
|
+ background: url("./images/sidebar-hide-bg.png") no-repeat center / contain;
|
|
|
|
|
|
&::before {
|
|
|
- content: '';
|
|
|
+ content: "";
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
left: 50%;
|
|
|
width: utils.vh-calc(11);
|
|
|
height: utils.vh-calc(18);
|
|
|
transform: translate(-50%, -50%);
|
|
|
- background: url('./images/sidebar-hide-icon.png') no-repeat center / contain;
|
|
|
+ background: url("./images/sidebar-hide-icon.png") no-repeat center /
|
|
|
+ contain;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|