htmlgen-mcp 0.2.5__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of htmlgen-mcp might be problematic. Click here for more details.

@@ -0,0 +1,696 @@
1
+ """改进版页面模板工具 - 支持 AI 生成和个性化内容"""
2
+ from __future__ import annotations
3
+
4
+ import html
5
+ import json
6
+ import urllib.parse
7
+ from pathlib import Path
8
+ from typing import Dict, Any, Optional, List
9
+
10
+
11
+ def create_menu_page(
12
+ file_path: str,
13
+ project_name: str | None = None,
14
+ context: Dict[str, Any] | None = None,
15
+ ai_content: Dict[str, Any] | None = None
16
+ ) -> str:
17
+ """创建餐厅/咖啡店的"菜单"页面 - 支持 AI 生成内容
18
+
19
+ Args:
20
+ file_path: 目标HTML文件路径
21
+ project_name: 项目名称
22
+ context: 项目上下文信息(描述、特色、位置等)
23
+ ai_content: AI 生成的菜单内容(可选)
24
+ """
25
+ brand = (project_name or "Coffee & Menu").strip()
26
+ title = f"{brand} · 菜单 Menu"
27
+ ctx = context or {}
28
+
29
+ # 如果提供了 AI 生成的内容,使用它;否则生成智能默认值
30
+ if ai_content and "categories" in ai_content:
31
+ menu_data = ai_content
32
+ else:
33
+ menu_data = _generate_smart_menu_defaults(brand, ctx)
34
+
35
+ # 生成菜单HTML
36
+ menu_sections = ""
37
+ for idx, category in enumerate(menu_data.get("categories", [])):
38
+ section_class = "section section-alt" if idx % 2 == 1 else "section"
39
+ category_name = category.get("name", "菜单")
40
+ items_html = ""
41
+
42
+ for item in category.get("items", []):
43
+ items_html += f"""
44
+ <article class="menu-card reveal col-md-4">
45
+ <img data-topic="{html.escape(item.get('image_topic', item.get('name', 'menu item')))}"
46
+ alt="{html.escape(item.get('name', ''))}"
47
+ class="img-fluid rounded shadow-sm">
48
+ <div class="d-flex justify-content-between align-items-center mt-3">
49
+ <h3 class="h5 mb-0">{html.escape(item.get('name', ''))}</h3>
50
+ <span class="price">{html.escape(str(item.get('price', '')))}</span>
51
+ </div>
52
+ <p class="text-muted mt-2">{html.escape(item.get('description', ''))}</p>
53
+ </article>"""
54
+
55
+ menu_sections += f"""
56
+ <section class="{section_class}" id="category-{idx}">
57
+ <div class="container">
58
+ <h2 class="h3 text-center mb-4">{html.escape(category_name)}</h2>
59
+ <div class="row g-4">
60
+ {items_html}
61
+ </div>
62
+ </div>
63
+ </section>"""
64
+
65
+ # 生成导航标签
66
+ nav_pills = ""
67
+ for idx, category in enumerate(menu_data.get("categories", [])):
68
+ active_class = "active" if idx == 0 else ""
69
+ nav_pills += f"""
70
+ <li class="nav-item">
71
+ <a class="nav-link {active_class}" href="#category-{idx}">
72
+ {html.escape(category.get('name', ''))}
73
+ </a>
74
+ </li>"""
75
+
76
+ # 获取页面描述和副标题
77
+ page_description = ctx.get('menu_description', menu_data.get('description', '精品咖啡与精致甜点的完美搭配'))
78
+
79
+ html_doc = f"""<!DOCTYPE html>
80
+ <html lang="zh-CN">
81
+ <head>
82
+ <meta charset="UTF-8" />
83
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
84
+ <title>{html.escape(title)}</title>
85
+ <link rel="stylesheet" href="assets/css/style.css" />
86
+ </head>
87
+ <body>
88
+ <header class="hero hero-ultra section text-center"
89
+ data-bg-topic="{ctx.get('hero_bg_topic', 'cozy coffee shop interior, warm light')}"
90
+ id="home">
91
+ <div class="container hero-inner">
92
+ <span class="badge badge-soft mb-3">菜单 MENU</span>
93
+ <h1 class="display-5 mb-2">{html.escape(brand)}</h1>
94
+ <p class="section-lead mx-auto">{html.escape(page_description)}</p>
95
+ </div>
96
+ </header>
97
+
98
+ <main>
99
+ <nav class="section section-sm" aria-label="菜单分类导航">
100
+ <div class="container">
101
+ <ul class="nav nav-pills justify-content-center gap-2">
102
+ {nav_pills}
103
+ </ul>
104
+ </div>
105
+ </nav>
106
+
107
+ {menu_sections}
108
+ </main>
109
+
110
+ <footer class="footer-minimal">
111
+ <div class="container">
112
+ <div class="footer-brand">
113
+ <span>{html.escape(brand)}</span>
114
+ <p>{html.escape(ctx.get('footer_text', 'See you in our cafe'))}</p>
115
+ </div>
116
+ <div class="footer-meta">
117
+ <span>© {html.escape(brand)}</span>
118
+ <a href="mailto:{html.escape(ctx.get('email', 'hello@example.com'))}">
119
+ {html.escape(ctx.get('email', 'hello@example.com'))}
120
+ </a>
121
+ </div>
122
+ </div>
123
+ </footer>
124
+ <script src="assets/js/main.js"></script>
125
+ </body>
126
+ </html>"""
127
+
128
+ # 保存文件
129
+ with open(file_path, 'w', encoding='utf-8') as f:
130
+ f.write(html_doc)
131
+
132
+ return f"菜单页面已创建:{file_path}"
133
+
134
+
135
+ def _generate_smart_menu_defaults(brand: str, context: Dict) -> Dict:
136
+ """根据项目类型生成智能的默认菜单"""
137
+ description = str(context.get('description', '')).lower()
138
+ features = str(context.get('features', '')).lower()
139
+
140
+ # 根据关键词判断项目类型并生成相应菜单
141
+ if '咖啡' in description or 'coffee' in description or '咖啡' in brand:
142
+ # 咖啡店菜单
143
+ categories = [
144
+ {
145
+ "name": "精品咖啡",
146
+ "items": [
147
+ {
148
+ "name": f"{brand}特调",
149
+ "price": "¥ 42",
150
+ "description": f"独家配方,{brand}的招牌饮品",
151
+ "image_topic": "signature coffee drink"
152
+ },
153
+ {
154
+ "name": "手冲单品",
155
+ "price": "¥ 38-68",
156
+ "description": "每日精选单一产区咖啡豆,手工冲泡",
157
+ "image_topic": "pour over coffee"
158
+ },
159
+ {
160
+ "name": "意式浓缩",
161
+ "price": "¥ 28",
162
+ "description": "经典意式,醇厚浓郁",
163
+ "image_topic": "espresso shot"
164
+ }
165
+ ]
166
+ },
167
+ {
168
+ "name": "轻食甜点",
169
+ "items": [
170
+ {
171
+ "name": "招牌蛋糕",
172
+ "price": "¥ 35",
173
+ "description": "每日新鲜烘焙,搭配咖啡的完美选择",
174
+ "image_topic": "cake dessert"
175
+ }
176
+ ]
177
+ }
178
+ ]
179
+ elif '餐厅' in description or 'restaurant' in description:
180
+ # 餐厅菜单
181
+ categories = [
182
+ {
183
+ "name": "招牌主菜",
184
+ "items": [
185
+ {
186
+ "name": f"{brand}招牌菜",
187
+ "price": "¥ 88",
188
+ "description": "主厨特别推荐,独家秘制",
189
+ "image_topic": "signature dish"
190
+ }
191
+ ]
192
+ }
193
+ ]
194
+ elif '茶' in description or 'tea' in description:
195
+ # 茶馆菜单
196
+ categories = [
197
+ {
198
+ "name": "精选好茶",
199
+ "items": [
200
+ {
201
+ "name": "特级龙井",
202
+ "price": "¥ 58/壶",
203
+ "description": "明前龙井,清香淡雅",
204
+ "image_topic": "green tea"
205
+ }
206
+ ]
207
+ }
208
+ ]
209
+ else:
210
+ # 通用服务菜单
211
+ categories = [
212
+ {
213
+ "name": "服务项目",
214
+ "items": [
215
+ {
216
+ "name": "基础套餐",
217
+ "price": "咨询报价",
218
+ "description": "包含标准服务流程",
219
+ "image_topic": "service package"
220
+ }
221
+ ]
222
+ }
223
+ ]
224
+
225
+ # 如果上下文中有具体的菜单项,添加进去
226
+ if 'menu_items' in context:
227
+ # 合并用户提供的菜单项
228
+ user_items = context['menu_items']
229
+ if isinstance(user_items, list) and user_items:
230
+ categories[0]['items'].extend(user_items[:3]) # 最多添加3个
231
+
232
+ return {
233
+ "categories": categories,
234
+ "description": context.get('menu_description', '为您提供优质的产品与服务')
235
+ }
236
+
237
+
238
+ def create_about_page(
239
+ file_path: str,
240
+ project_name: str | None = None,
241
+ context: Dict[str, Any] | None = None,
242
+ ai_content: Dict[str, Any] | None = None
243
+ ) -> str:
244
+ """创建"关于我们"页面 - 支持 AI 生成内容
245
+
246
+ Args:
247
+ file_path: 目标HTML文件路径
248
+ project_name: 项目名称
249
+ context: 项目上下文信息
250
+ ai_content: AI 生成的关于页面内容(可选)
251
+ """
252
+ brand = (project_name or "Modern Brand").strip()
253
+ ctx = context or {}
254
+
255
+ # 使用 AI 内容或生成默认内容
256
+ if ai_content:
257
+ content_data = ai_content
258
+ else:
259
+ content_data = _generate_about_defaults(brand, ctx)
260
+
261
+ # 生成品牌故事区块
262
+ story_html = ""
263
+ if content_data.get('story'):
264
+ story_html = f"""
265
+ <section class="section" id="story">
266
+ <div class="container">
267
+ <div class="row g-5 align-items-center">
268
+ <div class="col-lg-6">
269
+ <div class="vision-capsule reveal">
270
+ <span class="eyebrow">品牌故事</span>
271
+ <h2 class="h3">{html.escape(content_data['story'].get('title', '我们的故事'))}</h2>
272
+ <p class="text-muted">{html.escape(content_data['story'].get('content', ''))}</p>
273
+ </div>
274
+ </div>
275
+ <div class="col-lg-6">
276
+ <div class="vision-media reveal"
277
+ data-bg-topic="{html.escape(content_data['story'].get('image_topic', 'brand story'))}">
278
+ </div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ </section>"""
283
+
284
+ # 生成核心价值观区块
285
+ values_html = ""
286
+ if content_data.get('values'):
287
+ values_list = ""
288
+ for value in content_data['values']:
289
+ values_list += f"<li>{html.escape(value)}</li>\n"
290
+
291
+ values_html = f"""
292
+ <section class="section section-alt" id="values">
293
+ <div class="container">
294
+ <h2 class="h3 text-center mb-4">核心价值观</h2>
295
+ <ul class="list-check mx-auto" style="max-width: 600px;">
296
+ {values_list}
297
+ </ul>
298
+ </div>
299
+ </section>"""
300
+
301
+ # 生成团队介绍区块
302
+ team_html = ""
303
+ if content_data.get('team'):
304
+ team_cards = ""
305
+ for member in content_data['team']:
306
+ team_cards += f"""
307
+ <article class="team-card reveal col-md-3">
308
+ <img data-topic="{html.escape(member.get('image_topic', 'team member portrait'))}"
309
+ alt="{html.escape(member.get('role', ''))}"
310
+ class="img-fluid rounded shadow-sm">
311
+ <h3 class="h6 mt-3">{html.escape(member.get('name', ''))}</h3>
312
+ <p class="text-muted">{html.escape(member.get('description', ''))}</p>
313
+ </article>"""
314
+
315
+ team_html = f"""
316
+ <section class="section" id="team">
317
+ <div class="container">
318
+ <span class="eyebrow">团队</span>
319
+ <h2 class="h3">核心团队</h2>
320
+ <div class="row g-4">
321
+ {team_cards}
322
+ </div>
323
+ </div>
324
+ </section>"""
325
+
326
+ # 生成成就/数据区块
327
+ metrics_html = ""
328
+ if content_data.get('achievements'):
329
+ metric_cards = ""
330
+ for achievement in content_data['achievements']:
331
+ metric_cards += f"""
332
+ <div class="col-md-3">
333
+ <div class="feature-card glass p-4 reveal" data-tilt>
334
+ <div class="display-6 fw-bold">{html.escape(str(achievement.get('value', '')))}</div>
335
+ <div class="text-muted mt-2">{html.escape(achievement.get('label', ''))}</div>
336
+ </div>
337
+ </div>"""
338
+
339
+ metrics_html = f"""
340
+ <section class="section section-sm">
341
+ <div class="container">
342
+ <div class="row g-4 text-center">
343
+ {metric_cards}
344
+ </div>
345
+ </div>
346
+ </section>"""
347
+
348
+ # 使用实际联系信息
349
+ contact_email = ctx.get('email', 'hello@example.com')
350
+
351
+ html_doc = f"""<!DOCTYPE html>
352
+ <html lang="zh-CN">
353
+ <head>
354
+ <meta charset="UTF-8" />
355
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
356
+ <title>{html.escape(brand)} · 关于我们</title>
357
+ <link rel="stylesheet" href="assets/css/style.css" />
358
+ </head>
359
+ <body>
360
+
361
+ <header class="hero hero-minimal section" id="home">
362
+ <div class="container hero-inner">
363
+ <span class="tagline">{html.escape(brand)}</span>
364
+ <h1 class="display-4">{html.escape(content_data.get('headline', f'了解{brand}'))}</h1>
365
+ <p class="section-lead">{html.escape(content_data.get('subtitle', ''))}</p>
366
+ </div>
367
+ </header>
368
+
369
+ <main>
370
+ {metrics_html}
371
+ {story_html}
372
+ {values_html}
373
+ {team_html}
374
+
375
+ <section class="section" id="cta">
376
+ <div class="container">
377
+ <div class="cta-card reveal text-center">
378
+ <h2 class="h3">{html.escape(content_data.get('cta_title', '与我们合作'))}</h2>
379
+ <p class="text-muted">{html.escape(content_data.get('cta_text', '期待与您的合作'))}</p>
380
+ <a class="btn btn-primary btn-lg" href="contact.html">联系我们</a>
381
+ </div>
382
+ </div>
383
+ </section>
384
+ </main>
385
+
386
+ <footer class="footer-minimal">
387
+ <div class="container">
388
+ <div class="footer-brand">
389
+ <span>{html.escape(brand)}</span>
390
+ <p>{html.escape(ctx.get('footer_text', '与我们一起创造价值'))}</p>
391
+ </div>
392
+ <div class="footer-meta">
393
+ <span>© {html.escape(brand)}</span>
394
+ <a href="mailto:{html.escape(contact_email)}">{html.escape(contact_email)}</a>
395
+ </div>
396
+ </div>
397
+ </footer>
398
+ <script src="assets/js/main.js"></script>
399
+ </body>
400
+ </html>"""
401
+
402
+ with open(file_path, 'w', encoding='utf-8') as f:
403
+ f.write(html_doc)
404
+
405
+ return f"关于页面已创建:{file_path}"
406
+
407
+
408
+ def _generate_about_defaults(brand: str, context: Dict) -> Dict:
409
+ """生成关于页面的智能默认内容"""
410
+ description = str(context.get('description', '')).lower()
411
+
412
+ # 根据描述生成合适的内容
413
+ if '咖啡' in description or 'coffee' in description:
414
+ return {
415
+ 'headline': f'{brand} 的故事',
416
+ 'subtitle': '用心制作每一杯咖啡,创造温暖的社区空间',
417
+ 'story': {
418
+ 'title': '从一粒咖啡豆开始',
419
+ 'content': f'{brand}始于对咖啡的热爱。我们相信一杯好咖啡不仅是味觉享受,更是生活态度的体现。',
420
+ 'image_topic': 'coffee roasting process'
421
+ },
422
+ 'values': [
423
+ '精选全球优质咖啡豆',
424
+ '坚持手工烘焙与冲泡',
425
+ '营造温馨舒适的环境',
426
+ '支持可持续发展'
427
+ ],
428
+ 'team': [
429
+ {
430
+ 'name': context.get('founder_name', '创始人'),
431
+ 'role': '创始人/首席咖啡师',
432
+ 'description': '10年咖啡行业经验,Q-Grader认证',
433
+ 'image_topic': 'coffee master at work'
434
+ }
435
+ ],
436
+ 'achievements': [
437
+ {'value': context.get('years', '5') + '年', 'label': '品牌历程'},
438
+ {'value': '1000+', 'label': '每日服务顾客'},
439
+ {'value': '20+', 'label': '咖啡品种'},
440
+ {'value': '98%', 'label': '顾客满意度'}
441
+ ],
442
+ 'cta_title': '欢迎来店品尝',
443
+ 'cta_text': '体验我们的咖啡文化'
444
+ }
445
+ else:
446
+ # 通用企业默认内容
447
+ return {
448
+ 'headline': f'关于{brand}',
449
+ 'subtitle': context.get('mission', '致力于提供优质的产品与服务'),
450
+ 'story': {
451
+ 'title': '我们的使命',
452
+ 'content': context.get('story', f'{brand}专注于为客户创造价值,通过创新和品质赢得信任。'),
453
+ 'image_topic': 'modern office team'
454
+ },
455
+ 'values': [
456
+ '客户至上',
457
+ '持续创新',
458
+ '品质保证',
459
+ '团队协作'
460
+ ],
461
+ 'team': [],
462
+ 'achievements': [
463
+ {'value': '100+', 'label': '服务客户'},
464
+ {'value': '50+', 'label': '成功案例'},
465
+ {'value': '10+', 'label': '专业团队'},
466
+ {'value': '5星', 'label': '客户评价'}
467
+ ],
468
+ 'cta_title': '开始合作',
469
+ 'cta_text': '让我们一起实现目标'
470
+ }
471
+
472
+
473
+ def create_contact_page(
474
+ file_path: str,
475
+ project_name: str | None = None,
476
+ context: Dict[str, Any] | None = None,
477
+ ai_content: Dict[str, Any] | None = None
478
+ ) -> str:
479
+ """创建"联系我们"页面 - 使用真实联系信息
480
+
481
+ Args:
482
+ file_path: 目标HTML文件路径
483
+ project_name: 项目名称
484
+ context: 包含实际联系信息的上下文
485
+ ai_content: AI 生成的文案内容(可选)
486
+ """
487
+ brand = (project_name or "Modern Brand").strip()
488
+ ctx = context or {}
489
+
490
+ # 获取实际联系信息
491
+ contact_info = {
492
+ 'address': ctx.get('address', '请在context中提供address'),
493
+ 'phone': ctx.get('phone', '请在context中提供phone'),
494
+ 'email': ctx.get('email', f'contact@{brand.lower().replace(" ", "")}.com'),
495
+ 'hours': ctx.get('business_hours', '周一至周五 9:00-18:00'),
496
+ 'wechat': ctx.get('wechat', ''),
497
+ 'social_media': ctx.get('social_media', {})
498
+ }
499
+
500
+ # 使用 AI 内容或默认文案
501
+ if ai_content:
502
+ content_data = ai_content
503
+ else:
504
+ content_data = {
505
+ 'headline': f'联系{brand}',
506
+ 'subtitle': '我们期待听到您的声音',
507
+ 'form_title': '发送消息',
508
+ 'form_text': '请留下您的联系方式和需求,我们会尽快回复',
509
+ 'response_time': '我们通常在24小时内回复'
510
+ }
511
+
512
+ # 生成社交媒体链接
513
+ social_links = ""
514
+ if contact_info['social_media']:
515
+ for platform, link in contact_info['social_media'].items():
516
+ social_links += f"""
517
+ <a class="btn btn-outline-light btn-sm" href="{html.escape(link)}">
518
+ {html.escape(platform)}
519
+ </a>"""
520
+
521
+ # 生成营业时间详情
522
+ hours_detail = ""
523
+ if isinstance(contact_info['hours'], dict):
524
+ for day, time in contact_info['hours'].items():
525
+ hours_detail += f"<li>{html.escape(day)}: {html.escape(time)}</li>\n"
526
+ else:
527
+ hours_detail = f"<li>{html.escape(contact_info['hours'])}</li>"
528
+
529
+ html_doc = f"""<!DOCTYPE html>
530
+ <html lang="zh-CN">
531
+ <head>
532
+ <meta charset="UTF-8" />
533
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
534
+ <title>{html.escape(brand)} · 联系我们</title>
535
+ <link rel="stylesheet" href="assets/css/style.css" />
536
+ </head>
537
+ <body>
538
+ <header class="hero hero-ultra section text-center"
539
+ data-bg-topic="{ctx.get('hero_bg_topic', 'modern office reception')}"
540
+ id="home">
541
+ <div class="container hero-inner">
542
+ <span class="badge badge-soft mb-3">Contact</span>
543
+ <h1 class="display-5 mb-2">{html.escape(content_data.get('headline', f'联系{brand}'))}</h1>
544
+ <p class="section-lead mx-auto">{html.escape(content_data.get('subtitle', ''))}</p>
545
+ </div>
546
+ </header>
547
+
548
+ <main>
549
+ <section class="section">
550
+ <div class="container">
551
+ <div class="row g-4">
552
+ <div class="col-md-5">
553
+ <div class="contact-info-card reveal">
554
+ <h2 class="h4 mb-3">联系方式</h2>
555
+ <ul class="list-unstyled text-muted">
556
+ <li class="mb-2">
557
+ <strong>地址</strong>:{html.escape(contact_info['address'])}
558
+ </li>
559
+ <li class="mb-2">
560
+ <strong>电话</strong>:{html.escape(contact_info['phone'])}
561
+ </li>
562
+ <li class="mb-2">
563
+ <strong>邮箱</strong>:{html.escape(contact_info['email'])}
564
+ </li>
565
+ {f'<li class="mb-2"><strong>微信</strong>:{html.escape(contact_info["wechat"])}</li>' if contact_info['wechat'] else ''}
566
+ </ul>
567
+
568
+ <h3 class="h5 mt-4 mb-2">营业时间</h3>
569
+ <ul class="list-unstyled text-muted">
570
+ {hours_detail}
571
+ </ul>
572
+
573
+ {f'<div class="d-flex gap-2 mt-3">{social_links}</div>' if social_links else ''}
574
+ </div>
575
+ </div>
576
+
577
+ <div class="col-md-7">
578
+ <h2 class="h4 mb-3">{html.escape(content_data.get('form_title', '发送消息'))}</h2>
579
+ <p class="text-muted mb-3">{html.escape(content_data.get('form_text', ''))}</p>
580
+
581
+ <form class="contact-form reveal" action="{ctx.get('form_action', '#')}" method="POST">
582
+ <div class="field-pair">
583
+ <label>姓名 *</label>
584
+ <input type="text" name="name" placeholder="您的名字" required>
585
+ </div>
586
+ <div class="field-pair">
587
+ <label>邮箱 *</label>
588
+ <input type="email" name="email" placeholder="name@example.com" required>
589
+ </div>
590
+ <div class="field-pair">
591
+ <label>电话</label>
592
+ <input type="tel" name="phone" placeholder="您的联系电话">
593
+ </div>
594
+ <div class="field-pair">
595
+ <label>主题</label>
596
+ <select name="subject">
597
+ <option>一般咨询</option>
598
+ <option>商务合作</option>
599
+ <option>预约/预订</option>
600
+ <option>意见反馈</option>
601
+ <option>其他</option>
602
+ </select>
603
+ </div>
604
+ <div class="field-pair">
605
+ <label>留言 *</label>
606
+ <textarea name="message" rows="4"
607
+ placeholder="请描述您的需求或问题" required></textarea>
608
+ </div>
609
+ <p class="text-muted small">{html.escape(content_data.get('response_time', ''))}</p>
610
+ <button class="btn btn-primary" type="submit">发送消息</button>
611
+ </form>
612
+ </div>
613
+ </div>
614
+ </div>
615
+ </section>
616
+
617
+ {_generate_map_section(contact_info['address']) if ctx.get('show_map', False) else ''}
618
+
619
+ <section class="section section-alt">
620
+ <div class="container">
621
+ <h2 class="h3 text-center mb-4">常见问题</h2>
622
+ <div class="mx-auto" style="max-width: 700px;">
623
+ <details class="feature-card mb-3">
624
+ <summary class="fw-semibold">如何到达?</summary>
625
+ <div class="mt-2 text-muted">
626
+ {html.escape(ctx.get('directions', f'我们位于{contact_info["address"]},交通便利。'))}
627
+ </div>
628
+ </details>
629
+ <details class="feature-card mb-3">
630
+ <summary class="fw-semibold">是否提供停车位?</summary>
631
+ <div class="mt-2 text-muted">
632
+ {html.escape(ctx.get('parking_info', '提供免费停车位,请咨询工作人员。'))}
633
+ </div>
634
+ </details>
635
+ <details class="feature-card mb-3">
636
+ <summary class="fw-semibold">是否需要预约?</summary>
637
+ <div class="mt-2 text-muted">
638
+ {html.escape(ctx.get('reservation_info', '建议提前预约以确保最佳服务体验。'))}
639
+ </div>
640
+ </details>
641
+ </div>
642
+ </div>
643
+ </section>
644
+ </main>
645
+
646
+ <footer class="footer-minimal">
647
+ <div class="container">
648
+ <div class="footer-brand">
649
+ <span>{html.escape(brand)}</span>
650
+ <p>期待与您见面</p>
651
+ </div>
652
+ <div class="footer-meta">
653
+ <span>© {html.escape(brand)}</span>
654
+ <a href="mailto:{html.escape(contact_info['email'])}">{html.escape(contact_info['email'])}</a>
655
+ </div>
656
+ </div>
657
+ </footer>
658
+ <script src="assets/js/main.js"></script>
659
+ </body>
660
+ </html>"""
661
+
662
+ with open(file_path, 'w', encoding='utf-8') as f:
663
+ f.write(html_doc)
664
+
665
+ return f"联系页面已创建:{file_path}"
666
+
667
+
668
+ def _generate_map_section(address: str) -> str:
669
+ """生成地图区块(可选)"""
670
+ return f"""
671
+ <section class="section">
672
+ <div class="container">
673
+ <h2 class="h3 text-center mb-4">位置地图</h2>
674
+ <div class="feature-card glass p-3 reveal"
675
+ data-bg-topic="city map with location pin"
676
+ style="height:400px; border-radius: var(--radius-lg);"
677
+ aria-label="地图显示:{html.escape(address)}">
678
+ <div class="text-center" style="padding-top: 180px;">
679
+ <p class="text-muted">地图加载中...</p>
680
+ <p class="text-muted small">{html.escape(address)}</p>
681
+ </div>
682
+ </div>
683
+ </div>
684
+ </section>"""
685
+
686
+
687
+ # 保留原有的 create_html_file 函数以保证兼容性
688
+ from .html_templates import create_html_file
689
+
690
+
691
+ __all__ = [
692
+ "create_html_file",
693
+ "create_menu_page",
694
+ "create_about_page",
695
+ "create_contact_page",
696
+ ]