毕设完毕

This commit is contained in:
lsy 2025-04-12 15:29:10 +08:00
parent 9134f0b67a
commit 671ef9822a
21 changed files with 1267 additions and 711 deletions

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,87 @@
---
title: "阿那亚 - 面朝大海的理想之地"
description: "阿那亚位于河北秦皇岛北戴河新区,是一个集度假、艺术、建筑、人文于一体的滨海社区,以孤独图书馆、礼堂和静谧海岸而闻名,是当代都市人精神栖息的理想场所。"
featured: true
image: "/images/attractions/anaya.jpg"
city: ["秦皇岛"]
tags: ["建筑艺术", "海边", "人文社区", "现代设计", "精神空间"]
pubDate: 2025-04-12
---
阿那亚Anaya地处**河北秦皇岛北戴河新区黄金海岸线**,是近年来迅速走红的一个集“文化、艺术、度假、生活方式”于一体的滨海社区,以其极简主义建筑风格与精神性的空间构建,引发无数都市人的共鸣。
这里没有喧嚣的商业街,也没有打卡式的人潮,而是一个**被大海、沙滩和冥想空间围绕的精神庇护所**。它不仅是一处度假胜地,更像是一场逃离日常的沉思之旅。
---
## 🌊 主要看点
### 📚 孤独图书馆
阿那亚最出圈的地标。面朝大海,三面封闭,仅一侧朝海开放,**设计极简,却极具哲思**。内部书架林立,却不设娱乐功能,是为“孤独而阅读”而生。这里每天都有大量游客慕名前来,却都在安静中体会孤独的价值。
### ⛪ 阿那亚礼堂
又名“海边礼堂”或“阿那亚礼堂”,是为婚礼、冥想、祈福、仪式而设。建筑高耸,线条纯粹,如同从沙滩升起的一束信仰之光。每当清晨或黄昏,阳光洒下,它宛如信仰的容器,安静地耸立在海岸线。
### 🎭 阿那亚戏剧节
每年夏季举办,是中国最具先锋性与思想性的民间戏剧节之一。戏剧不再局限于舞台,**建筑、沙滩、街道、树林都可能成为舞台**。许多艺术家、导演、观众共同在此探索戏剧与生活的边界。
### 🏖️ 黄金海岸
阿那亚所在区域为中国最美的原生态沙滩之一,海岸线笔直,沙质细腻,是游泳、散步、拍照、冥想的绝佳场所。你可以在日出时分看到海边冥想的人群,也可以在夜晚坐在海边,听潮起潮落。
---
## 🏘️ 社区与生活方式
阿那亚并非传统意义上的景区,而是一个**有完整社区运营机制的生活共同体**。它包括:
- 精致的民宿和酒店,如**三联海边小屋、安岚酒店、香柏木居所等**
- 艺术空间、咖啡馆、酒吧、概念书店
- 社区市集、不定期举办的哲学沙龙、读书会、音乐演出
每一处设计都充满**仪式感与精神性**,注重人与空间的关系,提倡**慢生活、内观与思考**。
---
## 🚗 交通信息
- **地址**:河北省秦皇岛市北戴河新区黄金海岸阿那亚社区
- **推荐方式**
- **自驾**从北京出发约3.5小时高速直达
- **高铁+接驳**北京出发乘高铁至“北戴河站”或“秦皇岛站”换乘出租或社区班车约40分钟
- **需预约进入**:非住户或非入住游客需通过官方渠道申请入园权限
---
## 🏨 推荐住宿
| 酒店名称 | 类型 | 特点 |
|------------------|------------|------------------------------|
| 安岚酒店 | 高端酒店 | 海景无边泳池、艺术设计感强 |
| 三联·海边小屋 | 民宿 | 离海近,安静私密 |
| 香柏木居所 | 民宿 | 禅意极简风格,适合冥想和放松 |
| 阿那亚社区公寓 | 长住型住宿 | 空间大,配套生活设施齐全 |
---
## 🎟️ 门票与开放时间
阿那亚不收门票,但**需要预约或住宿登记方可进入社区**,访客数量每日有限。
- **图书馆开放时间**09:00 - 18:00视季节调整
- **礼堂非婚礼时间可自由参观,演出期间谢绝游客**
- **戏剧节时间**每年7月需提前购票抢票难度较高
---
## 🧳 游玩贴士
- 📷 **摄影圣地**:建议早晚拍摄,避开正午强光
- 🔇 **保持安静**:图书馆与礼堂为冥想空间,请勿喧哗
- 📅 **提前预约**:节假日或戏剧节期间需提前数周预约
- 📚 **推荐读物**:《阿那亚日记》《孤独六讲》《岛上书店》为精神调性相契合之选
---
阿那亚不是热闹的景点,而是一个让你愿意放下手机、重新对话内心的地方。在这里,**孤独不是负担,而是一种权利**;在这里,海浪、建筑与自我,共同编织出生活与理想的可能边界。
如果你厌倦了城市的喧嚣,不妨到阿那亚,听听自己的心跳。

View File

@ -0,0 +1,90 @@
---
title: "白石山 - 太行明珠 云端仙境"
description: "白石山位于河北保定涞源县,是太行山脉中罕见的大理岩峰林地貌景区,以险峻山势、奇石林立、悬空栈道和云海佛光著称,被誉为“太行明珠”。"
featured: true
image: "/images/attractions/baishishan.jpg"
city: ["保定"]
tags: ["自然风光", "山岳", "太行山", "5A景区", "地质奇观", "悬空栈道"]
pubDate: 2025-04-12
---
白石山景区位于河北省保定市涞源县城南15公里处主峰海拔2096米属太行山脉中段是国家地质公园、国家AAAAA级景区。其**独特的大理岩峰林地貌**在中国北方极为罕见,是目前国内发现的**规模最大、海拔最高、保存最完整的大理岩峰林群落**。
白石山被誉为“**太行之魂**”、“**北方张家界**”,拥有 **八大主峰、八十一座奇峰、百余条栈道**,堪称华北地区最具观赏价值的山岳型自然景区之一。
---
## 🏞️ 自然景观特色
### 🪨 大理岩峰林奇观
不同于张家界的砂岩峰林,白石山为**白色大理岩构成的峰林群落**,岩石洁白如玉,峰体耸立如剑,奇峰怪石星罗棋布,景象壮观。常见景点有:
- **天宫顶**主峰之一海拔2096米为观日出的绝佳位置。
- **巨灵掌**:自然风蚀形成的“掌印”造型,形神兼具。
- **仙人晒靴**:巨石形似一双靴子横卧在山崖之上,惟妙惟肖。
- **金鸡独立**:一块巨石高高矗立于山巅,形似金鸡俯瞰众山。
### 🌫️ 云海佛光与日出奇景
白石山地处高海拔山区,湿度大、温差大,极易形成大面积云海、平流雾等自然景观。遇到强光照时,游客甚至可以在悬崖栈道上看到“**佛光**”现象。
### 🌉 悬空栈道
白石山的栈道系统为全国最长之一全长超过5公里大多依山就势**贴悬崖而建,凌空而行**。其中最刺激的是“**天桥栈道**”与“**绝壁栈道**”,漫步其上仿佛置身云端,脚下是千丈深渊。
---
## 📍 景区分区
白石山分为以下几个主要区域:
| 分区名称 | 特色内容 |
|------------|--------------------------------------------------------|
| 天桥栈道区 | 精华路线,包含最惊险的悬崖栈道、观景平台,视野极佳 |
| 仙人峰区 | 奇石林立,包含“仙人晒靴”“望月台”“一线天”等 |
| 天宫顶区 | 海拔最高,可观云海日出,是摄影爱好者和登山客的首选 |
| 南天门景区 | 风景秀丽,适合轻度徒步,通往主要索道出入口 |
| 玻璃栈道区 | 配有高空玻璃栈道,挑战心理极限 |
---
## 🚗 交通方式
### 自驾
从北京市出发经京昆高速 → 张石高速 → 涞源南出口下高速 → 景区全程约230公里车程约3小时。
### 公共交通
从保定或石家庄有直达涞源县的长途车,再乘出租或景区接驳车前往白石山景区。节假日有旅游专线车直达。
---
## 🎫 门票信息2025年参考
- **旺季4月1日—10月31日**
- 门票115元
- 索道单程50元往返90元
- **淡季11月1日—次年3月31日**
- 门票75元
- 索道单程40元往返70元
- 学生、老人、儿童享有不同折扣,凭证件入园。
---
## 🕒 开放时间
- 旺季07:00 - 18:30
- 淡季08:00 - 17:00
> 建议尽早入园,避免高峰时段人流拥挤,亦便于欣赏日出与云海。
---
## 🧳 旅游贴士
- 🥾 **穿着**:建议穿防滑登山鞋,高海拔注意保暖。
- 🌂 **防晒与雨具**:山上紫外线强,备好帽子墨镜及防晒霜。天气变化快,建议携带便携雨衣。
- 💧 **饮食**:景区内物价略高,建议自带水和干粮。
- ⛑️ **安全**:严禁翻越护栏,不要在悬崖边打闹拍照。
- 📸 **最佳摄影时间**:早晨拍云海日出,下午拍山体色彩与夕阳。
---
白石山不仅是一座山,更是一场视觉与心灵的盛宴。这里有太行的雄浑,也有南方的俊美;既能挑战极限栈道,又能沉浸自然仙境。无论你是徒步爱好者、摄影达人还是亲子家庭,白石山都值得你来一趟!

View File

@ -1,59 +0,0 @@
---
title: "山海关长城 - 天下第一关"
description: "山海关长城是明长城东部起点,素有'天下第一关'之称,是中国长城文化的重要象征,也是秦皇岛最著名的历史文化景观。"
featured: true
image: "/images/attractions/shanhaiguan.jpg"
city: ["秦皇岛"]
tags: ["长城", "历史", "文化遗产", "世界遗产", "关隘"]
pubDate: 2023-04-20
---
# 山海关长城
山海关,又称"榆关",位于河北省秦皇岛市东北部,是明长城的东部起点,素有"天下第一关"之称。它北依燕山,南临渤海,扼守华北平原东北咽喉,自古就是兵家必争之地和军事要塞,也是中华文明与塞外文化交流的重要门户。
## 历史背景
山海关始建于明洪武年间(1381年),是明长城九镇之一的"榆关"所在地。明朝将领徐达主持修建了关城后历代均有修缮和加固。明末1644年山海关守将吴三桂"开关迎清",使清军得以入关,成为中国历史上著名的"山海关之变"。这里见证了中国明清两朝更替的重要历史事件,有着丰富的历史文化内涵。
## 建筑特色
山海关城墙全长约4公里呈矩形分为关城和附郭城两部分。关城东西长约1公里南北宽约750米四角各有一座角楼四面各有一座城门分别是:
- 东门:镇东门,上有"天下第一关"匾额
- 西门:迎恩门
- 南门:望洋门
- 北门:威远门
城墙高约14米底宽约7米顶宽约5米全部用城砖砌筑而成。整个关城气势雄伟防御工事严密是明代军事防御体系的典型代表。
## 主要景点
山海关景区包括多处历史文化景点:
1. **天下第一关楼**: 山海关东门城楼,楼上悬挂着"天下第一关"匾额,为清乾隆所书。登楼可俯瞰关城全貌,远眺碧海青山。
2. **老龙头**: 又称"入海长城",是明长城唯一入海处,有"万里长城第一台"之称。这里长城如巨龙探入大海,气势磅礴,是中国长城文化的象征之一。
3. **角山长城**: 山海关长城北部最高处,是观赏长城雄姿的绝佳位置。这里的长城蜿蜒于崇山峻岭之间,体现了长城依山就势的建筑特点。
4. **关帝庙**: 位于关城内,是供奉关羽的场所,体现了古代军事与宗教文化的结合。
5. **榆关古镇**: 保存了明清时期的街道布局和建筑风格,游客可以在此体验古时边关小镇的生活氛围。
## 文化价值
山海关不仅是重要的军事防御工程,也是中华文化的重要载体。这里流传着众多历史故事和民间传说,如"萧何月下追韩信"、"孟姜女哭长城"等。同时,山海关也是中国古代文人墨客寄托情怀的地方,留下了大量诗词歌赋,如"山似海湾痴老树,城如船舶驻沧浪"等名句,丰富了中国的文学艺术宝库。
## 旅游建议
- **最佳游览时间**: 春季(4-5月)和秋季(9-10月)气候宜人,是观光的最佳季节
- **建议游览时间**: 1-2天
- **开放时间**: 夏季7:30-18:30冬季8:00-17:30
- **门票**: 山海关古城区120元/人(旺季)老龙头50元/人
- **交通**:
- 从秦皇岛市区乘坐25路公交车可直达山海关
- 北京、天津等地有直达山海关的火车
- 自驾游可沿京哈高速公路前往
游览山海关长城,感受"天下第一关"的雄伟壮观,领略中华民族悠久历史与灿烂文化的同时,也能欣赏到独特的海河交融的自然风光。

View File

@ -1,38 +0,0 @@
---
title: "衡水老白干火锅 - 酒香四溢的本地特色美食"
description: "衡水老白干火锅是一种独具地方特色的饮食方式,以衡水老白干白酒为汤底,配以牛羊杂、豆制品等,酒香浓郁,辣中带麻,是冬日里驱寒暖胃的绝佳选择。"
category: "地方火锅"
featured: true
image: "/images/cuisine/hengshui-baigan-hotpot.jpg"
city: ["衡水"]
ingredients: ["衡水老白干", "牛杂", "羊杂", "豆腐皮", "白菜", "粉条", "辣椒", "花椒", "葱", "姜", "蒜"]
taste: "香辣浓烈"
cookTime: "1.5小时"
difficulty: "中等"
tags: ["火锅", "衡水美食", "白酒火锅", "地方特色", "冬季美食"]
pubDate: 2025-04-10
---
衡水老白干火锅,是衡水地区冬天非常流行的一道地方美食。不同于普通火锅以清汤或红油为底,这种火锅独具一格地用衡水特产白酒 —— 老白干为汤底,加热时酒香四溢,令人食欲大开。
## 地道食材
火锅的主料通常选用本地现宰的**牛羊杂碎**,经过反复洗净、焯水,再加以**葱姜蒜、花椒、辣椒**等辅料,焖煮入味。配菜方面,**豆腐皮、白菜、粉条**是不可或缺的经典搭配,既吸汤入味,又能平衡肉类的油腻。
## 烹饪方式
老白干火锅的灵魂在于“酒煮”。火锅上桌后,加入一整瓶衡水老白干,点火后酒精会快速挥发,只留下浓郁酒香,并把肉类与香料的味道彻底激发出来,形成一种极具辨识度的香辣口感。
## 口感特点
- **第一口**:酒香扑鼻,带着微醺的刺激感;
- **第二口**:辣味与香味交织,肉质酥烂入味;
- **第三口**:热气腾腾,身体从内而外都暖起来。
## 小贴士
- 初次尝试者可将酒量减少一半,适应其特殊香气;
- 建议搭配**馒头、凉菜**中和辣度;
- 餐后可饮**绿豆汤或酸奶**缓解燥热。
衡水老白干火锅不仅是一道美味,更是一种文化,它把地方酒文化与老百姓的餐桌生活完美融合,是冬天不可错过的一场味觉盛宴。

View File

@ -0,0 +1,64 @@
---
title: "沧州火锅鸡 - 热辣香浓的地方美食"
description: "沧州火锅鸡是一道具有浓郁地方特色的美食,以独特的麻辣味道、嫩滑的鸡肉和丰富的香料调料为特点,深受广大食客喜爱,是沧州地区的传统美食之一。"
category: "特色小吃"
featured: true
image: "/images/cuisine/cangzhou-hotpot-chicken.jpg"
city: ["沧州"]
ingredients: ["鸡肉", "辣椒", "花椒", "生姜", "大葱", "大蒜", "酱油", "黄豆酱", "香料"]
taste: "麻辣鲜香"
cookTime: "1小时"
difficulty: "中等"
tags: ["火锅鸡", "麻辣", "传统美食", "沧州特色", "河北小吃"]
pubDate: 2025-04-12
---
沧州火锅鸡,作为河北省沧州市的传统特色美食,凭借其麻辣鲜香的口味和丰富的香料调味,吸引了大量的食客。它以**鸡肉**为主要食材,搭配**辣椒、花椒、生姜、大葱、大蒜**等各种香料,味道香辣可口,堪称沧州的美食代表之一。
---
## 🍗 特色介绍
沧州火锅鸡的最大特点是麻辣味十足,并且汤底鲜香浓郁。通常这道菜由鸡肉、香料和大量的辣椒、花椒一同烹制,煮至鸡肉嫩滑,汤底充满辛辣的香气,食用时令人欲罢不能。
### 🥘 烹饪方式
- **火锅型烹饪**:传统的沧州火锅鸡常使用火锅的形式制作,采用大火煮沸后再小火慢炖,调料的香味渐渐渗透入鸡肉之中。
- **麻辣味浓**:花椒、辣椒是这道菜的灵魂,火锅鸡的麻辣味正是从这些调料中提取出来的。
- **汤底鲜美**:鸡肉和香料的炖煮,汤底的滋味更是浓郁、醇厚,既能滋补,又能唤醒味蕾。
---
## 🧑‍🍳 原料与调味
### 主要原料:
- **鸡肉**:通常选用嫩鸡,肉质鲜美,经过长时间炖煮后保持滑嫩的口感。
- **辣椒与花椒**:这两样调料是火锅鸡最重要的味道来源,辣味十足,花椒带来独特的麻感。
- **香料与佐料**:大葱、大蒜、生姜等配料丰富了味道层次,而黄豆酱、酱油等则增强了汤底的鲜香。
### 配料:
- **蘸料**:常配有酱油、香菜、蒜末、辣椒粉等调味料,可以根据个人口味调整麻辣度。
---
## 🏙️ 文化与特色
沧州火锅鸡不仅仅是美味的代表,它还是沧州地区的文化象征之一。当地人常常在家宴、朋友聚会和节庆活动中食用这道菜。无论是寒冷的冬季,还是炎热的夏季,火锅鸡都能以其热辣的味道让人感到无比温暖和满足。
此外,沧州火锅鸡的传统烹饪方法和独特的地方风味,也使它成为了沧州饮食文化的重要组成部分。
---
## 🍴 吃法推荐
- **搭配主食**:沧州火锅鸡可以与米饭、馒头等主食一同食用,吸收汤底的香浓味道,简直是绝配。
- **搭配啤酒或白酒**:火锅鸡麻辣鲜香,与啤酒或白酒相伴,能更好地平衡辣味,增加餐饮的乐趣。
---
## 🔥 火锅鸡与沧州的联系
沧州火锅鸡不仅是当地的传统美食,也是沧州人对食材、辛辣和香料的完美理解与运用。它的辣味与麻味是沧州独特的地方文化的体现,无论是街头小摊,还是高端餐厅,都能找到这道美食的身影。
---
沧州火锅鸡是一道鲜香扑鼻、辣味十足的美味佳肴,融入了浓厚的地方风味和传统手艺。如果你来到沧州,一定不要错过这道麻辣鲜香的美食,它会让你对沧州的美食文化留下深刻的印象。

View File

@ -0,0 +1,75 @@
---
title: "衡水老白干酒 - 地道烈香的河北名酒"
description: "衡水老白干酒是河北衡水的传统名酒,以高粱、大麦、豌豆为原料,采用“老五甄法”酿造,酒体醇厚、烈而不燥、香而不艳,是中国白酒文化的重要代表之一。"
category: "特色酒水"
featured: true
image: "/images/cuisine/laobaigan.jpg"
city: ["衡水"]
ingredients: ["高粱", "大麦", "豌豆", "清泉水", "酒曲"]
taste: "浓烈爽净,入口绵甜"
cookTime: "30天酿造周期以上"
difficulty: "高"
tags: ["白酒", "衡水老白干", "河北名酒", "酿造工艺", "烈酒"]
pubDate: 2025-04-12
---
衡水老白干,作为河北衡水的代表性名酒,有着悠久的历史和独特的酿造工艺。它不仅是衡水的重要地标产品,更是中国浓香型白酒体系中独树一帜的存在。
衡水老白干起源于汉代,至今已有**1900多年历史**。据《北齐书》记载:“卢奴烧酒,味极厚烈。”这“卢奴”即为今衡水市之地。它曾是宫廷贡酒,也是民间婚宴、喜庆、待客的标配酒品。
---
## 🍶 酿造工艺
衡水老白干采用传统的**“老五甄法”酿造工艺**,其特点为:
- **地缸发酵**:与窖池发酵不同,衡水老白干使用的是地缸发酵工艺,使得酒体更为干净、纯粹。
- **固态发酵**:全程不加水酿造,只靠粮食发酵,原汁原味。
- **高温馏酒**:高温制曲、高温堆积、高温蒸馏,有助于酒中香气和口感的充分释放。
- **精细分段取酒**:确保酒液品质统一、层次丰富。
其酿造周期较长,一般需要**30天至60天不等**,属于传统白酒工艺中较为复杂的一种。
---
## 🏺 风味特色
衡水老白干的最大特点是“烈而不燥,香而不艳,醇而不腻”,具体表现为:
- **度数高**常见为42°、52°、67°等**尤其67度**为其招牌强烈风格。
- **入口不冲**:虽烈但顺口,喝下去后不上头、不口干,回味悠长。
- **香型独特**:属于“老白干香型”,比浓香更清,比清香更厚。
适合配合重口味菜肴,尤其适合北方的炖肉、烧菜、凉菜佐餐。
---
## 🎉 场合与文化
衡水老白干不仅是酒,更是一种社交语言,在河北尤其常见于以下场合:
- 🥂 **婚宴喜酒**:婚礼上亲朋满堂,老白干象征热烈与真诚。
- 🍻 **兄弟聚会**:一口老白干,情谊全靠干。
- 🎁 **节日送礼**:春节、中秋送亲友一瓶“衡水老白干”,体面且实在。
在当地流传着一句话:“小酒怡情,大酒助兴,老白干,感情深一口闷。”
---
## 🔎 品牌代表
- **衡水老白干股份有限公司1915年巴拿马万国博览会金奖**
- **十八酒坊**:为衡水老白干的高端系列代表,口感更柔和。
- **醉美河北纪念酒、青花瓷系列等**,为不同消费场景打造的延展产品。
---
## 🥃 饮用建议
- **常温或微温饮用**,可减少刺激性。
- 配合**凉拌菜或卤肉类**,口感最佳。
- 注意度数,不建议空腹饮用或大量畅饮。
---
衡水老白干,是一杯白酒,也是一段文化。它承载着河北人的豪爽与朴实,也传递着中国白酒酿造的传承与创新。若你来到衡水,不妨举杯一试——**烈而不燥,饮后难忘。**

View File

@ -1,76 +0,0 @@
---
title: "唐山豆腐宴 - 中国北方豆香美食文化"
description: "唐山豆腐宴是河北唐山地区的传统特色美食,以豆腐为主料,制作工艺精细,变化多样,口味丰富,被誉为'豆腐的盛宴'。"
category: "地方特色"
featured: true
image: "/images/cuisine/tangshan-doufu.jpg"
city: ["唐山"]
ingredients: ["豆腐", "黄豆", "调味料", "素菜", "肉类"]
taste: "鲜香清淡"
cookTime: "因菜而异"
difficulty: "中等"
tags: ["豆腐宴", "传统美食", "健康饮食", "宫廷菜", "河北特色"]
pubDate: 2023-05-18
---
# 唐山豆腐宴
唐山豆腐宴,是河北省唐山市的传统特色美食,以豆腐为主要食材,融合了北方烹饪技艺和当地饮食特色,创造出数十种豆腐菜肴,被誉为"中国豆腐美食的集大成者"。这种独特的宴席文化不仅展现了豆腐的多样性和可塑性,也体现了唐山人民朴实、创新的饮食智慧。
## 历史渊源
唐山豆腐宴源起于明清时期,相传与清代宫廷御膳有一定渊源。据史料记载,乾隆皇帝下江南时曾品尝并赞赏唐山豆腐,甚至将其引入宫廷御膳。随着时间的推移,唐山豆腐宴逐渐在民间流传开来,并不断发展创新,形成了今天丰富多彩的菜系。
唐山大地肥沃,盛产优质黄豆,加上唐山地处渤海湾,水质优良,为制作上乘豆腐提供了得天独厚的条件。当地人用心研究豆腐制作技艺,并开发出多种烹饪方法,使豆腐宴成为当地独特的饮食文化符号。
## 特色工艺
唐山豆腐制作工艺精细,讲究选料、浸泡、磨浆、点卤等多道工序,每一步都影响着豆腐的品质。当地豆腐主要分为以下几类:
- **老豆腐**:质地坚实,口感细腻,是制作红烧、炖、炒等菜肴的基础。
- **嫩豆腐**:水分多,质地柔软,适合做汤羹、蒸菜等。
- **豆腐脑**:最为细嫩,多作早餐或甜品。
- **豆腐干**:经过压制去水,有咸、五香等多种口味,可直接食用或做菜。
- **豆腐皮**:富含蛋白质,可制作多种素菜或肉菜的包裹材料。
## 代表菜品
唐山豆腐宴通常包含20-30道菜品每道菜都有精心的制作工艺和独特的口味主要代表菜品有
### 镇宴三绝
- **八仙过海**:选用上等嫩豆腐,雕刻成八个仙人形象,放入高汤中,配以海鲜,象征八仙过海的传说。
- **百花豆腐**:将嫩豆腐切成薄片,上面点缀各色蔬菜,形如百花盛开,蒸制而成,清香鲜美。
- **玉龙戏珠**:用豆腐皮包裹馅料,捏制成龙形,配以红色"珍珠"点缀,造型生动,口感丰富。
### 传统名菜
- **麻婆豆腐**:唐山版麻婆豆腐油而不腻,麻而不辣,保持了豆腐的本味。
- **豆腐包子**:用豆腐替代面粉做皮,包入鲜美馅料,蒸熟后外软内香。
- **豆腐烧茄子**:将豆腐与茄子一同炖煮,吸收酱汁,味道醇厚。
- **豆腐丸子汤**:将豆腐捣碎加入配料制成丸子,放入鲜汤中煮制,鲜美可口。
- **罐儿豆腐**:将豆腐切块,与肉末、香菇等一同放入小罐中蒸制,风味独特。
### 创新菜式
- **豆腐冰淇淋**:将豆腐制成甜品,滑嫩爽口,营养健康。
- **彩色豆腐拼盘**:用天然食材为豆腐着色,制成多彩拼盘,美观诱人。
- **豆腐西施酒**:用豆腐发酵而成的特色酒,低度微甜,别具一格。
## 文化价值
唐山豆腐宴不仅是一种美食,更是一种文化象征。它体现了以下价值:
1. **健康饮食**:豆腐富含植物蛋白,低脂肪,符合现代健康饮食理念。
2. **勤俭美德**:利用简单食材创造丰富美食,展现了中华民族的勤俭智慧。
3. **文化创新**:在传统基础上不断创新,使古老食材焕发新生。
4. **地域特色**:成为唐山地区重要的文化名片和旅游资源。
## 品尝建议
- **最佳食用季节**:四季皆宜,夏季尤佳
- **推荐餐厅**:唐山饭店、丰润豆腐宴、唐山老字号豆香村
- **价格范围**套餐价格200-800元/桌不等,依菜品数量和档次而定
- **饮食礼仪**:先品尝原味豆腐,再享用其他菜品,以领略豆腐的本真风味
来唐山,品味一席豆腐盛宴,感受中国北方饮食文化的精致与智慧,体验豆香四溢的美食之旅。

View File

@ -1,96 +0,0 @@
---
title: "承德三日避暑之旅 - 清凉山水与皇家园林"
description: "这是一条专为夏季设计的承德避暑旅游路线,涵盖避暑山庄、外八庙、木兰围场等经典景点,让您在酷暑中体验清凉山水与皇家园林的完美结合。"
season: "夏季"
type: "休闲游"
featured: true
image: "/images/travel/chengde-summer.jpg"
city: ["承德", "热河"]
days: 3
difficulty: "简单"
tags: ["避暑", "皇家园林", "历史文化", "自然风光", "世界遗产"]
pubDate: 2023-07-25
---
# 承德三日避暑之旅
承德,这座被誉为"塞外北京"的历史文化名城以其宜人的夏季气候和丰富的文化遗产吸引着无数游客。夏季的承德平均气温比北京低4-5℃是北方地区理想的避暑胜地。本攻略为您规划了三天的行程带您领略避暑山庄的皇家风范、感受外八庙的宗教文化魅力、探索木兰围场的自然风光。
## 第一天:避暑山庄一日游
### 上午
- **07:30-12:00** 【避暑山庄】早晨抵达避暑山庄,参观山庄的宫殿区,包括澹泊敬诚殿、烟波致爽殿、清音阁等主要宫殿建筑。了解清代皇帝在此处理政务和休闲生活的历史。
### 中午
- **12:00-13:30** 在山庄附近的餐厅享用午餐,推荐品尝承德当地特色菜肴如围场烤全羊、承德火锅等。
### 下午
- **13:30-17:30** 继续游览避暑山庄的湖区和山区景观,如烟波致爽、月色澄莲、热河烟树等著名景点。可在湖区乘船游览,欣赏湖光山色的美景。
### 晚上
- **18:00-19:30** 在市区享用晚餐,品尝承德特色美食。
- **20:00** 入住酒店休息,或可选择在承德市区夜景散步,欣赏城市夜色。
## 第二天:外八庙与普宁寺
### 上午
- **08:30-12:00** 【外八庙】参观外八庙中最具代表性的普宁寺和普陀宗乘之庙。普宁寺内有世界最大的木雕佛像(释迦牟尼坐像),普陀宗乘之庙仿照西藏布达拉宫建造,具有浓郁的藏族建筑风格。
### 中午
- **12:00-13:30** 在庙区附近用午餐,可品尝当地特色素斋或小吃。
### 下午
- **13:30-17:00** 继续参观须弥福寿之庙和安远庙,感受不同民族宗教建筑风格的融合与特色。
- **17:00-18:30** 返回市区,在市区休息或购物。
### 晚上
- **18:30-20:00** 晚餐后可以欣赏《鼎盛王朝·康熙大典》实景剧演出(需提前预订),了解康熙与承德的历史故事。
## 第三天:木兰围场自然风光
### 上午
- **08:00-12:00** 【木兰围场】前往位于承德北部的木兰围场景区,参观御道口草原森林公园,体验辽阔的草原风光和清新的森林空气。可以骑马、放风筝、观赏野花等活动。
### 中午
- **12:00-13:30** 在景区内的蒙古包或农家乐享用午餐,品尝围场烤全羊、手把肉等特色美食。
### 下午
- **13:30-16:30** 参观塞罕坝国家森林公园,欣赏人工林海和天然湿地景观,感受"绿色长城"的壮观与生态保护的成就。
- **16:30-18:00** 返回市区,整理行装准备返程。
## 住宿推荐
- **高档**:承德金龙国际大酒店、承德勒泰假日酒店
- **中档**:承德避暑山庄宾馆、承德金山饭店
- **经济**如家快捷酒店承德火车站店、7天连锁酒店
## 交通建议
- **外部交通**从北京可乘坐高铁约2小时或长途汽车约4小时抵达承德
- **市内交通**
- 公交车承德市内公交网络发达可乘坐5路、15路等公交线路到达主要景点
- 出租车起步价约8元市区主要景点间车程一般不超过20分钟
- 景区观光车避暑山庄和外八庙内有观光车服务单程票价约10元
## 旅行贴士
1. **最佳旅游时间**7-8月是承德避暑的最佳季节平均气温约24℃
2. **着装建议**:夏季白天温暖但早晚温差大,建议携带轻薄外套
3. **门票信息**
- 避暑山庄旺季115元/人淡季70元/人
- 外八庙联票旺季约180元/人,可选择单独购买感兴趣的寺庙门票
- 木兰围场各景点门票约60-120元不等
4. **特别提示**:参观寺庙时注意着装得体,尊重宗教习俗;林区防蚊虫叮咬,注意防晒
来承德避暑,感受清代皇家园林的恢宏气势,领略塞外自然风光的独特魅力,度过一个清凉舒适的夏日之旅。

View File

@ -109,7 +109,15 @@ const relatedAttractions = allAttractions
<!-- 景点图片 -->
<div class="rounded-lg overflow-hidden shadow-md">
<div class="h-48 md:h-64 bg-gray-300 dark:bg-gray-700 flex items-center justify-center">
<span class="text-gray-500 dark:text-gray-400">{entry.data.title} 图片</span>
{entry.data.image ? (
<img
src={entry.data.image}
alt={entry.data.title}
class="w-full h-full object-cover"
/>
) : (
<span class="text-gray-500 dark:text-gray-400">{entry.data.title} 图片</span>
)}
</div>
</div>
@ -147,22 +155,22 @@ const relatedAttractions = allAttractions
</div>
<!-- 景点信息卡片 -->
<div class="bg-gray-50 dark:bg-color-dark-card rounded-lg shadow-md p-5">
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-md p-5">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">景点信息</h3>
<div class="space-y-3">
{entry.data.city && (
<div class="flex">
<span class="w-24 flex-shrink-0 text-gray-600 dark:text-gray-400">位置:</span>
<div class="flex items-start">
<span class="text-gray-600 dark:text-gray-300 mr-2">位置:</span>
<span class="text-gray-900 dark:text-white">{entry.data.city}</span>
</div>
)}
<div class="flex flex-wrap">
<span class="w-24 flex-shrink-0 text-gray-600 dark:text-gray-400">景点类型:</span>
<span class="text-gray-600 dark:text-gray-300 mr-2">景点类型:</span>
<div class="flex flex-wrap gap-1">
{entry.data.tags.map((tag: string) => (
<span class="px-2 py-0.5 bg-color-primary-100 text-color-primary-800 text-xs rounded-full dark:bg-color-dark-primary-900/70 dark:text-color-primary-300">
<span class="px-2 py-0.5 bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 text-xs rounded-full">
{tag}
</span>
))}
@ -170,8 +178,8 @@ const relatedAttractions = allAttractions
</div>
{entry.data.pubDate && (
<div class="flex">
<span class="w-24 flex-shrink-0 text-gray-600 dark:text-gray-400">发布时间:</span>
<div class="flex items-start">
<span class="text-gray-600 dark:text-gray-300 mr-2">发布时间:</span>
<span class="text-gray-900 dark:text-white">{new Date(entry.data.pubDate).toLocaleDateString('zh-CN')}</span>
</div>
)}
@ -262,8 +270,16 @@ const relatedAttractions = allAttractions
{relatedAttractions.map((attraction: CollectionEntry<"attractions">) => (
<a href={`/attractions/${attraction.slug}`} class="block group">
<div class="flex items-start space-x-3 p-3 rounded-lg hover:bg-primary-100 dark:hover:bg-primary-900/30 transition-colors">
<div class="w-12 h-12 md:w-16 md:h-16 rounded bg-primary-200 dark:bg-primary-800 flex items-center justify-center text-primary-700 dark:text-primary-300 flex-shrink-0">
{attraction.data.title.substring(0, 1)}
<div class="w-12 h-12 md:w-16 md:h-16 rounded bg-primary-200 dark:bg-primary-800 flex items-center justify-center text-primary-700 dark:text-primary-300 flex-shrink-0 overflow-hidden">
{attraction.data.image ? (
<img
src={attraction.data.image}
alt={attraction.data.title}
class="w-full h-full object-cover"
/>
) : (
<span>{attraction.data.title.substring(0, 1)}</span>
)}
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 transition-colors">

View File

@ -344,7 +344,15 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
<!-- 景点图片 - 彩色摄影展览风格 -->
<div class="aspect-[4/5] relative overflow-hidden rounded-t-lg">
<div class="absolute inset-0 bg-gradient-to-br from-primary-100 via-primary-100 to-secondary-100 dark:from-primary-900/30 dark:via-primary-900/30 dark:to-secondary-900/30 flex items-center justify-center">
<span class="text-primary-400 dark:text-primary-500">{attraction.data.title}</span>
{attraction.data.image ? (
<img
src={attraction.data.image}
alt={attraction.data.title}
class="w-full h-full object-cover"
/>
) : (
<span class="text-primary-400 dark:text-primary-500">{attraction.data.title}</span>
)}
</div>
<!-- 景点信息悬浮层 - 彩色相片信息卡片 -->
@ -371,20 +379,15 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
</p>
<!-- 标签 - 彩色快速信息 -->
<div class="attraction-tags flex flex-wrap gap-0.5 sm:gap-1 pt-1 opacity-80 group-hover:opacity-100 transition-opacity">
{attraction.data.tags.slice(0, 3).map((tag: string, i: number) => {
<div class="attraction-tags flex overflow-x-auto gap-0.5 sm:gap-1 pt-1 opacity-80 group-hover:opacity-100 transition-opacity scrollbar-thin scrollbar-thumb-primary-500/30 dark:scrollbar-thumb-primary-400/30 scrollbar-track-primary-100/20 dark:scrollbar-track-primary-900/20 pb-2">
{attraction.data.tags.map((tag: string, i: number) => {
const tagColors = ['bg-primary-500/40', 'bg-secondary-500/40', 'bg-accent-500/40'];
return (
<span class={`px-1.5 py-0.5 text-[10px] ${tagColors[i % tagColors.length]} text-white backdrop-blur-sm rounded-sm`}>
<span class={`flex-shrink-0 px-1.5 py-0.5 text-[10px] ${tagColors[i % tagColors.length]} text-white backdrop-blur-sm rounded-sm`}>
#{tag}
</span>
);
})}
{attraction.data.tags.length > 3 && (
<span class="px-1.5 py-0.5 text-[10px] bg-white/20 text-white backdrop-blur-sm rounded-sm">
+{attraction.data.tags.length - 3}
</span>
)}
</div>
</div>
</div>
@ -667,6 +670,72 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
height: 14px;
}
}
/* 自定义滚动条样式 */
.attraction-tags::-webkit-scrollbar {
height: 4px;
border-radius: 2px;
}
.attraction-tags::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.attraction-tags::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
transition: background-color 0.3s ease;
}
.attraction-tags::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
/* 暗色模式下的滚动条样式 */
.dark .attraction-tags::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
}
.dark .attraction-tags::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
.dark .attraction-tags::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* 火狐浏览器的滚动条样式 */
.attraction-tags {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) rgba(255, 255, 255, 0.1);
}
.dark .attraction-tags {
scrollbar-color: rgba(255, 255, 255, 0.2) rgba(0, 0, 0, 0.2);
}
/* 确保滚动条在悬停时显示 */
.attraction-tags {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch; /* 在iOS上实现平滑滚动 */
scroll-behavior: smooth; /* 平滑滚动效果 */
padding-bottom: 8px; /* 为滚动条预留空间 */
margin-bottom: -8px; /* 补偿padding-bottom造成的空间 */
}
/* 在触摸设备上优化滚动体验 */
@media (hover: none) {
.attraction-tags {
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
}
.attraction-tags > * {
scroll-snap-align: start;
}
}
</style>
<script>
@ -744,25 +813,41 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
// 客户端解析URL查询参数
const urlParams = new URLSearchParams(window.location.search);
const searchParamFromUrl = urlParams.get('search');
const tagParamFromUrl = urlParams.get('tag');
const cityParamFromUrl = urlParams.get('city');
const tagsParamFromUrl = urlParams.getAll('tags');
const citiesParamFromUrl = urlParams.getAll('cities');
const pageParam = urlParams.get('page');
const currentPage = pageParam ? parseInt(pageParam) : 1;
// 当前筛选状态
let currentTag = tagParamFromUrl || '';
let currentCity = cityParamFromUrl || '';
// 当前筛选状态 - 改为数组以支持多选
let currentSearch = searchParamFromUrl || '';
let selectedTags: string[] = tagsParamFromUrl || [];
let selectedCities: string[] = citiesParamFromUrl || [];
// 更新浏览器历史记录,不刷新页面
function updateHistory() {
const params = new URLSearchParams();
if (currentSearch) params.set('search', currentSearch);
if (currentTag) params.set('tag', currentTag);
if (currentCity) params.set('city', currentCity);
// 添加多个标签参数
selectedTags.forEach(tag => {
params.append('tags', tag);
});
// 添加多个城市参数
selectedCities.forEach(city => {
params.append('cities', city);
});
// 如果有分页参数
if (pageParam) params.set('page', pageParam);
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
window.history.pushState({ search: currentSearch, tag: currentTag, city: currentCity }, '', newUrl);
window.history.pushState({
search: currentSearch,
tags: selectedTags,
cities: selectedCities,
page: currentPage
}, '', newUrl);
}
// 显示当前筛选状态
@ -778,18 +863,18 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
searchFilter.classList.add('hidden');
}
// 处理标签筛选
if (currentTag && tagFilter && tagFilterText) {
tagFilterText.textContent = `标签: ${currentTag}`;
// 处理标签筛选 - 现在可能有多个标签
if (selectedTags.length > 0 && tagFilter && tagFilterText) {
tagFilterText.textContent = `标签: ${selectedTags.join(', ')}`;
tagFilter.classList.remove('hidden');
hasFilters = true;
} else if (tagFilter) {
tagFilter.classList.add('hidden');
}
// 处理城市筛选
if (currentCity && cityFilter && cityFilterText) {
cityFilterText.textContent = `城市: ${currentCity}`;
// 处理城市筛选 - 现在可能有多个城市
if (selectedCities.length > 0 && cityFilter && cityFilterText) {
cityFilterText.textContent = `城市: ${selectedCities.join(', ')}`;
cityFilter.classList.remove('hidden');
hasFilters = true;
} else if (cityFilter) {
@ -807,9 +892,6 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
// 客户端筛选函数
function filterAttractions() {
const searchValue = currentSearch.toLowerCase();
const tagValue = currentTag.toLowerCase();
const cityValue = currentCity.toLowerCase();
let matchCount = 0;
const matchedCards: HTMLElement[] = [];
@ -843,14 +925,32 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
isMatch = false;
}
// 标签筛选
if (tagValue && !cardData.tags.some((tag: string) => tag.includes(tagValue))) {
isMatch = false;
// 标签筛选 - 多标签匹配
if (selectedTags.length > 0) {
// 判断卡片的标签是否包含任意一个选中的标签
const hasMatchingTag = selectedTags.some(selectedTag =>
cardData.tags.some((cardTag: string) =>
cardTag.includes(selectedTag.toLowerCase())
)
);
if (!hasMatchingTag) {
isMatch = false;
}
}
// 城市筛选
if (cityValue && !cardData.city.some((city: string) => city.includes(cityValue))) {
isMatch = false;
// 城市筛选 - 多城市匹配
if (selectedCities.length > 0) {
// 判断卡片的城市是否包含任意一个选中的城市
const hasMatchingCity = selectedCities.some(selectedCity =>
cardData.city.some((cardCity: string) =>
cardCity.includes(selectedCity.toLowerCase())
)
);
if (!hasMatchingCity) {
isMatch = false;
}
}
if (isMatch) {
@ -861,7 +961,6 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
container.classList.add('hidden-card');
}
} catch (error) {
console.error('Error filtering card:', error);
container.classList.add('hidden-card');
}
});
@ -881,11 +980,11 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
if (searchValue) {
messageText += ` 搜索词: "${searchValue}"`;
}
if (tagValue) {
messageText += ` 标签: "${tagValue}"`;
if (selectedTags.length > 0) {
messageText += ` 标签: "${selectedTags.join(', ')}"`;
}
if (cityValue) {
messageText += ` 城市: "${cityValue}"`;
if (selectedCities.length > 0) {
messageText += ` 城市: "${selectedCities.join(', ')}"`;
}
messageText += '。请尝试其他条件或浏览所有景点。';
@ -910,19 +1009,25 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
// 处理点击标签/城市筛选的函数
function handleFilterClick(type: 'tag' | 'city', value: string) {
// 如果点击了已选中的标签/城市,则取消选择
if ((type === 'tag' && value === currentTag) || (type === 'city' && value === currentCity)) {
if (type === 'tag') {
currentTag = '';
if (type === 'tag') {
// 检查标签是否已经选中
const tagIndex = selectedTags.indexOf(value);
if (tagIndex !== -1) {
// 如果已经选中,则移除
selectedTags.splice(tagIndex, 1);
} else {
currentCity = '';
// 如果未选中,则添加
selectedTags.push(value);
}
} else {
// 否则,选择新标签/城市
if (type === 'tag') {
currentTag = value;
// 检查城市是否已经选中
const cityIndex = selectedCities.indexOf(value);
if (cityIndex !== -1) {
// 如果已经选中,则移除
selectedCities.splice(cityIndex, 1);
} else {
currentCity = value;
// 如果未选中,则添加
selectedCities.push(value);
}
}
@ -933,7 +1038,7 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
updateHistory();
}
// 新增:更新标签和城市的高亮状态
// 更新标签和城市的高亮状态
function updateTagsHighlight() {
// 处理桌面端标签
if (desktopTagElements) {
@ -959,8 +1064,8 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
if (isTag) {
tagText = tagText.replace(/^#/, '');
// 处理标签高亮
if (tagText === currentTag) {
// 处理标签高亮 - 检查是否在选中标签数组中
if (selectedTags.includes(tagText)) {
// 添加高亮样式
tag.classList.add('bg-accent-500/70', 'dark:bg-accent-500/50', 'text-white', 'dark:text-white', 'shadow-md');
tag.classList.remove('bg-accent-100', 'dark:bg-accent-900/30', 'text-accent-800', 'dark:text-accent-200');
@ -970,8 +1075,8 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
tag.classList.add('bg-accent-100', 'dark:bg-accent-900/30', 'text-accent-800', 'dark:text-accent-200');
}
} else {
// 处理城市高亮
if (tagText === currentCity) {
// 处理城市高亮 - 检查是否在选中城市数组中
if (selectedCities.includes(tagText)) {
// 添加高亮样式
tag.classList.add('bg-secondary-500/70', 'dark:bg-secondary-500/50', 'text-white', 'dark:text-white', 'shadow-md');
tag.classList.remove('bg-secondary-100', 'dark:bg-secondary-900/30', 'text-secondary-800', 'dark:text-secondary-200');
@ -1027,15 +1132,15 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
updateFilterStatusUI();
// 如果URL中有筛选参数执行筛选
if (searchParamFromUrl || tagParamFromUrl || cityParamFromUrl) {
if (searchParamFromUrl || tagsParamFromUrl.length > 0 || citiesParamFromUrl.length > 0) {
if (searchParamFromUrl) {
if (searchInput) searchInput.value = searchParamFromUrl;
if (searchInputMobile) searchInputMobile.value = searchParamFromUrl;
}
// 设置当前的标签和城市
if (tagParamFromUrl) currentTag = tagParamFromUrl;
if (cityParamFromUrl) currentCity = cityParamFromUrl;
// 标签和城市筛选从URL参数中获取
selectedTags = tagsParamFromUrl;
selectedCities = citiesParamFromUrl;
// 更新标签高亮
updateTagsHighlight();
@ -1049,8 +1154,8 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
// 从历史状态中恢复筛选条件
if (event.state) {
currentSearch = event.state.search || '';
currentTag = event.state.tag || '';
currentCity = event.state.city || '';
selectedTags = event.state.tags || [];
selectedCities = event.state.cities || [];
// 更新搜索框值
if (searchInput) searchInput.value = currentSearch;
@ -1058,8 +1163,8 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
} else {
// 如果没有状态(例如回到初始页面),清除所有筛选
currentSearch = '';
currentTag = '';
currentCity = '';
selectedTags = [];
selectedCities = [];
if (searchInput) searchInput.value = '';
if (searchInputMobile) searchInputMobile.value = '';
@ -1076,8 +1181,8 @@ const currentPageAttractions = filteredAttractions.slice((page - 1) * itemsPerPa
resetFiltersBtn.addEventListener('click', () => {
// 清除所有筛选条件
currentSearch = '';
currentTag = '';
currentCity = '';
selectedTags = [];
selectedCities = [];
// 清空搜索框
if (searchInput) searchInput.value = '';

View File

@ -135,6 +135,32 @@ const relatedCuisines = allCuisines
</div>
</div>
<!-- 返回按钮 -->
<a
href="/cuisine"
class="hidden lg:block w-full text-center px-4 py-3 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors shadow-sm border border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500"
>
<span class="inline-flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
</svg>
返回所有美食
</span>
</a>
<!-- 移动端悬浮返回按钮 -->
<a
href="/cuisine"
class="lg:hidden fixed bottom-6 left-1/2 -translate-x-1/2 px-6 py-3 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors shadow-lg border border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500 z-50"
>
<span class="inline-flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
</svg>
返回所有美食
</span>
</a>
<!-- 相关美食推荐 -->
{relatedCuisines.length > 0 && (
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">

View File

@ -707,19 +707,33 @@ if (queryParams) queryParams = '?' + queryParams.substring(1);
<!-- 食谱卡片头部 -->
<div class="aspect-[4/3] relative overflow-hidden">
<div class={`absolute inset-0 bg-recipe-paper dark:bg-recipe-paper-dark flex items-center justify-center`}>
<span class="text-brown-400 dark:text-brown-600 font-recipe-body">{cuisine.data.title}</span>
</div>
{cuisine.data.image ? (
<img
src={cuisine.data.image}
alt={cuisine.data.title}
class="w-full h-full object-cover"
/>
) : (
<span class="text-brown-400 dark:text-brown-600 font-recipe-body">{cuisine.data.title}</span>
)}
</div>
<!-- 图钉装饰 -->
<div class="absolute top-3 left-3 w-4 h-4 bg-red-500 dark:bg-red-700 rounded-full shadow-sm"></div>
<div class="absolute top-3 right-3 w-4 h-4 bg-amber-500 dark:bg-amber-700 rounded-full shadow-sm"></div>
{cuisine.data.category && (
<div class="absolute bottom-3 right-3 px-2 py-1 text-xs font-recipe-body bg-white/90 dark:bg-black/70 text-brown-800 dark:text-brown-200 rounded-md shadow-sm border border-brown-200 dark:border-brown-800">
<div class="absolute bottom-3 right-3 px-2 py-1 text-xs font-recipe-body bg-white/90 dark:bg-black/70 text-brown-800 dark:text-brown-200 rounded-md shadow-sm border border-brown-200 dark:border-brown-500">
{cuisine.data.category}
</div>
)}
{cuisine.data.taste && (
<div class="absolute bottom-3 left-3 px-2 py-1 text-xs font-recipe-body bg-white/90 dark:bg-black/70 text-brown-800 dark:text-brown-200 rounded-md shadow-sm border border-brown-200 dark:border-brown-500">
{cuisine.data.taste}
</div>
)}
</div>
<!-- 食谱内容 -->
@ -729,17 +743,12 @@ if (queryParams) queryParams = '?' + queryParams.substring(1);
</h3>
<!-- 食谱元数据 -->
<div class="flex flex-wrap gap-3 text-sm mb-3">
<div class="flex overflow-x-auto gap-3 text-sm mb-3 scrollbar-thin scrollbar-thumb-amber-500/30 dark:scrollbar-thumb-amber-400/30 scrollbar-track-amber-100/20 dark:scrollbar-track-amber-900/20 pb-2">
{cuisine.data.city && cuisine.data.city.length > 0 && cuisine.data.city.map((cityName: string) => (
<div class="flex items-center px-2 py-0.5 bg-amber-50/80 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-full">
<div class="flex-shrink-0 flex items-center px-2 py-0.5 bg-amber-50/80 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-full">
<span class="text-amber-800 dark:text-amber-300 text-xs font-recipe-body">{cityName}</span>
</div>
))}
{cuisine.data.taste && (
<div class="flex items-center px-2 py-0.5 bg-red-50/80 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-full">
<span class="text-red-800 dark:text-red-300 text-xs font-recipe-body">{cuisine.data.taste}</span>
</div>
)}
</div>
<!-- 食谱描述 -->
@ -748,22 +757,17 @@ if (queryParams) queryParams = '?' + queryParams.substring(1);
</p>
<!-- 食谱标签 -->
<div class="flex flex-wrap gap-2 mb-4">
{cuisine.data.tags.slice(0, 3).map((tag: string, i: number) => (
<span class="px-2 py-0.5 text-xs font-recipe-body bg-brown-100/80 dark:bg-brown-900/30 text-brown-800 dark:text-brown-200 border border-brown-200 dark:border-brown-800 rounded-full">
#{tag}
</span>
))}
{cuisine.data.tags.length > 3 && (
<span class="px-2 py-0.5 text-xs font-recipe-body bg-brown-100/80 dark:bg-brown-900/30 text-brown-800 dark:text-brown-200 border border-brown-200 dark:border-brown-800 rounded-full">
+{cuisine.data.tags.length - 3}
</span>
)}
<div class="flex overflow-x-auto gap-2 mb-4 scrollbar-thin scrollbar-thumb-brown-500/30 dark:scrollbar-thumb-brown-400/30 scrollbar-track-brown-100/20 dark:scrollbar-track-brown-900/20 pb-2">
{cuisine.data.tags.map((tag: string, i: number) => (
<span class="flex-shrink-0 px-2 py-0.5 text-xs font-recipe-body bg-brown-100/80 dark:bg-brown-900/30 text-brown-800 dark:text-brown-200 border border-brown-200 dark:border-brown-800 rounded-full">
{tag}
</span>
))}
</div>
<!-- 查看详情 -->
<div class="flex justify-between items-center">
<div class="text-sm text-amber-700 dark:text-amber-300 flex items-center group-hover:translate-x-1 transition-transform font-recipe-body">
<div class="text-sm text-amber-700 dark:text-amber-500 flex items-center group-hover:translate-x-1 transition-transform font-recipe-body">
查看详细食谱
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
@ -997,6 +1001,9 @@ if (queryParams) queryParams = '?' + queryParams.substring(1);
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
background-color: var(--recipe-card-bg, rgba(255, 251, 235, 0.9));
display: flex;
flex-direction: column;
height: 100%;
}
.dark .recipe-card-item {
@ -1004,6 +1011,115 @@ if (queryParams) queryParams = '?' + queryParams.substring(1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* 卡片内容区域布局 */
.recipe-card-item > div:last-child {
flex: 1;
display: flex;
flex-direction: column;
padding: 1.25rem;
min-height: 16rem;
}
/* 标题区域 */
.recipe-card-item h3 {
font-size: 1.25rem;
line-height: 1.4;
margin-bottom: 0.75rem;
min-height: 2.8rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 描述文本区域 */
.recipe-card-item p.text-brown-700 {
flex: 0 0 auto;
margin-bottom: 1rem;
min-height: 3.6rem;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 标签区域 */
.recipe-card-item .flex.overflow-x-auto.gap-2.mb-4 {
flex: 0 0 auto;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
min-height: 2.5rem;
max-height: 2.5rem;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(193, 154, 107, 0.3) rgba(193, 154, 107, 0.1);
}
/* 查看详情按钮区域 */
.recipe-card-item .flex.justify-between.items-center {
flex: 0 0 auto;
margin-top: auto;
padding-top: 0.5rem;
min-height: 2rem;
}
/* 标签样式 */
.recipe-card-item .flex.overflow-x-auto.gap-2.mb-4 span {
display: inline-flex;
align-items: center;
height: 1.5rem;
padding: 0 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 滚动条样式 */
.recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar {
height: 4px;
border-radius: 2px;
}
.recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-track {
background: rgba(193, 154, 107, 0.1);
border-radius: 2px;
}
.recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-thumb {
background: rgba(193, 154, 107, 0.3);
border-radius: 2px;
transition: background-color 0.3s ease;
}
.recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: rgba(193, 154, 107, 0.5);
}
/* 暗色模式下的滚动条样式 */
.dark .recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-track {
background: rgba(120, 53, 15, 0.1);
}
.dark .recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-thumb {
background: rgba(193, 154, 107, 0.2);
}
.dark .recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: rgba(193, 154, 107, 0.3);
}
/* Firefox 滚动条样式 */
.recipe-card-item .flex.overflow-x-auto {
scrollbar-width: thin;
scrollbar-color: rgba(193, 154, 107, 0.3) rgba(193, 154, 107, 0.1);
}
.dark .recipe-card-item .flex.overflow-x-auto {
scrollbar-color: rgba(193, 154, 107, 0.2) rgba(120, 53, 15, 0.1);
}
/* 隐藏滚动条但保留功能 */
.hide-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
@ -1108,6 +1224,72 @@ if (queryParams) queryParams = '?' + queryParams.substring(1);
min-height: auto;
height: auto;
}
/* 自定义滚动条样式 */
.recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar {
height: 4px;
border-radius: 2px;
}
.recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-track {
background: rgba(193, 154, 107, 0.1);
border-radius: 2px;
}
.recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-thumb {
background: rgba(193, 154, 107, 0.3);
border-radius: 2px;
transition: background-color 0.3s ease;
}
.recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: rgba(193, 154, 107, 0.5);
}
/* 暗色模式下的滚动条样式 */
.dark .recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-track {
background: rgba(120, 53, 15, 0.1);
}
.dark .recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-thumb {
background: rgba(193, 154, 107, 0.2);
}
.dark .recipe-card-item .flex.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: rgba(193, 154, 107, 0.3);
}
/* Firefox 滚动条样式 */
.recipe-card-item .flex.overflow-x-auto {
scrollbar-width: thin;
scrollbar-color: rgba(193, 154, 107, 0.3) rgba(193, 154, 107, 0.1);
}
.dark .recipe-card-item .flex.overflow-x-auto {
scrollbar-color: rgba(193, 154, 107, 0.2) rgba(120, 53, 15, 0.1);
}
/* 确保滚动条在悬停时显示 */
.recipe-card-item .flex.overflow-x-auto {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch; /* 在iOS上实现平滑滚动 */
scroll-behavior: smooth; /* 平滑滚动效果 */
padding-bottom: 8px; /* 为滚动条预留空间 */
margin-bottom: -8px; /* 补偿padding-bottom造成的空间 */
}
/* 在触摸设备上优化滚动体验 */
@media (hover: none) {
.recipe-card-item .flex.overflow-x-auto {
-webkit-overflow-scrolling: touch;
scroll-snap-type: x proximity;
}
.recipe-card-item .flex.overflow-x-auto > * {
scroll-snap-align: start;
}
}
</style>
<script>

View File

@ -36,7 +36,7 @@ const relatedCultures = allCultures
<div class="md:hidden fixed bottom-6 left-1/2 transform -translate-x-1/2 z-50">
<a
href="/culture"
class="flex items-center space-x-2 px-5 py-3 bg-ancient-accent dark:bg-ancient-accent-dark text-ancient-white rounded-full shadow-lg hover:bg-ancient-accent/90 dark:hover:bg-ancient-accent-dark/90 transition-colors border border-ancient-accent/30 dark:border-ancient-accent-dark/30"
class="flex items-center space-x-2 px-5 py-3 bg-ancient-accent dark:bg-ancient-accent-dark text-ancient-white rounded-full shadow-lg hover:bg-ancient-accent/90 dark:hover:bg-ancient-accent-dark/90 transition-colors border border-ancient-accent/30 dark:border-ancient-accent-dark/30 backdrop-blur-sm"
aria-label="返回所有文化"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -117,7 +117,15 @@ const relatedCultures = allCultures
<!-- 文化图片 - 古籍风格 -->
<div class="border border-ancient-accent/40 dark:border-ancient-accent-dark/40 overflow-hidden shadow-md">
<div class="h-64 bg-ancient-paper-light/70 dark:bg-ancient-paper-dark/70 flex items-center justify-center relative">
<span class="text-ancient-black/40 dark:text-ancient-white/40 font-ancient">{entry.data.title} 图片</span>
{entry.data.image ? (
<img
src={entry.data.image}
alt={entry.data.title}
class="w-full h-full object-cover"
/>
) : (
<span class="text-ancient-black/40 dark:text-ancient-white/40 font-ancient">{entry.data.title} 图片</span>
)}
</div>
</div>
@ -188,8 +196,16 @@ const relatedCultures = allCultures
class="block group"
>
<div class="flex items-start space-x-3">
<div class="w-16 h-16 flex-shrink-0 bg-ancient-paper/70 dark:bg-ancient-paper-dark/70 border border-ancient-accent/20 dark:border-ancient-accent-dark/20 rounded flex items-center justify-center">
<span class="text-xs text-ancient-black/40 dark:text-ancient-white/40">图片</span>
<div class="w-16 h-16 flex-shrink-0 bg-ancient-paper/70 dark:bg-ancient-paper-dark/70 border border-ancient-accent/20 dark:border-ancient-accent-dark/20 rounded flex items-center justify-center overflow-hidden">
{culture.data.image ? (
<img
src={culture.data.image}
alt={culture.data.title}
class="w-full h-full object-cover"
/>
) : (
<span class="text-xs text-ancient-black/40 dark:text-ancient-white/40">图片</span>
)}
</div>
<div>
<h4 class="text-base font-ancient-heading text-ancient-black dark:text-ancient-white group-hover:text-ancient-accent dark:group-hover:text-ancient-accent-dark transition-colors">
@ -210,7 +226,7 @@ const relatedCultures = allCultures
<div class="hidden md:block">
<a
href="/culture"
class="block w-full py-4 mt-8 mb-4 text-center bg-ancient-accent dark:bg-ancient-accent-dark text-ancient-white rounded-md hover:bg-ancient-accent/90 dark:hover:bg-ancient-accent-dark/90 transition-colors font-ancient shadow-md border border-ancient-accent/50 dark:border-ancient-accent-dark/50 text-lg"
class="block w-full py-4 mt-8 mb-4 text-center bg-ancient-accent dark:bg-ancient-accent-dark text-ancient-white rounded-md hover:bg-ancient-accent/90 dark:hover:bg-ancient-accent-dark/90 transition-colors font-ancient shadow-lg border border-ancient-accent/50 dark:border-ancient-accent-dark/50 text-lg backdrop-blur-sm"
>
返回所有文化
</a>

View File

@ -4,9 +4,9 @@ import MainLayout from "../../components/MainLayout.astro";
// 获取URL参数
const { searchParams } = Astro.url;
const categoryParam = searchParams.get('category');
const cityParam = searchParams.get('city');
const tagParam = searchParams.get('tag');
const categoryParam = searchParams.getAll('category');
const cityParam = searchParams.getAll('city');
const tagParam = searchParams.getAll('tag');
const searchParam = searchParams.get('search');
const pageParam = parseInt(searchParams.get('page') || '1');
@ -23,23 +23,29 @@ const sortByDate = <T extends { data: { pubDate?: Date | string, updatedDate?: D
let filteredCultures = [...cultures];
// 应用分类筛选
if (categoryParam) {
if (categoryParam.length > 0) {
filteredCultures = filteredCultures.filter(culture =>
culture.data.category && culture.data.category.toLowerCase() === categoryParam.toLowerCase()
culture.data.category && categoryParam.some(cat =>
cat.toLowerCase() === culture.data.category?.toLowerCase()
)
);
}
// 应用城市筛选
if (cityParam) {
if (cityParam.length > 0) {
filteredCultures = filteredCultures.filter(culture =>
culture.data.city && culture.data.city.some(city => city.toLowerCase() === cityParam.toLowerCase())
culture.data.city && cityParam.some(city =>
culture.data.city?.some(c => c.toLowerCase() === city.toLowerCase())
)
);
}
// 应用标签筛选
if (tagParam) {
if (tagParam.length > 0) {
filteredCultures = filteredCultures.filter(culture =>
culture.data.tags && culture.data.tags.some(tag => tag.toLowerCase() === tagParam.toLowerCase())
culture.data.tags && tagParam.some(tag =>
culture.data.tags?.some(t => t.toLowerCase() === tag.toLowerCase())
)
);
}
@ -130,27 +136,38 @@ const totalPages = Math.ceil(sortedCultures.length / itemsPerPage);
const currentPageCultures = sortedCultures.slice((page - 1) * itemsPerPage, page * itemsPerPage);
// 标记当前激活的筛选条件
const activeCategory = categoryParam || '';
const activeCity = cityParam || '';
const activeTag = tagParam || '';
const activeCategories = categoryParam || [];
const activeCities = cityParam || [];
const activeTags = tagParam || [];
const activeSearch = searchParam || '';
// 构建基础URL不包含分页参数
const buildUrl = (params: Record<string, string | null>) => {
const buildUrl = (params: Record<string, string[] | string | null>) => {
const url = new URL(Astro.url);
const newParams = new URLSearchParams();
// 添加现有参数
if (categoryParam && params.category !== null) newParams.set('category', categoryParam);
if (cityParam && params.city !== null) newParams.set('city', cityParam);
if (tagParam && params.tag !== null) newParams.set('tag', tagParam);
if (searchParam && params.search !== null) newParams.set('search', searchParam);
if (categoryParam.length > 0 && params.category !== null) {
categoryParam.forEach(cat => newParams.append('category', cat));
}
if (cityParam.length > 0 && params.city !== null) {
cityParam.forEach(city => newParams.append('city', city));
}
if (tagParam.length > 0 && params.tag !== null) {
tagParam.forEach(tag => newParams.append('tag', tag));
}
if (searchParam && params.search !== null) {
newParams.set('search', searchParam);
}
// 覆盖或添加新参数
Object.entries(params).forEach(([key, value]) => {
if (value === null) {
newParams.delete(key);
} else if (value) {
} else if (Array.isArray(value)) {
newParams.delete(key);
value.forEach(v => newParams.append(key, v));
} else if (typeof value === 'string') {
newParams.set(key, value);
}
});
@ -202,30 +219,30 @@ const buildUrl = (params: Record<string, string | null>) => {
</div>
<!-- 筛选状态显示 -->
{(categoryParam || cityParam || tagParam || searchParam) && (
{(categoryParam.length > 0 || cityParam.length > 0 || tagParam.length > 0 || searchParam) && (
<div class="mb-8 max-w-4xl mx-auto">
<div class="p-4 border-l-4 border-ancient-accent dark:border-ancient-accent-dark bg-ancient-paper-light/80 dark:bg-ancient-paper-dark-light/80 flex justify-between items-center">
<div class="flex flex-wrap gap-2 items-center">
<span class="text-ancient-black/80 dark:text-ancient-white/80 font-ancient-small">当前筛选:</span>
{categoryParam && (
{categoryParam.length > 0 && (
<div class="px-3 py-1 bg-ancient-paper/90 dark:bg-ancient-paper-dark/90 border border-ancient-accent/40 dark:border-ancient-accent-dark/40 text-sm font-ancient-small text-ancient-black dark:text-ancient-white rounded flex items-center">
<span class="mr-1">分类:</span>
<span class="font-medium">{categoryParam}</span>
<span class="font-medium">{categoryParam.join(', ')}</span>
</div>
)}
{cityParam && (
{cityParam.length > 0 && (
<div class="px-3 py-1 bg-amber-100/50 dark:bg-amber-900/30 border border-amber-200/70 dark:border-amber-700/40 text-sm font-ancient-small text-ancient-black dark:text-ancient-white rounded flex items-center">
<span class="mr-1">城市:</span>
<span class="font-medium">{cityParam}</span>
<span class="font-medium">{cityParam.join(', ')}</span>
</div>
)}
{tagParam && (
{tagParam.length > 0 && (
<div class="px-3 py-1 bg-ancient-paper/90 dark:bg-ancient-paper-dark/90 border border-ancient-accent/40 dark:border-ancient-accent-dark/40 text-sm font-ancient-small text-ancient-black dark:text-ancient-white rounded flex items-center">
<span class="mr-1">标签:</span>
<span class="font-medium">{tagParam}</span>
<span class="font-medium">{tagParam.join(', ')}</span>
</div>
)}
@ -259,11 +276,11 @@ const buildUrl = (params: Record<string, string | null>) => {
</svg>
<h3 class="text-xl font-ancient text-ancient-black dark:text-ancient-white mb-2">未找到相关文化内容</h3>
<p class="text-ancient-black/80 dark:text-ancient-white/80 font-ancient-body mb-4">
{categoryParam && `没有找到分类为"${categoryParam}"的内容。`}
{cityParam && `没有找到位于"${cityParam}"的内容。`}
{tagParam && `没有找到标签为"${tagParam}"的内容。`}
{categoryParam.length > 0 && `没有找到分类为"${categoryParam.join(', ')}"的内容。`}
{cityParam.length > 0 && `没有找到位于"${cityParam.join(', ')}"的内容。`}
{tagParam.length > 0 && `没有找到标签为"${tagParam.join(', ')}"的内容。`}
{searchParam && `没有找到包含"${searchParam}"的内容。`}
{!categoryParam && !cityParam && !tagParam && !searchParam && '没有找到符合条件的内容。'}
{!categoryParam.length && !cityParam.length && !tagParam.length && !searchParam && '没有找到符合条件的内容。'}
</p>
<a href="/culture" class="inline-block mt-2 px-4 py-2 bg-ancient-accent dark:bg-ancient-accent-dark text-white rounded-md font-ancient-small">查看全部文化内容</a>
</div>
@ -289,9 +306,9 @@ const buildUrl = (params: Record<string, string | null>) => {
class="w-full text-ancient-black dark:text-ancient-white bg-ancient-paper-light/90 dark:bg-ancient-paper-dark/50 border border-ancient-accent/30 dark:border-ancient-accent-dark/30 rounded-full py-2 pl-3 pr-10 focus:outline-none focus:border-ancient-accent/70 dark:focus:border-ancient-accent-dark/70 font-ancient-body"
/>
<!-- 保留现有参数 -->
{categoryParam && <input type="hidden" name="category" value={categoryParam} />}
{cityParam && <input type="hidden" name="city" value={cityParam} />}
{tagParam && <input type="hidden" name="tag" value={tagParam} />}
{categoryParam.length > 0 && <input type="hidden" name="category" value={categoryParam.join(',')} />}
{cityParam.length > 0 && <input type="hidden" name="city" value={cityParam.join(',')} />}
{tagParam.length > 0 && <input type="hidden" name="tag" value={tagParam.join(',')} />}
<button type="submit" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-ancient-accent dark:text-ancient-accent-dark" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -326,16 +343,27 @@ const buildUrl = (params: Record<string, string | null>) => {
<div class="flex flex-wrap gap-2">
{categories.slice(0, 6).map((category) => (
<a
href={buildUrl({ category: category.name === activeCategory ? null : category.name })}
href={buildUrl({
category: activeCategories.includes(category.name)
? activeCategories.filter(c => c !== category.name)
: [...activeCategories, category.name]
})}
class:list={[
"px-3 py-1.5 bg-ancient-paper/70 dark:bg-ancient-paper-dark/70 text-sm font-ancient-small border rounded-md",
category.name === activeCategory
? "border-ancient-accent dark:border-ancient-accent-dark text-ancient-accent dark:text-ancient-accent-dark font-medium"
: "border-ancient-accent/30 dark:border-ancient-accent-dark/30 text-ancient-black dark:text-ancient-white hover:border-ancient-accent/50 dark:hover:border-ancient-accent-dark/50"
"flex items-center group cursor-pointer hover:bg-amber-100/50 dark:hover:bg-amber-800/20 p-1.5 rounded",
{ "bg-amber-100/80 dark:bg-amber-800/30": activeCategories.includes(category.name) }
]}
>
{category.name}
<span class="ml-1 text-xs text-ancient-black/60 dark:text-ancient-white/60">({category.count})</span>
<div class={`w-4 h-4 border border-amber-300/50 dark:border-amber-700/50 mr-2 flex-shrink-0 group-hover:bg-amber-200/40 dark:group-hover:bg-amber-700/30 relative`}>
{activeCategories.includes(category.name) && (
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-2 h-3 border-r-2 border-b-2 border-amber-400 dark:border-amber-600 transform rotate-45 -translate-y-1/4"></div>
</div>
)}
</div>
<div class="text-ancient-black dark:text-ancient-white group-hover:text-amber-700 dark:group-hover:text-amber-300">
<span>{category.name}</span>
<span class="text-ancient-black/60 dark:text-ancient-white/60 text-sm">({category.count})</span>
</div>
</a>
))}
</div>
@ -353,16 +381,27 @@ const buildUrl = (params: Record<string, string | null>) => {
<div class="flex flex-wrap gap-2">
{cities.slice(0, 6).map((city) => (
<a
href={buildUrl({ city: city.name === activeCity ? null : city.name })}
href={buildUrl({
city: activeCities.includes(city.name)
? activeCities.filter(c => c !== city.name)
: [...activeCities, city.name]
})}
class:list={[
"px-3 py-1.5 bg-ancient-paper/70 dark:bg-ancient-paper-dark/70 text-sm font-ancient-small border rounded-md",
city.name === activeCity
? "border-ancient-accent dark:border-ancient-accent-dark text-ancient-accent dark:text-ancient-accent-dark font-medium"
: "border-ancient-accent/30 dark:border-ancient-accent-dark/30 text-ancient-black dark:text-ancient-white hover:border-ancient-accent/50 dark:hover:border-ancient-accent-dark/50"
"flex items-center group cursor-pointer hover:bg-amber-100/50 dark:hover:bg-amber-800/20 p-1.5 rounded",
{ "bg-amber-100/80 dark:bg-amber-800/30": activeCities.includes(city.name) }
]}
>
{city.name}
<span class="ml-1 text-xs text-ancient-black/60 dark:text-ancient-white/60">({city.count})</span>
<div class={`w-4 h-4 border border-amber-300/50 dark:border-amber-700/50 mr-2 flex-shrink-0 group-hover:bg-amber-200/40 dark:group-hover:bg-amber-700/30 relative`}>
{activeCities.includes(city.name) && (
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-2 h-3 border-r-2 border-b-2 border-amber-400 dark:border-amber-600 transform rotate-45 -translate-y-1/4"></div>
</div>
)}
</div>
<div class="text-ancient-black dark:text-ancient-white group-hover:text-amber-700 dark:group-hover:text-amber-300">
<span>{city.name}</span>
<span class="text-ancient-black/60 dark:text-ancient-white/60 text-sm">({city.count})</span>
</div>
</a>
))}
</div>
@ -377,17 +416,21 @@ const buildUrl = (params: Record<string, string | null>) => {
特色标签
</h3>
<div class="flex flex-wrap gap-2">
{allTags.slice(0, 12).map((tag) => (
{allTags.slice(0, 6).map((tag) => (
<a
href={buildUrl({ tag: tag.name === activeTag ? null : tag.name })}
href={buildUrl({
tag: activeTags.includes(tag.name)
? activeTags.filter(t => t !== tag.name)
: [...activeTags, tag.name]
})}
class:list={[
"px-3 py-1.5 bg-ancient-paper/70 dark:bg-ancient-paper-dark/70 text-sm font-ancient-small border rounded-md",
tag.name === activeTag
? "border-ancient-accent dark:border-ancient-accent-dark text-ancient-accent dark:text-ancient-accent-dark font-medium"
"px-3 py-1.5 bg-ancient-paper/70 dark:bg-ancient-paper-dark/70 text-sm font-ancient-small border rounded-md flex items-center",
activeTags.includes(tag.name)
? "border-ancient-accent dark:border-ancient-accent-dark text-ancient-accent dark:text-ancient-accent-dark font-medium bg-ancient-accent/20 dark:bg-ancient-accent-dark/20"
: "border-ancient-accent/30 dark:border-ancient-accent-dark/30 text-ancient-black dark:text-ancient-white hover:border-ancient-accent/50 dark:hover:border-ancient-accent-dark/50"
]}
>
{tag.name}
<span>{tag.name}</span>
<span class="ml-1 text-xs text-ancient-black/60 dark:text-ancient-white/60">({tag.count})</span>
</a>
))}
@ -426,9 +469,9 @@ const buildUrl = (params: Record<string, string | null>) => {
class="w-full px-4 py-2 border-2 border-ancient-accent/30 dark:border-ancient-accent-dark/30 bg-ancient-paper/70 dark:bg-ancient-paper-dark/70 placeholder-ancient-black/50 dark:placeholder-ancient-white/50 text-ancient-black dark:text-ancient-white font-ancient-body focus:outline-none focus:border-ancient-accent dark:focus:border-ancient-accent-dark"
/>
<!-- 保留现有参数 -->
{categoryParam && <input type="hidden" name="category" value={categoryParam} />}
{cityParam && <input type="hidden" name="city" value={cityParam} />}
{tagParam && <input type="hidden" name="tag" value={tagParam} />}
{categoryParam.length > 0 && <input type="hidden" name="category" value={categoryParam.join(',')} />}
{cityParam.length > 0 && <input type="hidden" name="city" value={cityParam.join(',')} />}
{tagParam.length > 0 && <input type="hidden" name="tag" value={tagParam.join(',')} />}
<button type="submit" class="absolute right-3 top-2 text-ancient-accent/70 dark:text-ancient-accent-dark/70">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@ -450,20 +493,24 @@ const buildUrl = (params: Record<string, string | null>) => {
<div class="space-y-2 font-ancient-body max-h-48 overflow-y-auto scrollbar-thin scrollbar-thumb-ancient-accent/30 dark:scrollbar-thumb-ancient-accent-dark/30 pr-2">
{categories.map((category) => (
<a
href={buildUrl({ category: category.name === activeCategory ? null : category.name })}
href={buildUrl({
category: activeCategories.includes(category.name)
? activeCategories.filter(c => c !== category.name)
: [...activeCategories, category.name]
})}
class:list={[
"flex items-center group cursor-pointer hover:bg-ancient-accent/10 dark:hover:bg-ancient-accent-dark/10 p-1.5 rounded",
{ "bg-ancient-accent/20 dark:bg-ancient-accent-dark/20": category.name === activeCategory }
"flex items-center group cursor-pointer hover:bg-amber-100/50 dark:hover:bg-amber-800/20 p-1.5 rounded",
{ "bg-amber-100/80 dark:bg-amber-800/30": activeCategories.includes(category.name) }
]}
>
<div class="w-4 h-4 border border-ancient-accent/50 dark:border-ancient-accent-dark/50 mr-2 flex-shrink-0 group-hover:bg-ancient-accent/20 dark:group-hover:bg-ancient-accent-dark/20 relative">
{category.name === activeCategory && (
<div class={`w-4 h-4 border border-amber-300/50 dark:border-amber-700/50 mr-2 flex-shrink-0 group-hover:bg-amber-200/40 dark:group-hover:bg-amber-700/30 relative`}>
{activeCategories.includes(category.name) && (
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-2 h-3 border-r-2 border-b-2 border-ancient-accent dark:border-ancient-accent-dark transform rotate-45 -translate-y-1/4"></div>
<div class="w-2 h-3 border-r-2 border-b-2 border-amber-400 dark:border-amber-600 transform rotate-45 -translate-y-1/4"></div>
</div>
)}
</div>
<div class="text-ancient-black dark:text-ancient-white group-hover:text-ancient-accent dark:group-hover:text-ancient-accent-dark">
<div class="text-ancient-black dark:text-ancient-white group-hover:text-amber-700 dark:group-hover:text-amber-300">
<span>{category.name}</span>
<span class="text-ancient-black/60 dark:text-ancient-white/60 text-sm">({category.count})</span>
</div>
@ -485,14 +532,18 @@ const buildUrl = (params: Record<string, string | null>) => {
<div class="space-y-2 font-ancient-body max-h-48 overflow-y-auto scrollbar-thin scrollbar-thumb-ancient-accent/30 dark:scrollbar-thumb-ancient-accent-dark/30 pr-2">
{cities.map((city) => (
<a
href={buildUrl({ city: city.name === activeCity ? null : city.name })}
href={buildUrl({
city: activeCities.includes(city.name)
? activeCities.filter(c => c !== city.name)
: [...activeCities, city.name]
})}
class:list={[
"flex items-center group cursor-pointer hover:bg-amber-100/50 dark:hover:bg-amber-800/20 p-1.5 rounded",
{ "bg-amber-100/80 dark:bg-amber-800/30": city.name === activeCity }
{ "bg-amber-100/80 dark:bg-amber-800/30": activeCities.includes(city.name) }
]}
>
<div class="w-4 h-4 border border-amber-300/50 dark:border-amber-700/50 mr-2 flex-shrink-0 group-hover:bg-amber-200/40 dark:group-hover:bg-amber-700/30 relative">
{city.name === activeCity && (
<div class={`w-4 h-4 border border-amber-300/50 dark:border-amber-700/50 mr-2 flex-shrink-0 group-hover:bg-amber-200/40 dark:group-hover:bg-amber-700/30 relative`}>
{activeCities.includes(city.name) && (
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-2 h-3 border-r-2 border-b-2 border-amber-400 dark:border-amber-600 transform rotate-45 -translate-y-1/4"></div>
</div>
@ -517,17 +568,21 @@ const buildUrl = (params: Record<string, string | null>) => {
</h3>
<div class="flex flex-wrap gap-2">
{allTags.slice(0, 12).map((tag) => (
{allTags.slice(0, 6).map((tag) => (
<a
href={buildUrl({ tag: tag.name === activeTag ? null : tag.name })}
href={buildUrl({
tag: activeTags.includes(tag.name)
? activeTags.filter(t => t !== tag.name)
: [...activeTags, tag.name]
})}
class:list={[
"px-3 py-1.5 bg-ancient-paper/70 dark:bg-ancient-paper-dark/70 text-sm font-ancient-small border rounded-md",
tag.name === activeTag
? "border-ancient-accent dark:border-ancient-accent-dark text-ancient-accent dark:text-ancient-accent-dark font-medium"
"px-3 py-1.5 bg-ancient-paper/70 dark:bg-ancient-paper-dark/70 text-sm font-ancient-small border rounded-md flex items-center",
activeTags.includes(tag.name)
? "border-ancient-accent dark:border-ancient-accent-dark text-ancient-accent dark:text-ancient-accent-dark font-medium bg-ancient-accent/20 dark:bg-ancient-accent-dark/20"
: "border-ancient-accent/30 dark:border-ancient-accent-dark/30 text-ancient-black dark:text-ancient-white hover:border-ancient-accent/50 dark:hover:border-ancient-accent-dark/50"
]}
>
{tag.name}
<span>{tag.name}</span>
<span class="ml-1 text-xs text-ancient-black/60 dark:text-ancient-white/60">({tag.count})</span>
</a>
))}
@ -557,9 +612,17 @@ const buildUrl = (params: Record<string, string | null>) => {
<div class="absolute top-2 left-2 w-8 h-8 bg-ink-decoration opacity-10 dark:opacity-15"></div>
<div class="absolute bottom-2 right-2 w-8 h-8 bg-ink-decoration opacity-10 dark:opacity-15 rotate-180"></div>
<!-- 默认内容显示 -->
<!-- 内容显示 -->
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-ancient-black/40 dark:text-amber-100/60 font-ancient">{culture.data.title}</span>
{culture.data.image ? (
<img
src={culture.data.image}
alt={culture.data.title}
class="w-full h-full object-cover"
/>
) : (
<span class="text-ancient-black/40 dark:text-amber-100/60 font-ancient">{culture.data.title}</span>
)}
</div>
<!-- 印章效果 -->
@ -591,33 +654,23 @@ const buildUrl = (params: Record<string, string | null>) => {
{culture.data.description}
</p>
<div class="flex flex-wrap gap-1.5 mb-4 min-h-[2rem]">
{culture.data.tags.slice(0, 3).map((tag: string) => (
<span class="px-2 py-1 bg-ancient-paper-light/50 dark:bg-dark-surface text-ancient-black/70 dark:text-amber-100/90 text-xs font-ancient-small border border-ancient-accent/20 dark:border-amber-700/40 tag-item" data-tag-value={tag.toLowerCase()}>
<div class="flex overflow-x-auto gap-1.5 mb-4 min-h-[2rem] scrollbar-thin scrollbar-thumb-ancient-accent/30 dark:scrollbar-thumb-ancient-accent-dark/30 scrollbar-track-ancient-paper/20 dark:scrollbar-track-ancient-paper-dark/20 pb-2">
{culture.data.tags.map((tag: string) => (
<span class="flex-shrink-0 px-2 py-1 bg-ancient-paper-light/50 dark:bg-dark-surface text-ancient-black/70 dark:text-amber-100/90 text-xs font-ancient-small border border-ancient-accent/20 dark:border-amber-700/40" data-tag-value={tag.toLowerCase()}>
{tag}
</span>
))}
{culture.data.tags.length > 3 && (
<span class="px-2 py-1 bg-ancient-paper-light/50 dark:bg-dark-surface text-ancient-black/70 dark:text-amber-100/90 text-xs font-ancient-small border border-ancient-accent/20 dark:border-amber-700/40">
+{culture.data.tags.length - 3}
</span>
)}
</div>
{/* 在卡片中添加城市信息显示 */}
<div class="min-h-[1.5rem]">
{culture.data.city && culture.data.city.length > 0 && (
<div class="flex flex-wrap gap-1.5 mt-1 mb-2">
{culture.data.city.slice(0, 2).map((cityName: string) => (
<span class="px-2 py-1 bg-amber-100/50 dark:bg-amber-900/50 text-ancient-black/70 dark:text-amber-100 text-xs font-ancient-small border border-amber-200/50 dark:border-amber-600/50 city-tag" data-city-value={cityName.toLowerCase()}>
<div class="flex overflow-x-auto gap-1.5 mt-1 mb-2 scrollbar-thin scrollbar-thumb-ancient-accent/30 dark:scrollbar-thumb-ancient-accent-dark/30 scrollbar-track-ancient-paper/20 dark:scrollbar-track-ancient-paper-dark/20 pb-2">
{culture.data.city.map((cityName: string) => (
<span class="flex-shrink-0 px-2 py-1 bg-amber-100/50 dark:bg-amber-900/50 text-ancient-black/70 dark:text-amber-100 text-xs font-ancient-small border border-amber-200/50 dark:border-amber-600/50 city-tag" data-city-value={cityName.toLowerCase()}>
{cityName}
</span>
))}
{culture.data.city.length > 2 && (
<span class="px-2 py-1 bg-amber-100/50 dark:bg-amber-900/50 text-ancient-black/70 dark:text-amber-100 text-xs font-ancient-small border border-amber-200/50 dark:border-amber-600/50">
+{culture.data.city.length - 2}
</span>
)}
</div>
)}
</div>
@ -748,9 +801,6 @@ const buildUrl = (params: Record<string, string | null>) => {
<script>
document.addEventListener('DOMContentLoaded', () => {
// 移动端筛选抽屉
const mobileFilterToggle = document.getElementById('mobile-filter-toggle');
const mobileFilterDrawer = document.getElementById('mobile-filter-drawer');
@ -761,7 +811,7 @@ const buildUrl = (params: Record<string, string | null>) => {
if (mobileFilterToggle && mobileFilterDrawer) {
mobileFilterToggle.addEventListener('click', () => {
mobileFilterDrawer.classList.remove('translate-x-full');
document.body.classList.add('overflow-hidden'); // 防止背景滚动
document.body.classList.add('overflow-hidden');
});
}
@ -783,70 +833,35 @@ const buildUrl = (params: Record<string, string | null>) => {
mobileFilterClose.addEventListener('click', closeFilterDrawer);
}
// 获取所有筛选链接
const filterLinks = document.querySelectorAll('a[href*="?category"], a[href*="?city"], a[href*="?taste"], a[href*="?tag"], a[href*="?ingredient"]');
// 为每个筛选链接添加点击事件监听器
filterLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault(); // 阻止默认行为
const href = (link as HTMLAnchorElement).href;
const url = new URL(href);
// 获取当前的筛选参数
const params = new URLSearchParams(window.location.search);
const clickedParam = Array.from(url.searchParams.entries())[0];
if (clickedParam) {
const [paramName, paramValue] = clickedParam;
// 如果当前参数值与点击的值相同,则移除该参数
if (params.get(paramName) === paramValue) {
params.delete(paramName);
} else {
// 否则设置新的参数值
params.set(paramName, paramValue);
}
// 更新 URL但不刷新页面
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
window.history.pushState({}, '', newUrl);
// 应用筛选
applyFilters();
}
});
});
// 应用筛选函数
function applyFilters() {
const params = new URLSearchParams(window.location.search);
const category = params.get('category')?.toLowerCase();
const city = params.get('city')?.toLowerCase();
const tag = params.get('tag')?.toLowerCase();
const search = params.get('search')?.toLowerCase();
const category = params.getAll('category');
const city = params.getAll('city');
const tag = params.getAll('tag');
const search = params.get('search');
// 获取所有文化卡片
const cards = document.querySelectorAll('.ancient-card');
let visibleCount = 0;
cards.forEach(card => {
cards.forEach((card, index) => {
let shouldShow = true;
// 检查分类
if (category && shouldShow) {
if (category.length > 0 && shouldShow) {
const cardCategory = card.getAttribute('data-category');
if (cardCategory !== category) {
if (!category.some(cat => cat.toLowerCase() === cardCategory)) {
shouldShow = false;
}
}
// 检查城市
if (city && shouldShow) {
if (city.length > 0 && shouldShow) {
try {
const cardCities = JSON.parse(card.getAttribute('data-cities') || '[]');
if (!cardCities.includes(city)) {
if (!city.some(city => cardCities.includes(city))) {
shouldShow = false;
}
} catch (e) {
@ -855,10 +870,10 @@ const buildUrl = (params: Record<string, string | null>) => {
}
// 检查标签
if (tag && shouldShow) {
if (tag.length > 0 && shouldShow) {
try {
const cardTags = JSON.parse(card.getAttribute('data-tags') || '[]');
if (!cardTags.includes(tag)) {
if (!tag.some(tag => cardTags.includes(tag))) {
shouldShow = false;
}
} catch (e) {
@ -881,14 +896,14 @@ const buildUrl = (params: Record<string, string | null>) => {
}
}
// 更新卡片显示状态
const cardContainer = card.closest('a')?.parentElement;
if (cardContainer) {
// 更新卡片显示状态 - 修复隐藏逻辑
const cardLink = card.closest('a');
if (cardLink) {
if (shouldShow) {
cardContainer.style.display = '';
cardLink.style.display = '';
visibleCount++;
} else {
cardContainer.style.display = 'none';
cardLink.style.display = 'none';
}
}
});
@ -910,19 +925,19 @@ const buildUrl = (params: Record<string, string | null>) => {
if (search) {
message += `没有找到包含"${search}"的内容。`;
}
if (category) {
message += `没有找到分类为"${category}"的内容。`;
if (category.length > 0) {
message += `没有找到分类为"${category.join(', ')}"的内容。`;
}
if (city) {
message += `没有找到位于"${city}"的内容。`;
if (city.length > 0) {
message += `没有找到位于"${city.join(', ')}"的内容。`;
}
if (tag) {
message += `没有找到标签为"${tag}"的内容。`;
if (tag.length > 0) {
message += `没有找到标签为"${tag.join(', ')}"的内容。`;
}
searchTermMessage.textContent = message;
}
}
} else {
} else {
if (gridContainer) gridContainer.style.display = 'grid';
if (noResultsMessage) noResultsMessage.style.display = 'none';
}
@ -931,125 +946,136 @@ const buildUrl = (params: Record<string, string | null>) => {
// 更新筛选 UI
function updateFilterUI() {
const params = new URLSearchParams(window.location.search);
const currentCategory = params.get('category');
const currentCity = params.get('city');
const currentTag = params.get('tag');
const currentCategory = params.getAll('category');
const currentCity = params.getAll('city');
const currentTag = params.getAll('tag');
const currentSearch = params.get('search');
console.log('当前筛选参数:', {
category: currentCategory,
city: currentCity,
tag: currentTag,
search: currentSearch
// 更新标签选中状态
const tagLinks = document.querySelectorAll('a[href*="?tag"]');
tagLinks.forEach((link, index) => {
try {
const url = new URL((link as HTMLAnchorElement).href);
const linkTag = url.searchParams.getAll('tag');
// 检查是否选中
const isSelected = currentTag.some(tag => linkTag.includes(tag));
// 更新标签样式
if (isSelected) {
link.classList.add('border-ancient-accent', 'dark:border-ancient-accent-dark');
link.classList.remove('border-ancient-accent/30', 'dark:border-ancient-accent-dark/30');
} else {
link.classList.remove('border-ancient-accent', 'dark:border-ancient-accent-dark');
link.classList.add('border-ancient-accent/30', 'dark:border-ancient-accent-dark/30');
}
} catch (e) {
// 处理错误
}
});
// 更新分类选中状态
document.querySelectorAll('a[href*="?category"]').forEach(link => {
const categoryLinks = document.querySelectorAll('a[href*="?category"]');
categoryLinks.forEach((link, index) => {
try {
const url = new URL((link as HTMLAnchorElement).href);
const linkCategory = url.searchParams.get('category');
const linkCategory = url.searchParams.getAll('category');
// 检查是否选中
const isSelected = currentCategory === linkCategory;
console.log('分类项:', linkCategory, '是否选中:', isSelected);
const isSelected = currentCategory.some(cat => linkCategory.includes(cat));
// 处理带复选框的分类项(桌面端左侧栏)
const checkbox = link.querySelector('div.w-4.h-4');
if (checkbox) {
console.log('找到分类复选框:', checkbox);
if (isSelected) {
checkbox.className = 'w-4 h-4 border border-ancient-accent/50 dark:border-ancient-accent-dark/50 mr-2 flex-shrink-0 relative';
checkbox.className = 'w-4 h-4 border border-ancient-accent dark:border-ancient-accent-dark mr-2 flex-shrink-0 relative';
checkbox.innerHTML = '<div class="absolute inset-0 flex items-center justify-center"><div class="w-2 h-3 border-r-2 border-b-2 border-ancient-accent dark:border-ancient-accent-dark transform rotate-45 -translate-y-1/4"></div></div>';
// 同时更新父元素背景
link.classList.add('bg-ancient-accent/10', 'dark:bg-ancient-accent-dark/10');
link.classList.add('bg-ancient-accent/20', 'dark:bg-ancient-accent-dark/20');
} else {
checkbox.className = 'w-4 h-4 border border-ancient-accent/50 dark:border-ancient-accent-dark/50 mr-2 flex-shrink-0 group-hover:bg-ancient-accent/20 dark:group-hover:bg-ancient-accent-dark/20';
checkbox.className = 'w-4 h-4 border border-ancient-accent/30 dark:border-ancient-accent-dark/30 mr-2 flex-shrink-0 group-hover:bg-ancient-accent/20 dark:group-hover:bg-ancient-accent-dark/20';
checkbox.innerHTML = '';
link.classList.remove('bg-ancient-accent/10', 'dark:bg-ancient-accent-dark/10');
link.classList.remove('bg-ancient-accent/20', 'dark:bg-ancient-accent-dark/20');
}
}
} catch (e) {
console.error('Error updating category UI:', e);
// 处理错误
}
});
// 更新城市选中状态
document.querySelectorAll('a[href*="?city"]').forEach(link => {
const cityLinks = document.querySelectorAll('a[href*="?city"]');
cityLinks.forEach((link, index) => {
try {
const url = new URL((link as HTMLAnchorElement).href);
const linkCity = url.searchParams.get('city');
const linkCity = url.searchParams.getAll('city');
// 检查是否选中
const isSelected = currentCity === linkCity;
console.log('城市项:', linkCity, '是否选中:', isSelected);
const isSelected = currentCity.some(city => linkCity.includes(city));
// 处理带复选框的城市项(桌面端左侧栏)
const checkbox = link.querySelector('div.w-4.h-4');
if (checkbox) {
console.log('找到城市复选框:', checkbox);
if (isSelected) {
checkbox.className = 'w-4 h-4 border border-amber-300/50 dark:border-amber-700/50 mr-2 flex-shrink-0 relative';
checkbox.innerHTML = '<div class="absolute inset-0 flex items-center justify-center"><div class="w-2 h-3 border-r-2 border-b-2 border-amber-400 dark:border-amber-600 transform rotate-45 -translate-y-1/4"></div></div>';
// 移除橙色背景的添加
checkbox.className = 'w-4 h-4 border border-ancient-accent dark:border-ancient-accent-dark mr-2 flex-shrink-0 relative';
checkbox.innerHTML = '<div class="absolute inset-0 flex items-center justify-center"><div class="w-2 h-3 border-r-2 border-b-2 border-ancient-accent dark:border-ancient-accent-dark transform rotate-45 -translate-y-1/4"></div></div>';
link.classList.add('bg-ancient-accent/20', 'dark:bg-ancient-accent-dark/20');
} else {
checkbox.className = 'w-4 h-4 border border-amber-300/50 dark:border-amber-700/50 mr-2 flex-shrink-0 group-hover:bg-amber-200/40 dark:group-hover:bg-amber-700/30';
checkbox.className = 'w-4 h-4 border border-ancient-accent/30 dark:border-ancient-accent-dark/30 mr-2 flex-shrink-0 group-hover:bg-ancient-accent/20 dark:group-hover:bg-ancient-accent-dark/20';
checkbox.innerHTML = '';
// 移除背景移除代码,因为不再添加背景
link.classList.remove('bg-ancient-accent/20', 'dark:bg-ancient-accent-dark/20');
}
}
} catch (e) {
console.error('Error updating city UI:', e);
// 处理错误
}
});
}
// 更新标签选中状态
document.querySelectorAll('a[href*="?tag"]').forEach(link => {
try {
const url = new URL((link as HTMLAnchorElement).href);
const linkTag = url.searchParams.get('tag');
// 获取所有筛选链接
const filterLinks = document.querySelectorAll('a[href*="?category"], a[href*="?city"], a[href*="?tag"]');
// 检查是否选中
const isSelected = currentTag === linkTag;
// 为每个筛选链接添加点击事件监听器
filterLinks.forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault(); // 阻止默认行为
// 标签可能有多种不同的样式结构,需要针对不同结构进行处理
// 处理带有类名px-3的标签桌面端和移动端通用标签样式
if (link.classList.contains('px-3')) {
if (isSelected) {
link.classList.add('border-ancient-accent', 'dark:border-ancient-accent-dark', 'text-ancient-accent', 'dark:text-ancient-accent-dark', 'font-medium', 'bg-ancient-accent/10', 'dark:bg-ancient-accent-dark/10', 'shadow-sm');
link.classList.remove('border-ancient-accent/30', 'dark:border-ancient-accent-dark/30');
} else {
link.classList.remove('border-ancient-accent', 'dark:border-ancient-accent-dark', 'text-ancient-accent', 'dark:text-ancient-accent-dark', 'font-medium', 'bg-ancient-accent/10', 'dark:bg-ancient-accent-dark/10', 'shadow-sm');
link.classList.add('border-ancient-accent/30', 'dark:border-ancient-accent-dark/30');
}
const href = (link as HTMLAnchorElement).href;
const url = new URL(href);
// 获取当前的筛选参数
const params = new URLSearchParams(window.location.search);
const clickedParam = Array.from(url.searchParams.entries())[0];
if (clickedParam) {
const [paramName, paramValue] = clickedParam;
// 如果当前参数值与点击的值相同,则移除该参数
if (params.getAll(paramName).includes(paramValue)) {
params.delete(paramName, paramValue);
} else {
// 否则设置新的参数值
params.append(paramName, paramValue);
}
} catch (e) {
console.error('Error updating tag UI:', e);
// 更新 URL但不刷新页面
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
window.history.pushState({}, '', newUrl);
// 应用筛选
applyFilters();
}
});
// 高亮显示当前筛选条件摘要(顶部筛选状态栏)
if (currentCategory || currentCity || currentTag || currentSearch) {
const filterSummary = document.querySelector('.flex.flex-wrap.gap-2.items-center');
if (filterSummary) {
filterSummary.classList.add('animate-pulse-once');
setTimeout(() => {
filterSummary.classList.remove('animate-pulse-once');
}, 500);
}
}
}
// 初始化:如果 URL 中有筛选参数,应用筛选
if (window.location.search) {
applyFilters();
}
});
// 处理搜索表单提交
const searchForm = document.getElementById('search-form');
const searchFormMobile = document.getElementById('search-form-mobile');
function handleSearch(e: Event) {
e.preventDefault();
e.preventDefault();
const form = e.target as HTMLFormElement;
const searchInput = form.querySelector('input[name="search"]') as HTMLInputElement;
@ -1059,7 +1085,7 @@ const buildUrl = (params: Record<string, string | null>) => {
// 更新或删除搜索参数
if (searchInput.value.trim()) {
params.set('search', searchInput.value.trim());
} else {
} else {
params.delete('search');
}
@ -1086,6 +1112,11 @@ const buildUrl = (params: Record<string, string | null>) => {
window.location.href = '/culture';
});
}
// 初始化:如果 URL 中有筛选参数,应用筛选
if (window.location.search) {
applyFilters();
}
});
</script>
@ -1865,56 +1896,52 @@ const buildUrl = (params: Record<string, string | null>) => {
}
/* 文化分类样式 - 明亮模式 */
.font-ancient-body.max-h-48 a[href*="?category"]:hover {
background-color: rgba(139, 90, 43, 0.1);
background-color: rgba(251, 191, 36, 0.1);
}
.font-ancient-body.max-h-48 a[href*="?category"] .w-4.h-4 {
border-color: rgba(139, 90, 43, 0.5);
border-color: rgba(251, 191, 36, 0.5);
}
.font-ancient-body.max-h-48 a[href*="?category"]:hover .w-4.h-4 {
border-color: rgba(139, 90, 43, 0.8);
background-color: rgba(139, 90, 43, 0.1);
border-color: rgba(251, 191, 36, 0.8);
background-color: rgba(251, 191, 36, 0.1);
}
.font-ancient-body.max-h-48 a[href*="?category"].bg-ancient-accent\/20 {
background-color: rgba(139, 90, 43, 0.15);
.font-ancient-body.max-h-48 a[href*="?category"].bg-amber-100\/80 {
background-color: rgba(251, 191, 36, 0.15);
}
.font-ancient-body.max-h-48 a[href*="?category"].bg-ancient-accent\/20 .w-4.h-4 {
border-color: rgba(139, 90, 43, 0.8);
background-color: rgba(139, 90, 43, 0.2);
.font-ancient-body.max-h-48 a[href*="?category"].bg-amber-100\/80 .w-4.h-4 {
border-color: rgba(251, 191, 36, 0.8);
background-color: rgba(251, 191, 36, 0.2);
}
/* 文化分类样式 - 暗黑模式 */
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?category"]:hover {
background-color: rgba(215, 171, 101, 0.1);
background-color: rgba(180, 83, 9, 0.2);
}
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?category"] .w-4.h-4 {
border-color: rgba(215, 171, 101, 0.5);
border-color: rgba(180, 83, 9, 0.5);
}
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?category"]:hover .w-4.h-4 {
border-color: rgba(215, 171, 101, 0.8);
background-color: rgba(215, 171, 101, 0.1);
border-color: rgba(180, 83, 9, 0.8);
background-color: rgba(180, 83, 9, 0.2);
}
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?category"].bg-ancient-accent\/20 {
background-color: rgba(215, 171, 101, 0.15);
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?category"].bg-amber-100\/80 {
background-color: rgba(180, 83, 9, 0.25);
}
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?category"].bg-ancient-accent\/20 .w-4.h-4 {
border-color: rgba(215, 171, 101, 0.8);
background-color: rgba(215, 171, 101, 0.2);
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?category"].bg-amber-100\/80 .w-4.h-4 {
border-color: rgba(180, 83, 9, 0.8);
background-color: rgba(180, 83, 9, 0.3);
}
/* 地域分布样式 - 暗黑模式 */
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?city"]:hover {
background-color: rgba(180, 83, 9, 0.2);
}
@ -1974,14 +2001,8 @@ const buildUrl = (params: Record<string, string | null>) => {
}
/* 文化分类选中状态 */
.font-ancient-body.max-h-48 a[href*="?category"].bg-ancient-accent\/20 {
background-color: rgba(139, 90, 43, 0.1);
}
.font-ancient-body.max-h-48 a[href*="?category"].bg-ancient-accent\/20 .w-4.h-4 {
border-color: rgba(139, 90, 43, 0.8);
color: rgba(139, 90, 43, 0.8);
}
/* 地域分布选中状态 */
.font-ancient-body.max-h-48 a[href*="?city"].bg-amber-100\/80 {
@ -1997,6 +2018,7 @@ const buildUrl = (params: Record<string, string | null>) => {
.flex.flex-wrap.gap-2 a[href*="?tag"] {
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.flex.flex-wrap.gap-2 a[href*="?tag"]:hover {
@ -2008,28 +2030,10 @@ const buildUrl = (params: Record<string, string | null>) => {
background-color: rgba(139, 90, 43, 0.15);
border-color: rgba(139, 90, 43, 0.8);
color: rgba(139, 90, 43, 1);
font-weight: 500;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* 暗黑模式样式 */
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?category"].bg-ancient-accent\/20 {
background-color: rgba(215, 171, 101, 0.15);
}
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?category"].bg-ancient-accent\/20 .w-4.h-4 {
border-color: rgba(215, 171, 101, 0.8);
color: rgba(215, 171, 101, 0.8);
}
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?city"].bg-amber-100\/80 {
background-color: rgba(180, 83, 9, 0.2);
}
[data-theme='dark'] .font-ancient-body.max-h-48 a[href*="?city"].bg-amber-100\/80 .w-4.h-4 {
border-color: rgba(180, 83, 9, 0.8);
color: rgba(180, 83, 9, 0.8);
}
[data-theme='dark'] .flex.flex-wrap.gap-2 a[href*="?tag"]:hover {
background-color: rgba(215, 171, 101, 0.1);
border-color: rgba(215, 171, 101, 0.5);
@ -2039,6 +2043,41 @@ const buildUrl = (params: Record<string, string | null>) => {
background-color: rgba(215, 171, 101, 0.15);
border-color: rgba(215, 171, 101, 0.8);
color: rgba(215, 171, 101, 1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
/* 自定义滚动条样式 */
.scrollbar-thin::-webkit-scrollbar {
height: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: rgba(248, 245, 232, 0.2);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(139, 90, 43, 0.3);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: rgba(139, 90, 43, 0.4);
}
/* 暗色模式下的滚动条样式 */
[data-theme='dark'] .scrollbar-thin::-webkit-scrollbar-track {
background: rgba(30, 26, 33, 0.2);
}
[data-theme='dark'] .scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(215, 171, 101, 0.3);
}
[data-theme='dark'] .scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: rgba(215, 171, 101, 0.4);
}
/* 其他现有样式 */
</style>

View File

@ -102,7 +102,15 @@ const relatedTravels = allTravels
<!-- 旅行图片 -->
<div class="rounded-lg overflow-hidden shadow-md">
<div class="h-64 bg-gray-300 dark:bg-gray-700 flex items-center justify-center">
<span class="text-gray-500 dark:text-gray-400">{entry.data.title} 图片</span>
{entry.data.image ? (
<img
src={entry.data.image}
alt={entry.data.title}
class="w-full h-full object-cover"
/>
) : (
<span class="text-gray-500 dark:text-gray-400">{entry.data.title} 图片</span>
)}
</div>
</div>
@ -206,7 +214,15 @@ const relatedTravels = allTravels
>
<div class="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-700/50 transition-colors">
<div class="w-16 h-16 flex-shrink-0 bg-gray-300 dark:bg-gray-700 rounded flex items-center justify-center">
<span class="text-xs text-gray-500 dark:text-gray-300">图片</span>
{travel.data.image ? (
<img
src={travel.data.image}
alt={travel.data.title}
class="w-full h-full object-cover"
/>
) : (
<span class="text-xs text-gray-500 dark:text-gray-300">图片</span>
)}
</div>
<div>
<h4 class="text-base font-medium text-gray-900 dark:text-white group-hover:text-color-primary-600 dark:group-hover:text-color-primary-400 transition-colors">

View File

@ -90,7 +90,7 @@ const visibleTravels = sortedTravels.slice(
<div class="mb-10 relative">
<div class="absolute -top-8 -left-8 w-16 h-16 bg-[url('/images/paper-clip.png')] bg-no-repeat opacity-70 transform -rotate-12"></div>
<div class="relative inline-block">
<h1 class="text-5xl font-handwriting text-slate-800 dark:text-primary-100 leading-tight transform -rotate-1 relative z-10">
<h1 class="text-5xl font-handwriting text-slate-800 dark:text-primary-200 leading-tight transform -rotate-1 relative z-10">
河北私人旅行笔记
</h1>
<div class="absolute -bottom-3 left-0 w-full h-3 bg-primary-300 dark:bg-primary-600 opacity-40 transform rotate-1"></div>
@ -109,7 +109,7 @@ const visibleTravels = sortedTravels.slice(
<!-- 右侧似手绘地图的概述 -->
<div class="md:w-1/2 bg-theme-primary-bg dark:bg-slate-800/50 p-5 border border-primary-200 dark:border-slate-700 rounded-sm shadow-md transform -rotate-1">
<h2 class="font-handwriting text-2xl text-slate-800 dark:text-primary-300 mb-3 flex items-center">
<h2 class="font-handwriting text-2xl text-slate-800 dark:text-primary-200 mb-3 flex items-center">
<span class="inline-block w-6 h-6 mr-2 bg-[url('/images/compass-icon.png')] bg-contain bg-no-repeat"></span>
地域指南
</h2>
@ -148,7 +148,7 @@ const visibleTravels = sortedTravels.slice(
<div class="lg:hidden mb-6">
<button id="mobile-filter-toggle" class="w-full py-3 px-4 flex items-center justify-between bg-white dark:bg-slate-800 rounded-sm border border-slate-200 dark:border-slate-700 shadow-md transform -rotate-1 relative">
<div class="absolute -top-2 left-1/2 transform -translate-x-1/2 w-12 h-4 bg-[url('/images/binding-tape.png')] bg-contain bg-no-repeat"></div>
<span class="font-handwriting text-xl text-slate-800 dark:text-primary-100">旅行者笔记本</span>
<span class="font-handwriting text-xl text-slate-800 dark:text-primary-200">旅行者笔记本</span>
<svg id="filter-arrow-down" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-600 dark:text-slate-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
@ -168,7 +168,7 @@ const visibleTravels = sortedTravels.slice(
<!-- 移动端筛选关闭按钮 -->
<div class="lg:hidden flex justify-between items-center mb-4">
<h2 class="font-handwriting text-2xl text-slate-800 dark:text-primary-100">
<h2 class="font-handwriting text-2xl text-slate-800 dark:text-primary-200">
旅行者笔记
</h2>
<button id="mobile-filter-close" class="p-1 rounded-full bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400">
@ -180,7 +180,7 @@ const visibleTravels = sortedTravels.slice(
<!-- 仅在桌面显示的筛选标题 -->
<div class="hidden lg:block">
<h2 class="font-handwriting text-2xl text-slate-800 dark:text-primary-100 mb-6 pb-3 border-b border-slate-200 dark:border-slate-700">
<h2 class="font-handwriting text-2xl text-slate-800 dark:text-primary-200 mb-6 pb-3 border-b border-slate-200 dark:border-slate-700">
旅行者笔记
</h2>
</div>
@ -194,12 +194,12 @@ const visibleTravels = sortedTravels.slice(
type="text"
name="search"
placeholder="输入关键词..."
class="w-full px-4 py-2 bg-theme-primary-bg dark:bg-slate-700/50 border-b-2 border-slate-300 dark:border-slate-600 font-handwriting text-slate-800 dark:text-slate-200 focus:outline-none focus:border-theme-primary dark:focus:border-theme-primary placeholder-slate-400"
class="w-full px-4 py-2 bg-theme-primary-bg dark:bg-slate-700/50 border-b-2 border-slate-300 dark:border-slate-600 text-slate-800 dark:text-slate-200 focus:outline-none focus:border-theme-primary dark:focus:border-theme-primary placeholder-slate-400 caret-slate-800 dark:caret-slate-200"
/>
</div>
<button
id="search-button"
class="mt-2 w-full py-2 px-4 bg-primary-100 dark:bg-primary-900/30 text-primary-800 dark:text-primary-400 font-handwriting text-sm border border-primary-200 dark:border-primary-800/40 hover:bg-primary-200 dark:hover:bg-primary-800/40 transition-colors flex items-center justify-center"
class="mt-2 w-full py-2 px-4 bg-primary-100 dark:bg-primary-900/30 text-primary-800 dark:text-primary-400 font-handwriting text-sm border border-primary-200 dark:border-primary-800/40 hover:bg-primary-200 dark:hover:bg-primary-800/40 hover:text-primary-900 dark:hover:text-primary-300 hover:shadow-md hover:scale-[1.02] transition-all duration-200 flex items-center justify-center"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
@ -228,7 +228,7 @@ const visibleTravels = sortedTravels.slice(
<!-- 季节关联 -->
<div id="seasons-panel" class="filter-panel lg:block mb-8">
<h3 class="font-handwriting text-xl text-slate-800 dark:text-primary-100 mb-4 flex items-center">
<h3 class="font-handwriting text-xl text-slate-800 dark:text-primary-200 mb-4 flex items-center">
<span class="inline-block w-5 h-5 mr-2 bg-[url('/images/season-icon.png')] bg-contain bg-no-repeat"></span>
最适季节
</h3>
@ -243,7 +243,7 @@ const visibleTravels = sortedTravels.slice(
<!-- 旅行类型 -->
<div id="types-panel" class="filter-panel hidden lg:block mb-8">
<h3 class="font-handwriting text-xl text-slate-800 dark:text-primary-100 mb-4 flex items-center">
<h3 class="font-handwriting text-xl text-slate-800 dark:text-primary-200 mb-4 flex items-center">
<span class="inline-block w-5 h-5 mr-2 bg-[url('/images/travel-type-icon.png')] bg-contain bg-no-repeat"></span>
旅行方式
</h3>
@ -258,7 +258,7 @@ const visibleTravels = sortedTravels.slice(
<!-- 城市筛选 - 新增部分 -->
<div id="cities-panel" class="filter-panel hidden lg:block mb-8">
<h3 class="font-handwriting text-xl text-slate-800 dark:text-primary-100 mb-4 flex items-center">
<h3 class="font-handwriting text-xl text-slate-800 dark:text-primary-200 mb-4 flex items-center">
<span class="inline-block w-5 h-5 mr-2 bg-[url('/images/city-icon.png')] bg-contain bg-no-repeat"></span>
目的地城市
</h3>
@ -273,7 +273,7 @@ const visibleTravels = sortedTravels.slice(
<!-- 旅行灵感标签 -->
<div id="tags-panel" class="filter-panel hidden lg:block mb-4">
<h3 class="font-handwriting text-xl text-slate-800 dark:text-primary-100 mb-4 flex items-center">
<h3 class="font-handwriting text-xl text-slate-800 dark:text-primary-200 mb-4 flex items-center">
<span class="inline-block w-5 h-5 mr-2 bg-[url('/images/tag-icon.png')] bg-contain bg-no-repeat"></span>
旅行灵感
</h3>
@ -307,6 +307,7 @@ const visibleTravels = sortedTravels.slice(
<!-- 筛选标签将在JS中动态添加 -->
</div>
<!-- 无搜索结果提示 - 在客户端控制显示 -->
<div id="no-results-message" class="hidden bg-white dark:bg-slate-800 p-8 text-center rounded-sm border border-slate-200 dark:border-slate-700 shadow-md mb-10 relative transform rotate-1">
<div class="absolute -top-3 right-5 w-12 h-12 bg-[url('/images/tape-piece.png')] bg-contain bg-no-repeat opacity-70"></div>
@ -352,18 +353,21 @@ const visibleTravels = sortedTravels.slice(
<div class="grid grid-cols-1 md:grid-cols-3 gap-0">
<div class="md:col-span-1 h-full">
<div class="h-48 md:h-full bg-primary-100 dark:bg-slate-700 relative border-b md:border-r border-slate-200 dark:border-slate-700">
<div class="flex items-center justify-center h-full font-handwriting italic text-slate-500 dark:text-slate-400">
{travel.data.title} 照片
</div>
{travel.data.featured && (
<div class="absolute top-3 left-3 px-3 py-1 bg-primary-100 dark:bg-primary-900/40 text-primary-800 dark:text-primary-400 text-xs font-handwriting border border-primary-200 dark:border-primary-800/50 transform -rotate-3 shadow-sm">
★ 私藏路线
{travel.data.image ? (
<img
src={travel.data.image}
alt={travel.data.title}
class="w-full h-full object-cover"
/>
) : (
<div class="flex items-center justify-center h-full font-handwriting italic text-slate-500 dark:text-slate-400">
{travel.data.title} 照片
</div>
)}
</div>
</div>
<div class="md:col-span-2 p-6">
<h3 class="text-2xl font-handwriting text-slate-800 dark:text-primary-100 mb-3 group-hover:text-primary-800 dark:group-hover:text-primary-400 transition-colors">
<h3 class="text-2xl font-handwriting text-slate-800 dark:text-primary-200 mb-3 group-hover:text-primary-800 dark:group-hover:text-primary-400 transition-colors">
{travel.data.title}
</h3>
@ -401,17 +405,12 @@ const visibleTravels = sortedTravels.slice(
{travel.data.description}
</p>
<div class="flex flex-wrap gap-2 mb-3">
{travel.data.tags.slice(0, 3).map((tag: string) => (
<span class="px-2 py-0.5 text-xs font-handwriting bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-600">
<div class="flex overflow-x-auto gap-2 mb-3 scrollbar-thin scrollbar-thumb-slate-500/30 dark:scrollbar-thumb-slate-400/30 scrollbar-track-slate-100/20 dark:scrollbar-track-slate-900/20 pb-2">
{travel.data.tags.map((tag: string) => (
<span class="flex-shrink-0 px-2 py-0.5 text-xs font-handwriting bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-600">
#{tag}
</span>
))}
{travel.data.tags.length > 3 && (
<span class="px-2 py-0.5 text-xs font-handwriting bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-600">
+{travel.data.tags.length - 3}
</span>
)}
</div>
<div class="text-primary-700 dark:text-primary-500 text-sm font-handwriting group-hover:underline mt-2 flex items-center">
@ -1164,6 +1163,21 @@ const visibleTravels = sortedTravels.slice(
animation: tag-remove 0.3s ease forwards;
pointer-events: none;
}
/* 为搜索框添加光标样式 */
#search-input {
font-family: sans-serif; /* 使用标准字体以确保光标可见 */
caret-color: currentColor; /* 确保光标颜色与文本颜色匹配 */
}
/* 解决光标不可见问题的额外样式 */
#search-input:focus {
color: var(--slate-900, #1e293b); /* 确保文本颜色足够深以便看到 */
}
.dark #search-input:focus {
color: var(--slate-100, #f1f5f9); /* 暗模式下的文本颜色 */
}
</style>
<script>
@ -1174,6 +1188,7 @@ const visibleTravels = sortedTravels.slice(
const searchButton = document.getElementById('search-button');
const testNoResultsBtn = document.getElementById('test-no-results-btn');
const travelCards = document.querySelectorAll('#travel-list div[data-tags]');
const noResultsMessage = document.getElementById('no-results-message');
const searchTermMessage = document.getElementById('search-term-message');
const travelList = document.getElementById('travel-list');
@ -1555,14 +1570,13 @@ const visibleTravels = sortedTravels.slice(
// 客户端筛选函数
function filterTravels() {
// 获取当前搜索词
const searchValue = searchInput?.value.trim().toLowerCase() || '';
const selectedSeasons: string[] = [];
const selectedTypes: string[] = [];
const selectedCities: string[] = [];
const selectedTags: string[] = [];
// 获取选中的季节贴纸
seasonStickers.forEach((sticker: Element) => {
if (sticker.classList.contains('selected')) {
const seasonText = sticker.getAttribute('data-value')?.toLowerCase() || '';
@ -1570,7 +1584,6 @@ const visibleTravels = sortedTravels.slice(
}
});
// 获取选中的旅行类型贴纸
typeStickers.forEach((sticker: Element) => {
if (sticker.classList.contains('selected')) {
const typeText = sticker.getAttribute('data-value')?.toLowerCase() || '';
@ -1578,17 +1591,13 @@ const visibleTravels = sortedTravels.slice(
}
});
// 获取选中的城市贴纸
cityStickers.forEach((sticker: Element) => {
if (sticker.classList.contains('selected')) {
const cityText = sticker.getAttribute('data-value')?.toLowerCase() || '';
if (cityText) {
selectedCities.push(cityText);
}
if (cityText) selectedCities.push(cityText);
}
});
// 获取选中的标签贴纸
tagStickers.forEach((sticker: Element) => {
if (sticker.classList.contains('selected')) {
const tagText = sticker.getAttribute('data-value')?.toLowerCase() || '';
@ -1597,14 +1606,21 @@ const visibleTravels = sortedTravels.slice(
});
let matchCount = 0;
let totalCards = 0;
// 遍历所有旅行卡片进行筛选
travelCards.forEach((card) => {
const cardContainer = card.closest('a')?.parentElement;
if (!cardContainer) return;
totalCards++;
// 检查DOM结构
const anchor = card.closest('a');
// 修正这里直接使用anchor作为容器而不是anchor.parentElement
const cardContainer = anchor;
if (!cardContainer) {
return;
}
try {
// 直接从card获取数据属性
const cardData = {
title: card.getAttribute('data-title')?.toLowerCase() || '',
description: card.getAttribute('data-description')?.toLowerCase() || '',
@ -1614,74 +1630,169 @@ const visibleTravels = sortedTravels.slice(
cities: JSON.parse(card.getAttribute('data-cities') || '[]').map((city: string) => city.toLowerCase())
};
// 匹配搜索词
const matchesSearch = !searchValue ||
cardData.title.includes(searchValue) ||
cardData.description.includes(searchValue) ||
cardData.tags.some((tag: string) => tag.includes(searchValue)) ||
cardData.cities.some((city: string) => city.includes(searchValue));
// 匹配季节
const matchesSeasons = selectedSeasons.length === 0 ||
selectedSeasons.some(season => cardData.season.includes(season));
// 匹配类型
const matchesTypes = selectedTypes.length === 0 ||
selectedTypes.some(type => cardData.type.includes(type));
// 匹配城市
const matchesCities = selectedCities.length === 0 ||
selectedCities.some(city => cardData.cities.some((cardCity: string) => cardCity.includes(city)));
// 匹配标签
const matchesTags = selectedTags.length === 0 ||
selectedTags.some(tag => cardData.tags.some((cardTag: string) => cardTag.includes(tag)));
// 所有条件都匹配才显示
const isMatch = matchesSearch && matchesSeasons && matchesTypes && matchesCities && matchesTags;
if (isMatch) {
matchCount++;
cardContainer.style.display = '';
// 使用多种方式尝试显示
cardContainer.style.cssText = 'display: block !important;'; // 使用内联CSS并添加!important
// 添加一个class确保显示
cardContainer.classList.remove('hidden');
} else {
cardContainer.style.display = 'none';
// 使用多种方式尝试隐藏
cardContainer.style.cssText = 'display: none !important;'; // 使用内联CSS并添加!important
// 添加一个class来隐藏
cardContainer.classList.add('hidden');
}
} catch (error) {
console.error('Error filtering card:', error);
cardContainer.style.display = 'none';
}
});
// 更新无结果提示显示
if (matchCount === 0) {
if (travelList) travelList.classList.add('hidden');
if (travelList) {
travelList.classList.add('hidden');
}
if (noResultsMessage) {
noResultsMessage.classList.remove('hidden');
if (searchTermMessage) {
let message = '抱歉,未找到相关旅行笔记。';
if (searchValue) {
message += `没有找到包含"${searchValue}"的内容。`;
}
if (selectedSeasons.length > 0) {
message += `没有找到${selectedSeasons.join('、')}季节的内容。`;
}
if (selectedTypes.length > 0) {
message += `没有找到${selectedTypes.join('、')}类型的内容。`;
}
if (selectedCities.length > 0) {
message += `没有找到位于${selectedCities.join('、')}的内容。`;
}
if (selectedTags.length > 0) {
message += `没有找到标签为${selectedTags.join('、')}的内容。`;
}
if (searchValue) message += `没有找到包含"${searchValue}"的内容。`;
if (selectedSeasons.length > 0) message += `没有找到${selectedSeasons.join('、')}季节的内容。`;
if (selectedTypes.length > 0) message += `没有找到${selectedTypes.join('、')}类型的内容。`;
if (selectedCities.length > 0) message += `没有找到位于${selectedCities.join('、')}的内容。`;
if (selectedTags.length > 0) message += `没有找到标签为${selectedTags.join('、')}的内容。`;
searchTermMessage.textContent = message;
}
}
} else {
if (travelList) travelList.classList.remove('hidden');
if (noResultsMessage) noResultsMessage.classList.add('hidden');
if (travelList) {
travelList.classList.remove('hidden');
}
if (noResultsMessage) {
noResultsMessage.classList.add('hidden');
}
}
// 更新匹配数量显示
const matchCountDisplay = document.getElementById('match-count-display');
const currentMatchCount = document.getElementById('current-match-count');
if (matchCountDisplay && currentMatchCount) {
if (matchCount === 0) {
matchCountDisplay.style.display = 'none';
} else {
matchCountDisplay.style.display = 'block';
currentMatchCount.textContent = matchCount.toString();
}
}
// 额外的DOM验证和修复
setTimeout(() => {
// 获取所有卡片的a标签容器
const allContainers = Array.from(document.querySelectorAll('#travel-list > a'));
// 如果没有找到,尝试其他选择器
if (allContainers.length === 0) {
// 尝试全部a标签
const altContainers = Array.from(document.querySelectorAll('#travel-list a'));
if (altContainers.length > 0) {
allContainers.splice(0, allContainers.length, ...altContainers);
}
}
let visibleCount = 0;
allContainers.forEach(container => {
const card = container.querySelector('div[data-tags]');
if (!card) {
return;
}
const cardTitle = card.getAttribute('data-title') || '未知标题';
// 检查这个卡片是否应该显示(通过我们前面的筛选逻辑)
const shouldShow = Array.from(travelCards).some(travelCard => {
if (travelCard === card) {
// 这个卡片的匹配状态
const cardData = {
title: travelCard.getAttribute('data-title')?.toLowerCase() || '',
description: travelCard.getAttribute('data-description')?.toLowerCase() || '',
tags: JSON.parse(travelCard.getAttribute('data-tags') || '[]').map((tag: string) => tag.toLowerCase()),
season: travelCard.getAttribute('data-season')?.toLowerCase() || '',
type: travelCard.getAttribute('data-type')?.toLowerCase() || '',
cities: JSON.parse(travelCard.getAttribute('data-cities') || '[]').map((city: string) => city.toLowerCase())
};
const matchesSearch = !searchValue ||
cardData.title.includes(searchValue) ||
cardData.description.includes(searchValue) ||
cardData.tags.some((tag: string) => tag.includes(searchValue)) ||
cardData.cities.some((city: string) => city.includes(searchValue));
const matchesSeasons = selectedSeasons.length === 0 ||
selectedSeasons.some(season => cardData.season.includes(season));
const matchesTypes = selectedTypes.length === 0 ||
selectedTypes.some(type => cardData.type.includes(type));
const matchesCities = selectedCities.length === 0 ||
selectedCities.some(city => cardData.cities.some((cardCity: string) => cardCity.includes(city)));
const matchesTags = selectedTags.length === 0 ||
selectedTags.some(tag => cardData.tags.some((cardTag: string) => cardTag.includes(tag)));
return matchesSearch && matchesSeasons && matchesTypes && matchesCities && matchesTags;
}
return false;
});
// 确保显示/隐藏状态正确
if (shouldShow) {
visibleCount++;
// 使用最强硬的方式确保显示
(container as HTMLElement).style.cssText = 'display: block !important;';
} else {
// 使用最强硬的方式确保隐藏
(container as HTMLElement).style.cssText = 'display: none !important;';
}
});
// 更新匹配计数显示
const currentMatchCount = document.getElementById('current-match-count');
if (currentMatchCount) {
currentMatchCount.textContent = visibleCount.toString();
}
// 检查是否需要显示无结果消息
if (visibleCount === 0) {
if (travelList) travelList.classList.add('hidden');
if (noResultsMessage) noResultsMessage.classList.remove('hidden');
} else {
if (travelList) travelList.classList.remove('hidden');
if (noResultsMessage) noResultsMessage.classList.add('hidden');
}
}, 100);
return matchCount;
}