| <!doctype html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="utf-8"> |
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
| <meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width"> |
| <title>美食探索+ | AI助手</title> |
| |
| <script> |
| window._AMapSecurityConfig = { |
| securityJsCode: 'cf71cd668b9003a1144459e461092afb', |
| } |
| </script> |
| <script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=11b1daeff703d83adef3e84cd746ab84&plugin=AMap.CitySearch,AMap.PlaceSearch,AMap.Geocoder"></script> |
| |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style type="text/css"> |
| :root { |
| --primary-color: #ff6b6b; |
| --primary-gradient: linear-gradient(135deg, #ff6b6b, #ff8e8e); |
| --secondary-color: #4ecdc4; |
| --dark-color: #292f36; |
| --light-color: #f7f7f7; |
| --text-color: #333; |
| --text-secondary: #666; |
| --border-radius: 12px; |
| --box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08); |
| --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); |
| |
| |
| --ios-bg: #f2f2f7; |
| --ios-card-bg: #ffffff; |
| --ios-primary: #007aff; |
| --ios-secondary: #5ac8fa; |
| --ios-success: #34c759; |
| --ios-warning: #ff9500; |
| --ios-danger: #ff3b30; |
| --ios-light-text: #8e8e93; |
| --ios-dark-text: #1c1c1e; |
| --ios-border: #c6c6c8; |
| --ios-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); |
| --ios-radius: 12px; |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif; |
| -webkit-font-smoothing: antialiased; |
| -moz-osx-font-smoothing: grayscale; |
| } |
| |
| body { |
| height: 100vh; |
| display: flex; |
| flex-direction: column; |
| background-color: var(--ios-bg); |
| color: var(--ios-dark-text); |
| } |
| |
| .header { |
| background: var(--ios-card-bg); |
| color: var(--ios-dark-text); |
| padding: 15px 20px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| box-shadow: var(--ios-shadow); |
| z-index: 100; |
| border-bottom: 1px solid rgba(0,0,0,0.05); |
| } |
| |
| .header h1 { |
| font-size: 20px; |
| font-weight: 600; |
| margin: 0; |
| } |
| |
| .header-actions { |
| display: flex; |
| gap: 15px; |
| align-items: center; |
| } |
| |
| .btn-icon { |
| background: transparent; |
| border: none; |
| color: var(--ios-primary); |
| width: 36px; |
| height: 36px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| transition: var(--transition); |
| font-size: 18px; |
| } |
| |
| .btn-icon:active { |
| opacity: 0.7; |
| } |
| |
| .search-container { |
| background: var(--ios-card-bg); |
| padding: 12px 15px; |
| display: flex; |
| align-items: center; |
| box-shadow: var(--ios-shadow); |
| z-index: 99; |
| } |
| |
| .search-bar { |
| display: flex; |
| flex: 1; |
| position: relative; |
| } |
| |
| .search-bar input { |
| flex: 1; |
| padding: 10px 16px; |
| border: none; |
| border-radius: 10px; |
| font-size: 16px; |
| outline: none; |
| transition: var(--transition); |
| background: var(--ios-bg); |
| } |
| |
| .search-bar input:focus { |
| background: #ffffff; |
| box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1); |
| } |
| |
| .search-bar button { |
| position: absolute; |
| right: 10px; |
| top: 50%; |
| transform: translateY(-50%); |
| background: none; |
| border: none; |
| color: var(--ios-primary); |
| font-size: 16px; |
| cursor: pointer; |
| transition: var(--transition); |
| } |
| |
| .search-bar button:active { |
| opacity: 0.7; |
| } |
| |
| .city-selector { |
| display: flex; |
| align-items: center; |
| cursor: pointer; |
| padding: 5px 12px; |
| border-radius: 15px; |
| background: var(--ios-bg); |
| transition: var(--transition); |
| color: var(--ios-dark-text); |
| font-weight: 400; |
| font-size: 14px; |
| border: 1px solid transparent; |
| } |
| |
| .city-selector i { |
| margin-right: 5px; |
| color: var(--ios-primary); |
| } |
| |
| .city-selector:active { |
| background: #e5e5ea; |
| } |
| |
| .food-categories { |
| display: flex; |
| overflow-x: auto; |
| padding: 12px 15px; |
| background: var(--ios-card-bg); |
| margin-bottom: 10px; |
| box-shadow: var(--ios-shadow); |
| scrollbar-width: none; |
| gap: 8px; |
| } |
| |
| .food-categories::-webkit-scrollbar { |
| display: none; |
| } |
| |
| .category { |
| flex: 0 0 auto; |
| padding: 8px 16px; |
| background: var(--ios-bg); |
| border-radius: 20px; |
| cursor: pointer; |
| transition: var(--transition); |
| font-size: 14px; |
| white-space: nowrap; |
| font-weight: 400; |
| } |
| |
| .category.active { |
| background: var(--ios-primary); |
| color: white; |
| } |
| |
| .category:active:not(.active) { |
| background: #e5e5ea; |
| } |
| |
| .main-content { |
| display: flex; |
| flex: 1; |
| overflow: hidden; |
| position: relative; |
| } |
| |
| #container { |
| flex: 1; |
| height: 100%; |
| } |
| |
| #panel { |
| width: 360px; |
| background: var(--ios-card-bg); |
| overflow-y: auto; |
| box-shadow: -2px 0 10px rgba(0, 0, 0, 0.05); |
| padding: 0; |
| transition: var(--transition); |
| z-index: 10; |
| } |
| |
| .tabs { |
| display: flex; |
| border-bottom: 1px solid var(--ios-border); |
| background: var(--ios-card-bg); |
| } |
| |
| .tab { |
| padding: 15px; |
| cursor: pointer; |
| transition: var(--transition); |
| flex: 1; |
| text-align: center; |
| color: var(--ios-light-text); |
| font-weight: 500; |
| font-size: 14px; |
| } |
| |
| .tab.active { |
| color: var(--ios-primary); |
| border-bottom: 2px solid var(--ios-primary); |
| font-weight: 600; |
| } |
| |
| .tab-content { |
| display: none; |
| background: var(--ios-bg); |
| min-height: 300px; |
| } |
| |
| .tab-content.active { |
| display: block; |
| } |
| |
| .result-item { |
| background: var(--ios-card-bg); |
| border-radius: var(--ios-radius); |
| margin: 10px; |
| padding: 15px; |
| cursor: pointer; |
| transition: var(--transition); |
| position: relative; |
| box-shadow: var(--ios-shadow); |
| } |
| |
| .result-item:active { |
| transform: scale(0.98); |
| } |
| |
| .result-item h3 { |
| font-size: 16px; |
| margin-bottom: 8px; |
| color: var(--ios-dark-text); |
| font-weight: 600; |
| padding-right: 30px; |
| } |
| |
| .result-item p { |
| font-size: 14px; |
| color: var(--ios-light-text); |
| margin-bottom: 8px; |
| } |
| |
| .result-item .rating { |
| color: var(--ios-warning); |
| margin-bottom: 8px; |
| font-size: 14px; |
| } |
| |
| .result-item .address { |
| display: flex; |
| align-items: center; |
| font-size: 13px; |
| color: var(--ios-light-text); |
| } |
| |
| .result-item .address i { |
| margin-right: 5px; |
| color: var(--ios-primary); |
| } |
| |
| .favorite-btn { |
| position: absolute; |
| top: 15px; |
| right: 15px; |
| background: none; |
| border: none; |
| color: #ddd; |
| font-size: 18px; |
| cursor: pointer; |
| transition: var(--transition); |
| } |
| |
| .favorite-btn:hover, .favorite-btn.active { |
| color: var(--ios-warning); |
| } |
| |
| .favorite-btn.active { |
| transform: scale(1.1); |
| } |
| |
| .loading { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| display: none; |
| z-index: 5; |
| } |
| |
| .loading i { |
| font-size: 40px; |
| color: var(--ios-primary); |
| animation: spin 1s infinite linear; |
| } |
| |
| @keyframes spin { |
| from { transform: rotate(0deg); } |
| to { transform: rotate(360deg); } |
| } |
| |
| .no-results { |
| padding: 30px; |
| text-align: center; |
| color: var(--ios-light-text); |
| background: var(--ios-card-bg); |
| margin: 20px; |
| border-radius: var(--ios-radius); |
| } |
| |
| .modal { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0, 0, 0, 0.5); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| z-index: 1000; |
| opacity: 0; |
| visibility: hidden; |
| transition: var(--transition); |
| } |
| |
| .modal.active { |
| opacity: 1; |
| visibility: visible; |
| } |
| |
| .modal-content { |
| background: var(--ios-card-bg); |
| border-radius: var(--ios-radius); |
| width: 90%; |
| max-width: 500px; |
| max-height: 80vh; |
| overflow-y: auto; |
| box-shadow: var(--ios-shadow); |
| transform: translateY(-20px); |
| transition: transform 0.3s ease; |
| } |
| |
| .modal.active .modal-content { |
| transform: translateY(0); |
| } |
| |
| .modal-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 15px 20px; |
| border-bottom: 1px solid var(--ios-border); |
| } |
| |
| .modal-header h2 { |
| font-size: 18px; |
| font-weight: 600; |
| color: var(--ios-dark-text); |
| margin: 0; |
| } |
| |
| .modal-close { |
| background: none; |
| border: none; |
| font-size: 22px; |
| cursor: pointer; |
| color: var(--ios-light-text); |
| } |
| |
| .modal-body { |
| padding: 20px; |
| } |
| |
| .city-list { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 10px; |
| } |
| |
| .city-item { |
| padding: 12px 10px; |
| text-align: center; |
| background: var(--ios-bg); |
| border-radius: 10px; |
| cursor: pointer; |
| transition: var(--transition); |
| font-size: 14px; |
| } |
| |
| .city-item:active { |
| background: var(--ios-primary); |
| color: white; |
| } |
| |
| .form-group { |
| display: flex; |
| flex-direction: column; |
| gap: 5px; |
| margin-bottom: 15px; |
| } |
| |
| .form-group label { |
| font-size: 14px; |
| font-weight: 500; |
| color: var(--ios-dark-text); |
| } |
| |
| .form-group input, .form-group textarea, .form-group select { |
| padding: 12px; |
| border: 1px solid var(--ios-border); |
| border-radius: 10px; |
| font-size: 16px; |
| transition: var(--transition); |
| background: var(--ios-bg); |
| } |
| |
| .form-group input:focus, .form-group textarea:focus, .form-group select:focus { |
| border-color: var(--ios-primary); |
| outline: none; |
| box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1); |
| } |
| |
| .btn-submit { |
| background: var(--ios-primary); |
| color: white; |
| border: none; |
| padding: 12px; |
| border-radius: 10px; |
| font-weight: 500; |
| font-size: 16px; |
| cursor: pointer; |
| transition: var(--transition); |
| width: 100%; |
| } |
| |
| .btn-submit:active { |
| opacity: 0.8; |
| } |
| |
| .toast { |
| position: fixed; |
| bottom: 90px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: rgba(0, 0, 0, 0.8); |
| color: white; |
| padding: 12px 20px; |
| border-radius: 20px; |
| font-size: 14px; |
| z-index: 1000; |
| opacity: 0; |
| visibility: hidden; |
| transition: var(--transition); |
| } |
| |
| .toast.show { |
| opacity: 1; |
| visibility: visible; |
| } |
| |
| |
| .ai-assistant { |
| position: fixed; |
| bottom: 80px; |
| right: 20px; |
| width: 320px; |
| height: 450px; |
| background: var(--ios-card-bg); |
| border-radius: var(--ios-radius); |
| box-shadow: var(--ios-shadow); |
| display: flex; |
| flex-direction: column; |
| z-index: 1000; |
| overflow: hidden; |
| transform: translateY(20px); |
| opacity: 0; |
| visibility: hidden; |
| transition: var(--transition); |
| } |
| |
| .ai-assistant.active { |
| transform: translateY(0); |
| opacity: 1; |
| visibility: visible; |
| } |
| |
| .chat-header { |
| background: var(--ios-card-bg); |
| color: var(--ios-dark-text); |
| padding: 15px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| border-bottom: 1px solid var(--ios-border); |
| } |
| |
| .chat-header h3 { |
| font-size: 16px; |
| font-weight: 600; |
| margin: 0; |
| } |
| |
| .close-btn { |
| background: none; |
| border: none; |
| color: var(--ios-light-text); |
| font-size: 20px; |
| cursor: pointer; |
| padding: 0; |
| line-height: 1; |
| } |
| |
| .chat-messages { |
| flex: 1; |
| padding: 15px; |
| overflow-y: auto; |
| background: var(--ios-bg); |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .chat-input { |
| display: flex; |
| padding: 10px; |
| border-top: 1px solid var(--ios-border); |
| background: var(--ios-card-bg); |
| } |
| |
| .chat-input input { |
| flex: 1; |
| padding: 10px 15px; |
| border: 1px solid var(--ios-border); |
| border-radius: 18px; |
| font-size: 15px; |
| outline: none; |
| background: var(--ios-bg); |
| } |
| |
| .chat-input input:focus { |
| border-color: var(--ios-primary); |
| box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1); |
| } |
| |
| .chat-input button { |
| background: var(--ios-primary); |
| color: white; |
| border: none; |
| border-radius: 18px; |
| width: 36px; |
| height: 36px; |
| margin-left: 8px; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .chat-input button:active { |
| opacity: 0.8; |
| } |
| |
| .message { |
| margin-bottom: 12px; |
| padding: 10px 15px; |
| border-radius: 18px; |
| max-width: 80%; |
| font-size: 15px; |
| line-height: 1.4; |
| animation: message-appear 0.3s ease; |
| } |
| |
| @keyframes message-appear { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .user-message { |
| background: var(--ios-primary); |
| color: white; |
| align-self: flex-end; |
| border-bottom-right-radius: 5px; |
| } |
| |
| .ai-message { |
| background: white; |
| color: var(--ios-dark-text); |
| align-self: flex-start; |
| border-bottom-left-radius: 5px; |
| box-shadow: var(--ios-shadow); |
| } |
| |
| .ai-message a { |
| color: var(--ios-primary); |
| text-decoration: none; |
| } |
| |
| .ai-thinking { |
| display: flex; |
| align-items: center; |
| padding: 8px 15px; |
| background: white; |
| border-radius: 18px; |
| align-self: flex-start; |
| margin-bottom: 12px; |
| border-bottom-left-radius: 5px; |
| box-shadow: var(--ios-shadow); |
| font-size: 15px; |
| } |
| |
| .thinking-dots { |
| display: flex; |
| margin-left: 8px; |
| } |
| |
| .thinking-dots span { |
| width: 8px; |
| height: 8px; |
| background: var(--ios-light-text); |
| border-radius: 50%; |
| margin: 0 2px; |
| opacity: 0.5; |
| animation: thinking 1.4s infinite; |
| } |
| |
| .thinking-dots span:nth-child(2) { |
| animation-delay: 0.2s; |
| } |
| |
| .thinking-dots span:nth-child(3) { |
| animation-delay: 0.4s; |
| } |
| |
| @keyframes thinking { |
| 0%, 60%, 100% { |
| transform: translateY(0); |
| opacity: 0.5; |
| } |
| 30% { |
| transform: translateY(-4px); |
| opacity: 1; |
| } |
| } |
| |
| .open-assistant-btn { |
| position: fixed; |
| bottom: 20px; |
| right: 20px; |
| width: 50px; |
| height: 50px; |
| border-radius: 25px; |
| background: var(--ios-primary); |
| color: white; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3); |
| cursor: pointer; |
| z-index: 999; |
| border: none; |
| font-size: 20px; |
| transition: all 0.2s; |
| } |
| |
| .open-assistant-btn:active { |
| transform: scale(0.92); |
| } |
| |
| .chat-suggestions { |
| display: flex; |
| flex-wrap: wrap; |
| padding: 10px; |
| gap: 8px; |
| border-top: 1px solid var(--ios-border); |
| background: var(--ios-card-bg); |
| } |
| |
| .suggestion-chip { |
| background: var(--ios-bg); |
| padding: 8px 12px; |
| border-radius: 15px; |
| font-size: 13px; |
| cursor: pointer; |
| transition: background 0.2s; |
| color: var(--ios-primary); |
| } |
| |
| .suggestion-chip:active { |
| background: #e5e5ea; |
| } |
| |
| .map-poi-info { |
| margin-top: 10px; |
| background: white; |
| border-radius: 12px; |
| padding: 12px; |
| border: 1px solid var(--ios-border); |
| box-shadow: var(--ios-shadow); |
| } |
| |
| .map-poi-info h4 { |
| font-size: 15px; |
| margin-bottom: 5px; |
| font-weight: 600; |
| } |
| |
| .map-poi-info p { |
| font-size: 13px; |
| color: var(--ios-light-text); |
| margin-bottom: 8px; |
| } |
| |
| .poi-actions { |
| display: flex; |
| gap: 8px; |
| } |
| |
| .poi-action-btn { |
| flex: 1; |
| padding: 8px; |
| border: none; |
| border-radius: 8px; |
| font-size: 13px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 5px; |
| cursor: pointer; |
| transition: var(--transition); |
| } |
| |
| .poi-action-btn.view { |
| background: var(--ios-bg); |
| color: var(--ios-dark-text); |
| } |
| |
| .poi-action-btn.navigate { |
| background: var(--ios-primary); |
| color: white; |
| } |
| |
| .poi-action-btn:active { |
| opacity: 0.8; |
| } |
| |
| .loading-indicator i { |
| animation: spin 1s infinite linear; |
| color: var(--ios-primary); |
| } |
| |
| @media (max-width: 768px) { |
| .main-content { |
| flex-direction: column; |
| } |
| |
| #container { |
| height: 50%; |
| } |
| |
| #panel { |
| width: 100%; |
| height: 50%; |
| box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); |
| } |
| |
| .header h1 { |
| font-size: 18px; |
| } |
| |
| .ai-assistant { |
| width: 100%; |
| height: 70%; |
| right: 0; |
| bottom: 0; |
| border-radius: var(--ios-radius) var(--ios-radius) 0 0; |
| } |
| |
| .city-list { |
| grid-template-columns: repeat(2, 1fr); |
| } |
| } |
| |
| |
| .custom-info-window { |
| background: white; |
| border-radius: var(--ios-radius); |
| box-shadow: var(--ios-shadow); |
| padding: 15px; |
| width: 280px; |
| } |
| |
| .info-window-header { |
| display: flex; |
| justify-content: space-between; |
| margin-bottom: 10px; |
| } |
| |
| .info-window-title { |
| font-weight: 600; |
| font-size: 16px; |
| } |
| |
| .info-window-favorite { |
| color: #ddd; |
| cursor: pointer; |
| transition: var(--transition); |
| } |
| |
| .info-window-favorite:hover, .info-window-favorite.active { |
| color: var(--ios-warning); |
| } |
| |
| .info-window-body { |
| font-size: 14px; |
| } |
| |
| .info-window-rating { |
| color: var(--ios-warning); |
| margin-bottom: 8px; |
| } |
| |
| .info-window-address { |
| color: var(--ios-light-text); |
| margin-bottom: 12px; |
| display: flex; |
| align-items: center; |
| } |
| |
| .info-window-address i { |
| margin-right: 5px; |
| color: var(--ios-primary); |
| } |
| |
| .info-window-actions { |
| display: flex; |
| gap: 8px; |
| } |
| |
| .info-window-btn { |
| flex: 1; |
| padding: 8px; |
| border: none; |
| border-radius: 8px; |
| font-size: 13px; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 5px; |
| transition: var(--transition); |
| } |
| |
| .info-window-btn.navigate { |
| background: var(--ios-primary); |
| color: white; |
| } |
| |
| .info-window-btn.share { |
| background: var(--ios-bg); |
| color: var(--ios-dark-text); |
| } |
| |
| .info-window-btn:active { |
| opacity: 0.8; |
| } |
| |
| |
| .route-guidance { |
| position: fixed; |
| bottom: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: white; |
| border-radius: var(--ios-radius); |
| box-shadow: var(--ios-shadow); |
| padding: 15px; |
| width: 90%; |
| max-width: 400px; |
| z-index: 900; |
| display: none; |
| } |
| |
| .route-guidance.active { |
| display: block; |
| animation: slide-up 0.3s ease; |
| } |
| |
| @keyframes slide-up { |
| from { |
| transform: translate(-50%, 50px); |
| opacity: 0; |
| } |
| to { |
| transform: translate(-50%, 0); |
| opacity: 1; |
| } |
| } |
| |
| .route-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 12px; |
| } |
| |
| .route-header h3 { |
| font-size: 16px; |
| font-weight: 600; |
| margin: 0; |
| } |
| |
| .route-close { |
| background: none; |
| border: none; |
| color: var(--ios-light-text); |
| font-size: 18px; |
| cursor: pointer; |
| } |
| |
| .route-stops { |
| display: flex; |
| flex-direction: column; |
| gap: 5px; |
| margin-bottom: 15px; |
| max-height: 150px; |
| overflow-y: auto; |
| } |
| |
| .route-stop { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| padding: 8px; |
| border-radius: 8px; |
| background: var(--ios-bg); |
| } |
| |
| .stop-number { |
| width: 24px; |
| height: 24px; |
| border-radius: 50%; |
| background: var(--ios-primary); |
| color: white; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 12px; |
| font-weight: 600; |
| } |
| |
| .stop-info { |
| flex: 1; |
| } |
| |
| .stop-name { |
| font-size: 14px; |
| font-weight: 500; |
| } |
| |
| .stop-address { |
| font-size: 12px; |
| color: var(--ios-light-text); |
| } |
| |
| .route-actions { |
| display: flex; |
| gap: 8px; |
| } |
| |
| .route-btn { |
| flex: 1; |
| padding: 10px; |
| border: none; |
| border-radius: 8px; |
| font-size: 14px; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 5px; |
| } |
| |
| .route-btn.primary { |
| background: var(--ios-primary); |
| color: white; |
| } |
| |
| .route-btn.secondary { |
| background: var(--ios-bg); |
| color: var(--ios-dark-text); |
| } |
| |
| .fab-menu { |
| position: fixed; |
| bottom: 80px; |
| right: 20px; |
| display: flex; |
| flex-direction: column; |
| align-items: flex-end; |
| gap: 12px; |
| z-index: 990; |
| opacity: 0; |
| visibility: hidden; |
| transition: var(--transition); |
| } |
| |
| .fab-menu.active { |
| opacity: 1; |
| visibility: visible; |
| } |
| |
| .fab-item { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| animation: slide-left 0.3s ease forwards; |
| opacity: 0; |
| transform: translateX(20px); |
| } |
| |
| @keyframes slide-left { |
| to { |
| opacity: 1; |
| transform: translateX(0); |
| } |
| } |
| |
| .fab-item:nth-child(1) { |
| animation-delay: 0s; |
| } |
| |
| .fab-item:nth-child(2) { |
| animation-delay: 0.05s; |
| } |
| |
| .fab-item:nth-child(3) { |
| animation-delay: 0.1s; |
| } |
| |
| .fab-label { |
| background: rgba(0, 0, 0, 0.7); |
| color: white; |
| padding: 6px 12px; |
| border-radius: 15px; |
| font-size: 13px; |
| box-shadow: var(--ios-shadow); |
| } |
| |
| .fab-button { |
| width: 40px; |
| height: 40px; |
| border-radius: 20px; |
| background: white; |
| color: var(--ios-primary); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| box-shadow: var(--ios-shadow); |
| cursor: pointer; |
| border: none; |
| font-size: 16px; |
| } |
| |
| .fab-button:active { |
| transform: scale(0.95); |
| } |
| |
| .fab-button.share { |
| background: var(--ios-secondary); |
| color: white; |
| } |
| |
| .fab-button.favorite { |
| background: var(--ios-warning); |
| color: white; |
| } |
| |
| .fab-button.route { |
| background: var(--ios-success); |
| color: white; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="header"> |
| <h1>美食探索+</h1> |
| <div class="header-actions"> |
| <div id="current-city" class="city-selector"> |
| <i class="fas fa-map-marker-alt"></i> |
| <span>定位中...</span> |
| </div> |
| <button id="favorites-btn" class="btn-icon"> |
| <i class="fas fa-heart"></i> |
| </button> |
| </div> |
| </div> |
| |
| <div class="search-container"> |
| <div class="search-bar"> |
| <input type="text" id="keyword" placeholder="搜索美食、餐厅" value="美食"> |
| <button id="search-btn"><i class="fas fa-search"></i></button> |
| </div> |
| </div> |
| |
| <div class="food-categories"> |
| <div class="category active" data-keyword="美食">全部美食</div> |
| <div class="category" data-keyword="火锅">火锅</div> |
| <div class="category" data-keyword="烧烤">烧烤</div> |
| <div class="category" data-keyword="小吃">特色小吃</div> |
| <div class="category" data-keyword="西餐">西餐</div> |
| <div class="category" data-keyword="日料">日料</div> |
| <div class="category" data-keyword="甜点">甜点</div> |
| <div class="category" data-keyword="咖啡">咖啡</div> |
| <div class="category" data-keyword="面包">面包</div> |
| <div class="category" data-keyword="早餐">早餐</div> |
| </div> |
| |
| <div class="main-content"> |
| <div id="container"></div> |
| <div id="panel"> |
| <div class="tabs"> |
| <div class="tab active" data-tab="results">搜索结果</div> |
| <div class="tab" data-tab="community">美食分享</div> |
| </div> |
| <div id="results-content" class="tab-content active"></div> |
| <div id="community-content" class="tab-content"> |
| |
| </div> |
| </div> |
| </div> |
| |
| <div class="loading"> |
| <i class="fas fa-spinner"></i> |
| </div> |
| |
| |
| <div id="city-modal" class="modal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h2>选择城市</h2> |
| <button class="modal-close">×</button> |
| </div> |
| <div class="modal-body"> |
| <div class="city-list"> |
| <div class="city-item" data-city="北京" data-adcode="010">北京</div> |
| <div class="city-item" data-city="上海" data-adcode="021">上海</div> |
| <div class="city-item" data-city="广州" data-adcode="020">广州</div> |
| <div class="city-item" data-city="深圳" data-adcode="0755">深圳</div> |
| <div class="city-item" data-city="杭州" data-adcode="0571">杭州</div> |
| <div class="city-item" data-city="南京" data-adcode="025">南京</div> |
| <div class="city-item" data-city="武汉" data-adcode="027">武汉</div> |
| <div class="city-item" data-city="成都" data-adcode="028">成都</div> |
| <div class="city-item" data-city="重庆" data-adcode="023">重庆</div> |
| <div class="city-item" data-city="西安" data-adcode="029">西安</div> |
| <div class="city-item" data-city="天津" data-adcode="022">天津</div> |
| <div class="city-item" data-city="苏州" data-adcode="0512">苏州</div> |
| <div class="city-item" data-city="厦门" data-adcode="0592">厦门</div> |
| <div class="city-item" data-city="青岛" data-adcode="0532">青岛</div> |
| <div class="city-item" data-city="大连" data-adcode="0411">大连</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="share-modal" class="modal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h2>分享美食</h2> |
| <button class="modal-close">×</button> |
| </div> |
| <div class="modal-body"> |
| <div id="share-preview" class="share-preview"> |
| <div class="share-preview-img"> |
| <i class="fas fa-utensils"></i> |
| </div> |
| <div class="share-preview-info"> |
| <h3>未选择地点</h3> |
| <p>请先在地图上选择一个地点</p> |
| </div> |
| </div> |
| <form id="share-form" class="share-form"> |
| <div class="form-group"> |
| <label for="share-rating">评分</label> |
| <select id="share-rating" required> |
| <option value="5">5分 - 超赞</option> |
| <option value="4" selected>4分 - 很好</option> |
| <option value="3">3分 - 一般</option> |
| <option value="2">2分 - 较差</option> |
| <option value="1">1分 - 很差</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label for="share-comments">评价</label> |
| <textarea id="share-comments" rows="4" placeholder="分享您的用餐体验..." required></textarea> |
| </div> |
| <div class="form-group"> |
| <label for="share-image">上传图片</label> |
| <input type="file" id="share-image" accept="image/*"> |
| </div> |
| <button type="submit" class="btn-submit">发布分享</button> |
| </form> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="favorites-modal" class="modal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h2>我的收藏</h2> |
| <button class="modal-close">×</button> |
| </div> |
| <div class="modal-body"> |
| <div id="favorites-list" class="favorites-list"> |
| |
| </div> |
| <div id="empty-favorites" class="no-results"> |
| <p>您还没有收藏任何地点</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <button id="open-assistant" class="open-assistant-btn"> |
| <i class="fas fa-robot"></i> |
| </button> |
| |
| |
| <div id="ai-assistant" class="ai-assistant"> |
| <div class="chat-header"> |
| <h3>AI美食助手</h3> |
| <button id="close-chat" class="close-btn">×</button> |
| </div> |
| <div id="chat-messages" class="chat-messages"> |
| |
| </div> |
| <div class="chat-input"> |
| <input type="text" id="user-input" placeholder="问我关于美食的问题..."> |
| <button id="send-message"><i class="fas fa-paper-plane"></i></button> |
| </div> |
| <div id="suggestions" class="chat-suggestions"> |
| <div class="suggestion-chip">附近有什么好吃的?</div> |
| <div class="suggestion-chip">推荐火锅店</div> |
| <div class="suggestion-chip">帮我规划美食路线</div> |
| <div class="suggestion-chip">有什么深夜美食?</div> |
| </div> |
| </div> |
| |
| |
| <div id="route-guidance" class="route-guidance"> |
| <div class="route-header"> |
| <h3>美食探索路线</h3> |
| <button id="close-route" class="route-close">×</button> |
| </div> |
| <div id="route-stops" class="route-stops"> |
| |
| </div> |
| <div class="route-actions"> |
| <button id="start-navigation" class="route-btn primary"> |
| <i class="fas fa-directions"></i> 开始导航 |
| </button> |
| <button id="modify-route" class="route-btn secondary"> |
| <i class="fas fa-edit"></i> 修改路线 |
| </button> |
| </div> |
| </div> |
| |
| |
| <div id="fab-menu" class="fab-menu"> |
| <div class="fab-item"> |
| <div class="fab-label">规划美食路线</div> |
| <button class="fab-button route" id="fab-route"> |
| <i class="fas fa-route"></i> |
| </button> |
| </div> |
| <div class="fab-item"> |
| <div class="fab-label">收藏此地点</div> |
| <button class="fab-button favorite" id="fab-favorite"> |
| <i class="fas fa-heart"></i> |
| </button> |
| </div> |
| <div class="fab-item"> |
| <div class="fab-label">分享美食体验</div> |
| <button class="fab-button share" id="fab-share"> |
| <i class="fas fa-share-alt"></i> |
| </button> |
| </div> |
| </div> |
| |
| |
| <div id="toast" class="toast"></div> |
|
|
| <script type="text/javascript"> |
| |
| var map = null; |
| var placeSearch = null; |
| var infoWindow = null; |
| var markers = []; |
| var favorites = JSON.parse(localStorage.getItem('foodExplorerFavorites')) || []; |
| var sharedPosts = JSON.parse(localStorage.getItem('foodExplorerSharedPosts')) || []; |
| var currentPOI = null; |
| |
| |
| var currentPage = 1; |
| var currentTotalPages = 1; |
| var isLoading = false; |
| |
| |
| var currentCity = { |
| name: '北京', |
| adcode: '010' |
| }; |
| |
| |
| var currentKeyword = '美食'; |
| |
| |
| const AI_ASSISTANT = { |
| apiKey: 'sk-0AvjMPgKyTW1HvBmFe18Cc90C32b48DbAb921e4cBb4eB4B2', |
| conversationHistory: [], |
| currentLocation: null, |
| userPreferences: { |
| favoriteCuisines: [], |
| dietaryRestrictions: [], |
| priceRange: null, |
| previousSearches: [] |
| } |
| }; |
| |
| |
| function initMap() { |
| map = new AMap.Map("container", { |
| resizeEnable: true, |
| zoom: 12, |
| center: [116.397428, 39.90923] |
| }); |
| |
| |
| infoWindow = new AMap.InfoWindow({ |
| isCustom: true, |
| autoMove: true, |
| offset: new AMap.Pixel(0, -40) |
| }); |
| |
| |
| getCityByIP(); |
| |
| |
| map.plugin(['AMap.ToolBar', 'AMap.Scale'], function() { |
| map.addControl(new AMap.ToolBar()); |
| map.addControl(new AMap.Scale()); |
| }); |
| |
| |
| map.on('click', function() { |
| infoWindow.close(); |
| }); |
| } |
| |
| |
| function updateCityText() { |
| document.querySelector('#current-city span').textContent = currentCity.name; |
| } |
| |
| |
| function getCityByIP() { |
| |
| var citysearch = new AMap.CitySearch(); |
| |
| |
| citysearch.getLocalCity(function(status, result) { |
| if (status === 'complete' && result.info === 'OK') { |
| if (result && result.city && result.bounds) { |
| currentCity = { |
| name: result.city, |
| adcode: result.adcode |
| }; |
| |
| updateCityText(); |
| |
| |
| map.setBounds(result.bounds); |
| |
| |
| initPlaceSearch(); |
| |
| |
| loadCommunityContent(); |
| } |
| } else { |
| |
| updateCityText(); |
| initPlaceSearch(); |
| loadCommunityContent(); |
| } |
| }); |
| |
| |
| getCurrentLocation(); |
| } |
| |
| |
| function getCurrentLocation() { |
| if (navigator.geolocation) { |
| navigator.geolocation.getCurrentPosition( |
| function(position) { |
| const coords = [position.coords.longitude, position.coords.latitude]; |
| AI_ASSISTANT.currentLocation = coords; |
| |
| |
| markUserLocation(coords); |
| }, |
| function(error) { |
| console.error("Error getting location:", error); |
| |
| const center = map.getCenter(); |
| AI_ASSISTANT.currentLocation = [center.lng, center.lat]; |
| |
| |
| markUserLocation([center.lng, center.lat]); |
| } |
| ); |
| } else { |
| |
| const center = map.getCenter(); |
| AI_ASSISTANT.currentLocation = [center.lng, center.lat]; |
| |
| |
| markUserLocation([center.lng, center.lat]); |
| } |
| } |
| |
| |
| function markUserLocation(location) { |
| |
| map.remove(markers.filter(marker => marker.getExtData && marker.getExtData().type === 'userLocation')); |
| |
| |
| const position = new AMap.LngLat(location[0], location[1]); |
| |
| |
| var userMarker = new AMap.Marker({ |
| position: position, |
| icon: new AMap.Icon({ |
| |
| image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png', |
| size: new AMap.Size(25, 34), |
| imageSize: new AMap.Size(25, 34) |
| }), |
| offset: new AMap.Pixel(-12, -34), |
| zIndex: 200, |
| title: '我的位置' |
| }); |
| |
| |
| userMarker.setExtData({ |
| type: 'userLocation' |
| }); |
| |
| map.add(userMarker); |
| markers.push(userMarker); |
| map.setCenter(position); |
| |
| |
| var circle = new AMap.Circle({ |
| center: position, |
| radius: 100, |
| fillColor: '#1791fc', |
| fillOpacity: 0.1, |
| strokeColor: '#1791fc', |
| strokeOpacity: 0.3, |
| strokeWeight: 1 |
| }); |
| map.add(circle); |
| |
| return userMarker; |
| } |
| |
| |
| function initPlaceSearch() { |
| placeSearch = new AMap.PlaceSearch({ |
| pageSize: 20, |
| pageIndex: 1, |
| city: currentCity.adcode, |
| citylimit: true, |
| autoFitView: true, |
| extensions: 'all' |
| }); |
| |
| |
| placeSearch.on('complete', function(results) { |
| document.querySelector('.loading').style.display = 'none'; |
| if (results.info === 'OK') { |
| customizeResultList(results.poiList.pois); |
| addMarkersToMap(results.poiList.pois); |
| |
| |
| currentTotalPages = Math.ceil(results.poiList.count / results.poiList.pageSize); |
| currentPage = 1; |
| } else { |
| document.getElementById('results-content').innerHTML = '<div class="no-results">没有找到相关结果</div>'; |
| } |
| }); |
| |
| |
| searchPOI(); |
| } |
| |
| |
| function searchPOI() { |
| if (!placeSearch) { |
| return; |
| } |
| |
| |
| clearMarkers(); |
| |
| document.querySelector('.loading').style.display = 'block'; |
| placeSearch.setCity(currentCity.adcode); |
| placeSearch.setType('餐饮'); |
| placeSearch.setPageIndex(1); |
| currentPage = 1; |
| |
| |
| placeSearch.search(currentKeyword, function(status, result) { |
| document.querySelector('.loading').style.display = 'none'; |
| if (status === 'error' || status === 'no_data') { |
| document.getElementById('results-content').innerHTML = '<div class="no-results">没有找到相关结果</div>'; |
| } |
| }); |
| } |
| |
| |
| function searchMorePOI() { |
| if (isLoading || currentPage >= currentTotalPages) return; |
| |
| isLoading = true; |
| document.querySelector('.loading').style.display = 'block'; |
| |
| currentPage++; |
| placeSearch.setPageIndex(currentPage); |
| |
| placeSearch.search(currentKeyword, function(status, result) { |
| document.querySelector('.loading').style.display = 'none'; |
| isLoading = false; |
| |
| if (status === 'complete' && result.info === 'OK') { |
| |
| appendResultList(result.poiList.pois); |
| addMarkersToMap(result.poiList.pois, true); |
| } |
| }); |
| } |
| |
| |
| function clearMarkers() { |
| if (markers.length > 0) { |
| map.remove(markers); |
| markers = []; |
| } |
| } |
| |
| |
| function addMarkersToMap(pois, append = false) { |
| if (!append) { |
| clearMarkers(); |
| } |
| |
| pois.forEach(function(poi) { |
| var marker = new AMap.Marker({ |
| position: poi.location, |
| title: poi.name, |
| map: map |
| }); |
| |
| marker.on('click', function() { |
| showInfoWindow(poi); |
| currentPOI = poi; |
| }); |
| |
| markers.push(marker); |
| }); |
| } |
| |
| |
| function showInfoWindow(poi) { |
| |
| var rating = (Math.random() * 1.0 + 4.0).toFixed(1); |
| var price = Math.floor(Math.random() * 180 + 20); |
| |
| |
| var stars = ''; |
| var fullStars = Math.floor(rating); |
| var halfStar = rating % 1 >= 0.5; |
| |
| for (var i = 0; i < 5; i++) { |
| if (i < fullStars) { |
| stars += '<i class="fas fa-star"></i>'; |
| } else if (halfStar && i === fullStars) { |
| stars += '<i class="fas fa-star-half-alt"></i>'; |
| halfStar = false; |
| } else { |
| stars += '<i class="far fa-star"></i>'; |
| } |
| } |
| |
| |
| var isFavorite = favorites.some(function(fav) { |
| return fav.id === poi.id; |
| }); |
| |
| |
| var content = ` |
| <div class="custom-info-window"> |
| <div class="info-window-header"> |
| <div class="info-window-title">${poi.name}</div> |
| <div class="info-window-favorite ${isFavorite ? 'active' : ''}" onclick="toggleFavorite(event)"> |
| <i class="fas fa-heart"></i> |
| </div> |
| </div> |
| <div class="info-window-body"> |
| <div class="info-window-rating">${stars} ${rating} · 人均 ¥${price}</div> |
| <div class="info-window-address"><i class="fas fa-map-marker-alt"></i>${poi.address || poi.location.toString()}</div> |
| <div class="info-window-actions"> |
| <button class="info-window-btn navigate" onclick="navigateTo()"><i class="fas fa-directions"></i>导航</button> |
| <button class="info-window-btn share" onclick="openShareModal()"><i class="fas fa-share-alt"></i>分享</button> |
| </div> |
| </div> |
| </div> |
| `; |
| |
| infoWindow.setContent(content); |
| infoWindow.open(map, poi.location); |
| |
| |
| showFabMenu(); |
| } |
| |
| |
| function customizeResultList(pois) { |
| var resultsContent = document.getElementById('results-content'); |
| resultsContent.innerHTML = ''; |
| |
| if (!pois || pois.length === 0) { |
| resultsContent.innerHTML = '<div class="no-results">没有找到相关结果</div>'; |
| return; |
| } |
| |
| pois.forEach(function(poi) { |
| var item = document.createElement('div'); |
| item.className = 'result-item'; |
| |
| |
| var rating = (Math.random() * 1.0 + 4.0).toFixed(1); |
| var price = Math.floor(Math.random() * 180 + 20); |
| |
| |
| var stars = ''; |
| var fullStars = Math.floor(rating); |
| var halfStar = rating % 1 >= 0.5; |
| |
| for (var i = 0; i < 5; i++) { |
| if (i < fullStars) { |
| stars += '<i class="fas fa-star"></i>'; |
| } else if (halfStar && i === fullStars) { |
| stars += '<i class="fas fa-star-half-alt"></i>'; |
| halfStar = false; |
| } else { |
| stars += '<i class="far fa-star"></i>'; |
| } |
| } |
| |
| |
| var isFavorite = favorites.some(function(fav) { |
| return fav.id === poi.id; |
| }); |
| |
| item.innerHTML = ` |
| <h3>${poi.name}</h3> |
| <div class="rating">${stars} ${rating} · 人均 ¥${price}</div> |
| <p>${poi.type || '特色美食'}</p> |
| <div class="address"><i class="fas fa-map-marker-alt"></i>${poi.address || poi.location.toString()}</div> |
| <button class="favorite-btn ${isFavorite ? 'active' : ''}" data-id="${poi.id}"> |
| <i class="fas fa-heart"></i> |
| </button> |
| `; |
| |
| |
| item.addEventListener('click', function(e) { |
| if (!e.target.closest('.favorite-btn')) { |
| map.setCenter(poi.location); |
| map.setZoom(15); |
| showInfoWindow(poi); |
| currentPOI = poi; |
| } |
| }); |
| |
| |
| var favBtn = item.querySelector('.favorite-btn'); |
| favBtn.addEventListener('click', function(e) { |
| e.stopPropagation(); |
| var id = this.getAttribute('data-id'); |
| toggleFavoriteById(id, poi, this); |
| }); |
| |
| resultsContent.appendChild(item); |
| }); |
| } |
| |
| |
| function appendResultList(pois) { |
| var resultsContent = document.getElementById('results-content'); |
| |
| if (!pois || pois.length === 0) return; |
| |
| pois.forEach(function(poi) { |
| var item = document.createElement('div'); |
| item.className = 'result-item'; |
| |
| |
| var rating = (Math.random() * 1.0 + 4.0).toFixed(1); |
| var price = Math.floor(Math.random() * 180 + 20); |
| |
| |
| var stars = ''; |
| var fullStars = Math.floor(rating); |
| var halfStar = rating % 1 >= 0.5; |
| |
| for (var i = 0; i < 5; i++) { |
| if (i < fullStars) { |
| stars += '<i class="fas fa-star"></i>'; |
| } else if (halfStar && i === fullStars) { |
| stars += '<i class="fas fa-star-half-alt"></i>'; |
| halfStar = false; |
| } else { |
| stars += '<i class="far fa-star"></i>'; |
| } |
| } |
| |
| |
| var isFavorite = favorites.some(function(fav) { |
| return fav.id === poi.id; |
| }); |
| |
| item.innerHTML = ` |
| <h3>${poi.name}</h3> |
| <div class="rating">${stars} ${rating} · 人均 ¥${price}</div> |
| <p>${poi.type || '特色美食'}</p> |
| <div class="address"><i class="fas fa-map-marker-alt"></i>${poi.address || poi.location.toString()}</div> |
| <button class="favorite-btn ${isFavorite ? 'active' : ''}" data-id="${poi.id}"> |
| <i class="fas fa-heart"></i> |
| </button> |
| `; |
| |
| |
| item.addEventListener('click', function(e) { |
| if (!e.target.closest('.favorite-btn')) { |
| map.setCenter(poi.location); |
| map.setZoom(15); |
| showInfoWindow(poi); |
| currentPOI = poi; |
| } |
| }); |
| |
| |
| var favBtn = item.querySelector('.favorite-btn'); |
| favBtn.addEventListener('click', function(e) { |
| e.stopPropagation(); |
| var id = this.getAttribute('data-id'); |
| toggleFavoriteById(id, poi, this); |
| }); |
| |
| resultsContent.appendChild(item); |
| }); |
| } |
| |
| |
| function toggleFavorite(event) { |
| event.stopPropagation(); |
| if (!currentPOI) return; |
| |
| var favoriteBtn = event.currentTarget; |
| var isFavorite = favoriteBtn.classList.contains('active'); |
| |
| if (isFavorite) { |
| |
| favorites = favorites.filter(function(fav) { |
| return fav.id !== currentPOI.id; |
| }); |
| favoriteBtn.classList.remove('active'); |
| showToast('已取消收藏'); |
| } else { |
| |
| favorites.push({ |
| id: currentPOI.id, |
| name: currentPOI.name, |
| address: currentPOI.address || currentPOI.location.toString(), |
| location: [currentPOI.location.lng, currentPOI.location.lat], |
| type: currentPOI.type || '特色美食' |
| }); |
| favoriteBtn.classList.add('active'); |
| showToast('已添加到收藏'); |
| } |
| |
| |
| localStorage.setItem('foodExplorerFavorites', JSON.stringify(favorites)); |
| |
| |
| updateResultListFavoriteStatus(); |
| } |
| |
| |
| function toggleFavoriteById(id, poi, button) { |
| var isFavorite = button.classList.contains('active'); |
| |
| if (isFavorite) { |
| |
| favorites = favorites.filter(function(fav) { |
| return fav.id !== id; |
| }); |
| button.classList.remove('active'); |
| showToast('已取消收藏'); |
| } else { |
| |
| favorites.push({ |
| id: id, |
| name: poi.name, |
| address: poi.address || poi.location.toString(), |
| location: [poi.location.lng, poi.location.lat], |
| type: poi.type || '特色美食' |
| }); |
| button.classList.add('active'); |
| showToast('已添加到收藏'); |
| } |
| |
| |
| localStorage.setItem('foodExplorerFavorites', JSON.stringify(favorites)); |
| |
| |
| if (currentPOI && currentPOI.id === id && infoWindow.getIsOpen()) { |
| var favoriteBtn = document.querySelector('.info-window-favorite'); |
| if (isFavorite) { |
| favoriteBtn.classList.remove('active'); |
| } else { |
| favoriteBtn.classList.add('active'); |
| } |
| } |
| } |
| |
| |
| function updateResultListFavoriteStatus() { |
| var favBtns = document.querySelectorAll('.result-item .favorite-btn'); |
| favBtns.forEach(function(btn) { |
| var id = btn.getAttribute('data-id'); |
| var isFavorite = favorites.some(function(fav) { |
| return fav.id === id; |
| }); |
| |
| if (isFavorite) { |
| btn.classList.add('active'); |
| } else { |
| btn.classList.remove('active'); |
| } |
| }); |
| } |
| |
| |
| function showFavorites() { |
| var favoritesList = document.getElementById('favorites-list'); |
| var emptyFavorites = document.getElementById('empty-favorites'); |
| |
| favoritesList.innerHTML = ''; |
| |
| if (favorites.length === 0) { |
| favoritesList.style.display = 'none'; |
| emptyFavorites.style.display = 'block'; |
| return; |
| } |
| |
| favoritesList.style.display = 'block'; |
| emptyFavorites.style.display = 'none'; |
| |
| favorites.forEach(function(fav) { |
| var item = document.createElement('div'); |
| item.className = 'favorites-item'; |
| |
| item.innerHTML = ` |
| <div class="favorites-info"> |
| <h3>${fav.name}</h3> |
| <p>${fav.type}</p> |
| <p class="address"><i class="fas fa-map-marker-alt"></i>${fav.address}</p> |
| </div> |
| <div class="favorites-actions"> |
| <button class="delete-favorite" data-id="${fav.id}"><i class="fas fa-trash"></i></button> |
| </div> |
| `; |
| |
| |
| item.addEventListener('click', function(e) { |
| if (!e.target.closest('.delete-favorite')) { |
| var location = new AMap.LngLat(fav.location[0], fav.location[1]); |
| map.setCenter(location); |
| map.setZoom(15); |
| |
| |
| var tempPOI = { |
| id: fav.id, |
| name: fav.name, |
| address: fav.address, |
| location: location, |
| type: fav.type |
| }; |
| |
| showInfoWindow(tempPOI); |
| currentPOI = tempPOI; |
| |
| |
| closeModal('favorites-modal'); |
| } |
| }); |
| |
| |
| var deleteBtn = item.querySelector('.delete-favorite'); |
| deleteBtn.addEventListener('click', function(e) { |
| e.stopPropagation(); |
| var id = this.getAttribute('data-id'); |
| |
| |
| favorites = favorites.filter(function(fav) { |
| return fav.id !== id; |
| }); |
| |
| |
| localStorage.setItem('foodExplorerFavorites', JSON.stringify(favorites)); |
| |
| |
| item.remove(); |
| |
| |
| updateResultListFavoriteStatus(); |
| |
| |
| if (favorites.length === 0) { |
| favoritesList.style.display = 'none'; |
| emptyFavorites.style.display = 'block'; |
| } |
| |
| showToast('已从收藏中移除'); |
| }); |
| |
| favoritesList.appendChild(item); |
| }); |
| } |
| |
| |
| function navigateTo() { |
| if (!currentPOI) return; |
| |
| |
| var url = `https://uri.amap.com/navigation?to=${currentPOI.location.lng},${currentPOI.location.lat},${encodeURIComponent(currentPOI.name)}&mode=car&src=myapp&callnative=1`; |
| |
| |
| window.open(url, '_blank'); |
| } |
| |
| |
| function loadCommunityContent() { |
| var communityContent = document.getElementById('community-content'); |
| |
| |
| var cityPosts = sharedPosts.filter(function(post) { |
| return post.city === currentCity.name; |
| }); |
| |
| if (cityPosts.length === 0) { |
| communityContent.innerHTML = '<div class="no-results">暂无分享内容</div>'; |
| return; |
| } |
| |
| communityContent.innerHTML = ''; |
| |
| |
| cityPosts.sort(function(a, b) { |
| return new Date(b.time) - new Date(a.time); |
| }); |
| |
| cityPosts.forEach(function(post) { |
| var item = document.createElement('div'); |
| item.className = 'shared-food-item'; |
| |
| |
| var stars = ''; |
| for (var i = 0; i < 5; i++) { |
| if (i < post.rating) { |
| stars += '<i class="fas fa-star"></i>'; |
| } else { |
| stars += '<i class="far fa-star"></i>'; |
| } |
| } |
| |
| |
| var postTime = new Date(post.time); |
| var timeString = postTime.toLocaleDateString('zh-CN') + ' ' + postTime.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit'}); |
| |
| item.innerHTML = ` |
| <div class="shared-food-header"> |
| <div class="shared-user-avatar"> |
| <i class="fas fa-user"></i> |
| </div> |
| <div class="shared-user-info"> |
| <div class="shared-user-name">美食爱好者</div> |
| <div class="shared-time">${timeString}</div> |
| </div> |
| </div> |
| <div class="shared-food-content"> |
| <h3>${post.name}</h3> |
| <div class="rating">${stars} · ${post.address}</div> |
| <p>${post.comments}</p> |
| ${post.image ? `<div class="shared-food-image" style="background-image: url(${post.image}); background-size: cover;"></div>` : ''} |
| </div> |
| <div class="shared-food-actions"> |
| <button class="like-btn"><i class="far fa-thumbs-up"></i> 点赞</button> |
| <button class="comment-btn"><i class="far fa-comment"></i> 评论</button> |
| <button class="navigate-btn" data-lng="${post.location[0]}" data-lat="${post.location[1]}" data-name="${post.name}"><i class="fas fa-map-marker-alt"></i> 查看位置</button> |
| </div> |
| `; |
| |
| |
| var navigateBtn = item.querySelector('.navigate-btn'); |
| navigateBtn.addEventListener('click', function() { |
| var lng = parseFloat(this.getAttribute('data-lng')); |
| var lat = parseFloat(this.getAttribute('data-lat')); |
| var name = this.getAttribute('data-name'); |
| |
| var location = new AMap.LngLat(lng, lat); |
| map.setCenter(location); |
| map.setZoom(15); |
| |
| |
| var tempPOI = { |
| id: 'share_' + Date.now(), |
| name: name, |
| address: post.address, |
| location: location, |
| type: '分享的美食' |
| }; |
| |
| showInfoWindow(tempPOI); |
| currentPOI = tempPOI; |
| |
| |
| switchTab('results'); |
| }); |
| |
| |
| var likeBtn = item.querySelector('.like-btn'); |
| likeBtn.addEventListener('click', function() { |
| if (this.classList.contains('active')) { |
| this.classList.remove('active'); |
| this.innerHTML = '<i class="far fa-thumbs-up"></i> 点赞'; |
| } else { |
| this.classList.add('active'); |
| this.innerHTML = '<i class="fas fa-thumbs-up"></i> 已点赞'; |
| showToast('谢谢您的点赞!'); |
| } |
| }); |
| |
| |
| var commentBtn = item.querySelector('.comment-btn'); |
| commentBtn.addEventListener('click', function() { |
| showToast('评论功能即将上线'); |
| }); |
| |
| communityContent.appendChild(item); |
| }); |
| } |
| |
| |
| function shareFood() { |
| if (!currentPOI) { |
| showToast('请先选择一个地点'); |
| return; |
| } |
| |
| var shareRating = document.getElementById('share-rating').value; |
| var shareComments = document.getElementById('share-comments').value; |
| var shareImage = document.getElementById('share-image').files[0]; |
| |
| if (!shareComments.trim()) { |
| showToast('请输入您的评价'); |
| return; |
| } |
| |
| |
| var newPost = { |
| id: 'post_' + Date.now(), |
| name: currentPOI.name, |
| address: currentPOI.address || currentPOI.location.toString(), |
| location: [currentPOI.location.lng, currentPOI.location.lat], |
| rating: parseInt(shareRating), |
| comments: shareComments, |
| city: currentCity.name, |
| time: new Date().toISOString(), |
| image: null |
| }; |
| |
| |
| if (shareImage) { |
| var reader = new FileReader(); |
| reader.onload = function(e) { |
| newPost.image = e.target.result; |
| |
| |
| sharedPosts.push(newPost); |
| localStorage.setItem('foodExplorerSharedPosts', JSON.stringify(sharedPosts)); |
| |
| |
| loadCommunityContent(); |
| |
| |
| switchTab('community'); |
| |
| |
| closeModal('share-modal'); |
| |
| |
| document.getElementById('share-form').reset(); |
| |
| showToast('分享成功!'); |
| }; |
| reader.readAsDataURL(shareImage); |
| } else { |
| |
| sharedPosts.push(newPost); |
| localStorage.setItem('foodExplorerSharedPosts', JSON.stringify(sharedPosts)); |
| |
| |
| loadCommunityContent(); |
| |
| |
| switchTab('community'); |
| |
| |
| closeModal('share-modal'); |
| |
| |
| document.getElementById('share-form').reset(); |
| |
| showToast('分享成功!'); |
| } |
| } |
| |
| |
| function updateSharePreview() { |
| if (!currentPOI) return; |
| |
| var previewContainer = document.getElementById('share-preview'); |
| previewContainer.innerHTML = ` |
| <div class="share-preview-img"> |
| <i class="fas fa-utensils"></i> |
| </div> |
| <div class="share-preview-info"> |
| <h3>${currentPOI.name}</h3> |
| <p>${currentPOI.address || currentPOI.location.toString()}</p> |
| </div> |
| `; |
| } |
| |
| |
| function openModal(id) { |
| document.getElementById(id).classList.add('active'); |
| document.body.style.overflow = 'hidden'; |
| } |
| |
| |
| function closeModal(id) { |
| document.getElementById(id).classList.remove('active'); |
| document.body.style.overflow = ''; |
| } |
| |
| |
| function switchTab(tabId) { |
| document.querySelectorAll('.tab').forEach(function(tab) { |
| tab.classList.remove('active'); |
| }); |
| document.querySelectorAll('.tab-content').forEach(function(content) { |
| content.classList.remove('active'); |
| }); |
| |
| document.querySelector(`.tab[data-tab="${tabId}"]`).classList.add('active'); |
| document.getElementById(`${tabId}-content`).classList.add('active'); |
| } |
| |
| |
| function showToast(message) { |
| var toast = document.getElementById('toast'); |
| toast.textContent = message; |
| toast.classList.add('show'); |
| |
| setTimeout(function() { |
| toast.classList.remove('show'); |
| }, 2000); |
| } |
| |
| |
| function showFabMenu() { |
| document.getElementById('fab-menu').classList.add('active'); |
| } |
| |
| |
| function hideFabMenu() { |
| document.getElementById('fab-menu').classList.remove('active'); |
| } |
| |
| |
| function markSearchCenter(position) { |
| |
| map.remove(markers.filter(marker => marker.getExtData && marker.getExtData().type === 'searchCenter')); |
| |
| |
| var centerMarker = new AMap.Marker({ |
| position: position, |
| icon: new AMap.Icon({ |
| |
| image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png', |
| size: new AMap.Size(25, 34), |
| imageSize: new AMap.Size(25, 34) |
| }), |
| offset: new AMap.Pixel(-12, -34), |
| zIndex: 100, |
| title: '搜索中心' |
| }); |
| |
| |
| centerMarker.setExtData({ |
| type: 'searchCenter' |
| }); |
| |
| map.add(centerMarker); |
| markers.push(centerMarker); |
| } |
| |
| |
| |
| |
| function initAIAssistant() { |
| |
| addMessage("你好!我是你的AI美食助手。想知道附近有什么好吃的,或者需要美食规划吗?", "ai"); |
| |
| |
| initSuggestions(); |
| } |
| |
| |
| function createCollapsibleSuggestions() { |
| |
| const suggestionsContainer = document.getElementById('suggestions'); |
| if (!suggestionsContainer) return; |
| |
| |
| suggestionsContainer.innerHTML = ''; |
| |
| |
| const titleBar = document.createElement('div'); |
| titleBar.className = 'suggestions-title'; |
| titleBar.innerHTML = ` |
| <span>常见问题</span> |
| <i class="fas fa-chevron-down"></i> |
| `; |
| suggestionsContainer.appendChild(titleBar); |
| |
| |
| const suggestionsContent = document.createElement('div'); |
| suggestionsContent.className = 'suggestions-content'; |
| suggestionsContent.style.display = 'none'; |
| suggestionsContainer.appendChild(suggestionsContent); |
| |
| |
| const suggestions = [ |
| "附近有什么好吃的火锅店?", |
| "推荐几家距离我500米内的咖啡厅", |
| "帮我规划一个3小时的美食路线", |
| "有什么适合带孩子去的餐厅?" |
| ]; |
| |
| suggestions.forEach(text => { |
| const chip = document.createElement('div'); |
| chip.className = 'suggestion-chip'; |
| chip.textContent = text; |
| chip.addEventListener('click', function() { |
| document.getElementById('user-input').value = this.textContent; |
| sendUserMessage(); |
| }); |
| suggestionsContent.appendChild(chip); |
| }); |
| |
| |
| titleBar.addEventListener('click', function() { |
| const isExpanded = suggestionsContent.style.display !== 'none'; |
| if (isExpanded) { |
| suggestionsContent.style.display = 'none'; |
| titleBar.querySelector('i').className = 'fas fa-chevron-down'; |
| } else { |
| suggestionsContent.style.display = 'flex'; |
| titleBar.querySelector('i').className = 'fas fa-chevron-up'; |
| } |
| }); |
| |
| |
| const styleElement = document.createElement('style'); |
| styleElement.textContent = ` |
| .suggestions-title { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 10px 15px; |
| background-color: var(--ios-bg); |
| border-radius: 10px; |
| margin-bottom: 5px; |
| cursor: pointer; |
| font-size: 14px; |
| color: var(--ios-primary); |
| font-weight: 500; |
| } |
| |
| .suggestions-content { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| padding: 5px 10px 10px 10px; |
| overflow: hidden; |
| transition: max-height 0.3s ease; |
| } |
| |
| .chat-suggestions { |
| border-top: 1px solid var(--ios-border); |
| padding: 10px; |
| background: var(--ios-card-bg); |
| } |
| `; |
| document.head.appendChild(styleElement); |
| } |
| |
| |
| function initSuggestions() { |
| createCollapsibleSuggestions(); |
| } |
| |
| function toggleAssistant(show) { |
| const assistant = document.getElementById('ai-assistant'); |
| if (show) { |
| assistant.classList.add('active'); |
| } else { |
| assistant.classList.toggle('active'); |
| } |
| } |
| |
| |
| function addMessage(message, sender, extraContent = null) { |
| const messagesDiv = document.getElementById('chat-messages'); |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `message ${sender}-message`; |
| |
| |
| messageDiv.innerHTML = `<p>${message}</p>`; |
| |
| |
| if (extraContent) { |
| if (extraContent.type === 'poi') { |
| const poiInfo = document.createElement('div'); |
| poiInfo.className = 'map-poi-info'; |
| poiInfo.innerHTML = ` |
| <h4>${extraContent.name}</h4> |
| <p>${extraContent.address}</p> |
| <div class="poi-actions"> |
| <button class="poi-action-btn view" data-lng="${extraContent.location[0]}" data-lat="${extraContent.location[1]}"> |
| <i class="fas fa-map-marker-alt"></i> 查看位置 |
| </button> |
| <button class="poi-action-btn navigate" data-lng="${extraContent.location[0]}" data-lat="${extraContent.location[1]}"> |
| <i class="fas fa-directions"></i> 导航 |
| </button> |
| </div> |
| `; |
| messageDiv.appendChild(poiInfo); |
| |
| |
| setTimeout(() => { |
| poiInfo.querySelectorAll('.poi-action-btn').forEach(btn => { |
| btn.addEventListener('click', function() { |
| const action = this.classList.contains('view') ? 'view' : 'navigate'; |
| const lng = this.getAttribute('data-lng'); |
| const lat = this.getAttribute('data-lat'); |
| |
| if (action === 'view') { |
| map.setCenter([lng, lat]); |
| map.setZoom(15); |
| } else if (action === 'navigate') { |
| |
| const url = `https://uri.amap.com/navigation?to=${lng},${lat},${encodeURIComponent(extraContent.name)}&mode=car&src=myapp&callnative=1`; |
| window.open(url, '_blank'); |
| } |
| }); |
| }); |
| }, 0); |
| } else if (extraContent.type === 'route') { |
| |
| showRouteGuidance(extraContent.stops); |
| } |
| } |
| |
| messagesDiv.appendChild(messageDiv); |
| messagesDiv.scrollTop = messagesDiv.scrollHeight; |
| } |
| |
| |
| async function sendUserMessage() { |
| const userInput = document.getElementById('user-input'); |
| const message = userInput.value.trim(); |
| |
| if (!message) return; |
| |
| |
| addMessage(message, 'user'); |
| |
| |
| userInput.value = ''; |
| |
| |
| const thinkingDiv = document.createElement('div'); |
| thinkingDiv.className = 'ai-thinking'; |
| thinkingDiv.innerHTML = ` |
| AI思考中 |
| <div class="thinking-dots"> |
| <span></span> |
| <span></span> |
| <span></span> |
| </div> |
| `; |
| document.getElementById('chat-messages').appendChild(thinkingDiv); |
| |
| try { |
| |
| AI_ASSISTANT.conversationHistory.push({ |
| role: "user", |
| content: message |
| }); |
| |
| |
| const response = await callOpenAI(); |
| |
| |
| thinkingDiv.remove(); |
| |
| |
| if (response.choices && response.choices.length > 0) { |
| const choice = response.choices[0]; |
| |
| |
| if (choice.message.tool_calls && choice.message.tool_calls.length > 0) { |
| |
| AI_ASSISTANT.conversationHistory.push(choice.message); |
| |
| |
| const processingDiv = document.createElement('div'); |
| processingDiv.className = 'ai-thinking'; |
| processingDiv.innerHTML = ` |
| 正在处理您的请求... |
| <div class="thinking-dots"> |
| <span></span> |
| <span></span> |
| <span></span> |
| </div> |
| `; |
| document.getElementById('chat-messages').appendChild(processingDiv); |
| |
| |
| const toolResults = await handleToolCalls(choice.message.tool_calls); |
| |
| |
| processingDiv.remove(); |
| |
| |
| toolResults.forEach(result => { |
| AI_ASSISTANT.conversationHistory.push(result); |
| }); |
| |
| |
| const finalResponse = await callOpenAI(); |
| |
| if (finalResponse.choices && finalResponse.choices.length > 0) { |
| const aiMessage = finalResponse.choices[0].message.content; |
| addMessage(aiMessage, "ai"); |
| |
| |
| AI_ASSISTANT.conversationHistory.push({ |
| role: "assistant", |
| content: aiMessage |
| }); |
| } |
| } else if (choice.message.content) { |
| |
| const aiMessage = choice.message.content; |
| addMessage(aiMessage, "ai"); |
| |
| |
| AI_ASSISTANT.conversationHistory.push({ |
| role: "assistant", |
| content: aiMessage |
| }); |
| } |
| } |
| } catch (error) { |
| console.error("Error processing message:", error); |
| thinkingDiv.remove(); |
| addMessage("抱歉,处理您的请求时出现了问题。请稍后再试。", "ai"); |
| } |
| } |
| |
| |
| async function callOpenAI() { |
| |
| const center = map.getCenter(); |
| const locationStr = AI_ASSISTANT.currentLocation ? |
| `${AI_ASSISTANT.currentLocation[0]},${AI_ASSISTANT.currentLocation[1]}` : |
| `${center.lng},${center.lat}`; |
| |
| |
| const requestBody = { |
| model: "deepseek-chat", |
| messages: [ |
| { |
| role: "system", |
| content: `你是一个专业的美食AI助手,专注于帮助用户发现和规划美食体验。 |
| 当前城市是${currentCity.name}。 |
| 当前用户位置坐标是${locationStr}。 |
| 请以友好、专业的方式回答用户的问题,并根据需要使用工具函数。` |
| }, |
| ...AI_ASSISTANT.conversationHistory |
| ], |
| tools: [ |
| { |
| type: "function", |
| function: { |
| name: "searchNearbyFood", |
| description: "搜索用户附近的美食餐厅", |
| parameters: { |
| type: "object", |
| properties: { |
| location: { |
| type: "string", |
| description: "用户当前位置坐标,格式为'经度,纬度'" |
| }, |
| keyword: { |
| type: "string", |
| description: "搜索关键词,如'火锅'、'日料'等" |
| }, |
| radius: { |
| type: "number", |
| description: "搜索半径,单位为米,默认为3000米" |
| }, |
| type: { |
| type: "string", |
| description: "餐厅类型,如'快餐'、'中餐'、'西餐'等" |
| } |
| }, |
| required: ["location"] |
| } |
| } |
| }, |
| { |
| type: "function", |
| function: { |
| name: "getFoodRecommendations", |
| description: "基于用户偏好、饮食限制、价格范围和用餐时间推荐美食", |
| parameters: { |
| type: "object", |
| properties: { |
| location: { |
| type: "string", |
| description: "用户当前位置坐标,格式为'经度,纬度'" |
| }, |
| preferences: { |
| type: "array", |
| items: { |
| type: "string" |
| }, |
| description: "用户的食物偏好,如['辣', '海鲜', '甜点']" |
| }, |
| dietary_restrictions: { |
| type: "array", |
| items: { |
| type: "string" |
| }, |
| description: "用户的饮食限制,如['素食', '无麸质', '无坚果']" |
| }, |
| price_range: { |
| type: "string", |
| description: "价格范围,'低价'、'中价'或'高价'" |
| }, |
| meal_time: { |
| type: "string", |
| description: "用餐时间,如'早餐'、'午餐'、'晚餐'、'夜宵'" |
| } |
| }, |
| required: ["location"] |
| } |
| } |
| }, |
| { |
| type: "function", |
| function: { |
| name: "planFoodRoute", |
| description: "规划一条美食探索路线", |
| parameters: { |
| type: "object", |
| properties: { |
| start_location: { |
| type: "string", |
| description: "起始位置坐标,格式为'经度,纬度'" |
| }, |
| food_preferences: { |
| type: "array", |
| items: { |
| type: "string" |
| }, |
| description: "食物偏好,如['小吃', '甜点', '咖啡']" |
| }, |
| duration: { |
| type: "number", |
| description: "计划的持续时间,单位为小时" |
| }, |
| transport_mode: { |
| type: "string", |
| description: "交通方式,'walking'、'driving'或'transit'" |
| } |
| }, |
| required: ["start_location"] |
| } |
| } |
| } |
| ], |
| tool_choice: "auto" |
| }; |
| |
| |
| const response = await fetch('https://api.deepseek.com/v1/chat/completions', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': `Bearer sk-2e95d4c1be9d4b2aa285639885a82e4e` |
| }, |
| body: JSON.stringify(requestBody) |
| }); |
| |
| |
| if (!response.ok) { |
| const errorData = await response.json(); |
| throw new Error(`API error: ${JSON.stringify(errorData)}`); |
| } |
| |
| return await response.json(); |
| } |
| |
| |
| async function handleToolCalls(toolCalls) { |
| const results = []; |
| |
| for (const call of toolCalls) { |
| if (call.type === 'function') { |
| const functionName = call.function.name; |
| const args = JSON.parse(call.function.arguments); |
| |
| console.log(`执行函数: ${functionName},参数:`, args); |
| |
| let result; |
| try { |
| |
| switch (functionName) { |
| case 'searchNearbyFood': |
| result = await realSearchNearbyFood( |
| args.location || `${AI_ASSISTANT.currentLocation[0]},${AI_ASSISTANT.currentLocation[1]}`, |
| args.keyword, |
| args.radius || 3000, |
| args.type |
| ); |
| break; |
| case 'getFoodRecommendations': |
| result = await realGetFoodRecommendations( |
| args.location || `${AI_ASSISTANT.currentLocation[0]},${AI_ASSISTANT.currentLocation[1]}`, |
| args.preferences, |
| args.dietary_restrictions, |
| args.price_range, |
| args.meal_time |
| ); |
| break; |
| case 'planFoodRoute': |
| result = await realPlanFoodRoute( |
| args.start_location || `${AI_ASSISTANT.currentLocation[0]},${AI_ASSISTANT.currentLocation[1]}`, |
| args.food_preferences, |
| args.duration || 3, |
| args.transport_mode || 'walking' |
| ); |
| break; |
| default: |
| result = { error: "未知函数" }; |
| } |
| |
| console.log(`函数执行结果:`, result); |
| |
| |
| results.push({ |
| tool_call_id: call.id, |
| role: "tool", |
| |
| content: JSON.stringify(result) |
| }); |
| } catch (error) { |
| console.error(`执行函数${functionName}时出错:`, error); |
| result = { |
| status: 'error', |
| message: `执行函数时出错: ${error.message}`, |
| error: error.message |
| }; |
| |
| |
| results.push({ |
| tool_call_id: call.id, |
| role: "tool", |
| content: JSON.stringify(result) |
| }); |
| } |
| } |
| } |
| |
| |
| return results; |
| } |
| |
| |
| async function realSearchNearbyFood(location, keyword = '美食', radius = 3000, type = null) { |
| return new Promise((resolve, reject) => { |
| try { |
| |
| document.querySelector('.loading').style.display = 'block'; |
| |
| |
| currentKeyword = keyword || '美食'; |
| document.getElementById('keyword').value = currentKeyword; |
| |
| |
| document.querySelectorAll('.category').forEach(function(cat) { |
| cat.classList.remove('active'); |
| if (cat.dataset.keyword === currentKeyword) { |
| cat.classList.add('active'); |
| } |
| }); |
| |
| |
| const [lng, lat] = location.split(',').map(parseFloat); |
| const point = new AMap.LngLat(lng, lat); |
| |
| |
| const placeSearch = new AMap.PlaceSearch({ |
| pageSize: 10, |
| pageIndex: 1, |
| extensions: 'all' |
| }); |
| |
| |
| placeSearch.setCity(currentCity.adcode); |
| |
| |
| let searchKeyword = keyword || '美食'; |
| if (type) { |
| searchKeyword = `${searchKeyword} ${type}`; |
| } |
| |
| |
| markSearchCenter(point); |
| |
| |
| placeSearch.searchNearBy(searchKeyword, point, radius, function(status, result) { |
| |
| document.querySelector('.loading').style.display = 'none'; |
| |
| if (status === 'complete' && result.info === 'OK') { |
| |
| const restaurants = result.poiList.pois.map(poi => ({ |
| id: poi.id, |
| name: poi.name, |
| address: poi.address || '地址未知', |
| location: [poi.location.lng, poi.location.lat], |
| type: poi.type || '餐饮', |
| distance: poi.distance, |
| tel: poi.tel || '电话未知' |
| })); |
| |
| |
| addMarkersToMap(result.poiList.pois); |
| |
| |
| customizeResultList(result.poiList.pois); |
| |
| resolve({ |
| status: 'success', |
| count: restaurants.length, |
| restaurants: restaurants |
| }); |
| } else { |
| resolve({ |
| status: 'error', |
| message: '没有找到符合条件的餐厅', |
| restaurants: [] |
| }); |
| } |
| }); |
| } catch (error) { |
| console.error("Error in realSearchNearbyFood:", error); |
| document.querySelector('.loading').style.display = 'none'; |
| resolve({ |
| status: 'error', |
| message: '搜索过程中发生错误', |
| restaurants: [] |
| }); |
| } |
| }); |
| } |
| |
| |
| async function realGetFoodRecommendations(location, preferences = [], dietary_restrictions = [], price_range = null, meal_time = null) { |
| |
| const searchResult = await realSearchNearbyFood(location, '美食', 5000); |
| |
| if (searchResult.status === 'error' || searchResult.restaurants.length === 0) { |
| return { |
| status: 'error', |
| message: '没有找到适合推荐的餐厅', |
| recommendations: [] |
| }; |
| } |
| |
| |
| let restaurants = searchResult.restaurants; |
| |
| |
| restaurants = restaurants.map(restaurant => { |
| |
| const cuisineTypes = ['中餐', '西餐', '日料', '韩餐', '火锅', '烧烤', '小吃', '甜点', '咖啡', '素食']; |
| const priceLevel = ['低价', '中价', '高价']; |
| |
| |
| const tags = []; |
| const tagCount = Math.floor(Math.random() * 3) + 1; |
| for (let i = 0; i < tagCount; i++) { |
| const tag = cuisineTypes[Math.floor(Math.random() * cuisineTypes.length)]; |
| if (!tags.includes(tag)) tags.push(tag); |
| } |
| |
| |
| const price = priceLevel[Math.floor(Math.random() * priceLevel.length)]; |
| |
| |
| let score = 0; |
| |
| |
| if (preferences && preferences.length > 0) { |
| preferences.forEach(pref => { |
| if (tags.some(tag => tag.includes(pref) || pref.includes(tag))) { |
| score += 2; |
| } |
| }); |
| } |
| |
| |
| if (dietary_restrictions && dietary_restrictions.length > 0) { |
| dietary_restrictions.forEach(restriction => { |
| if (tags.some(tag => tag.includes(restriction) && !tag.includes('无') && !tag.includes('不含'))) { |
| score -= 3; |
| } |
| }); |
| } |
| |
| |
| if (price_range && price === price_range) { |
| score += 1; |
| } |
| |
| return { |
| ...restaurant, |
| tags, |
| price, |
| score |
| }; |
| }); |
| |
| |
| const recommendations = restaurants |
| .filter(r => r.score >= 0) |
| .sort((a, b) => b.score - a.score) |
| .slice(0, 5); |
| |
| return { |
| status: 'success', |
| count: recommendations.length, |
| recommendations |
| }; |
| } |
| |
| |
| async function realPlanFoodRoute(start_location, food_preferences = [], duration = 3, transport_mode = 'walking') { |
| try { |
| |
| const radius = transport_mode === 'walking' ? 2000 : 5000; |
| |
| |
| const stopCount = Math.min(Math.max(Math.floor(duration * 1.5), 2), 5); |
| |
| |
| const allStops = []; |
| |
| |
| const preferences = food_preferences && food_preferences.length > 0 ? |
| food_preferences : ['美食', '小吃', '甜点']; |
| |
| |
| for (const pref of preferences) { |
| const result = await realSearchNearbyFood(start_location, pref, radius); |
| if (result.status === 'success' && result.restaurants.length > 0) { |
| allStops.push(...result.restaurants); |
| } |
| } |
| |
| |
| if (allStops.length === 0) { |
| return { |
| status: 'error', |
| message: '没有找到符合条件的美食地点', |
| route: null |
| }; |
| } |
| |
| |
| const uniqueStops = []; |
| const seenIds = new Set(); |
| |
| allStops.forEach(stop => { |
| if (!seenIds.has(stop.id)) { |
| seenIds.add(stop.id); |
| uniqueStops.push(stop); |
| } |
| }); |
| |
| |
| const [startLng, startLat] = start_location.split(',').map(parseFloat); |
| const startPoint = new AMap.LngLat(startLng, startLat); |
| |
| uniqueStops.sort((a, b) => { |
| const aPoint = new AMap.LngLat(a.location[0], a.location[1]); |
| const bPoint = new AMap.LngLat(b.location[0], b.location[1]); |
| return startPoint.distance(aPoint) - startPoint.distance(bPoint); |
| }); |
| |
| |
| const route = []; |
| |
| |
| route.push({ |
| id: 'start', |
| name: '起点', |
| address: '当前位置', |
| location: [startLng, startLat], |
| isStart: true |
| }); |
| |
| |
| const selectedTypes = new Set(); |
| let remainingStops = [...uniqueStops]; |
| |
| |
| while (route.length < stopCount + 1 && remainingStops.length > 0) { |
| let selectedIndex = -1; |
| |
| |
| for (let i = 0; i < remainingStops.length; i++) { |
| const stop = remainingStops[i]; |
| if (!selectedTypes.has(stop.type)) { |
| selectedIndex = i; |
| break; |
| } |
| } |
| |
| |
| if (selectedIndex === -1) { |
| selectedIndex = 0; |
| } |
| |
| const selectedStop = remainingStops[selectedIndex]; |
| route.push(selectedStop); |
| selectedTypes.add(selectedStop.type); |
| |
| |
| remainingStops.splice(selectedIndex, 1); |
| |
| |
| const lastPoint = new AMap.LngLat(selectedStop.location[0], selectedStop.location[1]); |
| remainingStops.sort((a, b) => { |
| const aPoint = new AMap.LngLat(a.location[0], a.location[1]); |
| const bPoint = new AMap.LngLat(b.location[0], b.location[1]); |
| return lastPoint.distance(aPoint) - lastPoint.distance(bPoint); |
| }); |
| } |
| |
| |
| let totalDistance = 0; |
| for (let i = 0; i < route.length - 1; i++) { |
| const pointA = new AMap.LngLat(route[i].location[0], route[i].location[1]); |
| const pointB = new AMap.LngLat(route[i+1].location[0], route[i+1].location[1]); |
| totalDistance += pointA.distance(pointB); |
| } |
| |
| |
| let speed; |
| if (transport_mode === 'walking') { |
| speed = 1.2; |
| } else if (transport_mode === 'driving') { |
| speed = 8.3; |
| } else { |
| speed = 4.2; |
| } |
| |
| const travelTimeMinutes = Math.round(totalDistance / 1000 / speed * 60); |
| |
| |
| drawRouteOnMap(route); |
| |
| |
| showRouteGuidance(route); |
| |
| return { |
| status: 'success', |
| route_info: { |
| stop_count: route.length, |
| total_distance: Math.round(totalDistance), |
| travel_time_minutes: travelTimeMinutes, |
| transport_mode: transport_mode |
| }, |
| stops: route |
| }; |
| } catch (error) { |
| console.error("Error in realPlanFoodRoute:", error); |
| return { |
| status: 'error', |
| message: '规划路线时发生错误', |
| route: null |
| }; |
| } |
| } |
| |
| |
| function drawRouteOnMap(stops) { |
| if (!stops || stops.length < 2) return; |
| |
| |
| map.clearInfoWindow(); |
| |
| |
| const path = stops.map(stop => new AMap.LngLat(stop.location[0], stop.location[1])); |
| |
| |
| const polyline = new AMap.Polyline({ |
| path: path, |
| strokeColor: '#007aff', |
| strokeWeight: 6, |
| strokeOpacity: 0.8, |
| strokeStyle: 'solid', |
| strokeDasharray: [10, 5], |
| lineJoin: 'round' |
| }); |
| |
| map.add(polyline); |
| |
| |
| const routeMarkers = []; |
| |
| stops.forEach((stop, index) => { |
| let icon; |
| if (index === 0) { |
| icon = "https://webapi.amap.com/theme/v1.3/markers/n/start.png"; |
| } else if (index === stops.length - 1) { |
| icon = "https://webapi.amap.com/theme/v1.3/markers/n/end.png"; |
| } else { |
| icon = "https://webapi.amap.com/theme/v1.3/markers/n/mid.png"; |
| } |
| |
| const marker = new AMap.Marker({ |
| map: map, |
| position: new AMap.LngLat(stop.location[0], stop.location[1]), |
| icon: icon, |
| label: { |
| content: `<div style="padding: 5px; background: white; border-radius: 5px;">${index + 1}. ${stop.name}</div>`, |
| direction: 'top' |
| } |
| }); |
| |
| routeMarkers.push(marker); |
| }); |
| |
| markers = [...markers, ...routeMarkers]; |
| |
| |
| map.setFitView(); |
| } |
| |
| |
| function showRouteGuidance(stops) { |
| const routeGuidance = document.getElementById('route-guidance'); |
| const routeStops = document.getElementById('route-stops'); |
| |
| |
| routeStops.innerHTML = ''; |
| |
| |
| stops.forEach((stop, index) => { |
| const stopItem = document.createElement('div'); |
| stopItem.className = 'route-stop'; |
| |
| stopItem.innerHTML = ` |
| <div class="stop-number">${index + 1}</div> |
| <div class="stop-info"> |
| <div class="stop-name">${stop.name}</div> |
| <div class="stop-address">${stop.address}</div> |
| </div> |
| `; |
| |
| routeStops.appendChild(stopItem); |
| }); |
| |
| |
| routeGuidance.classList.add('active'); |
| |
| |
| document.getElementById('start-navigation').addEventListener('click', function() { |
| |
| const start = stops[0].location.join(','); |
| const end = stops[stops.length - 1].location.join(','); |
| |
| |
| const waypoints = stops.slice(1, -1).map(stop => stop.location.join(',')).join(';'); |
| |
| |
| let navUrl = `https://uri.amap.com/navigation?from=${start},起点&to=${end},终点`; |
| |
| if (waypoints) { |
| navUrl += `&waypoints=${waypoints}`; |
| } |
| |
| navUrl += '&mode=walk&policy=1&src=myapp&callnative=1'; |
| |
| |
| window.open(navUrl, '_blank'); |
| }); |
| |
| |
| document.getElementById('modify-route').addEventListener('click', function() { |
| |
| toggleAssistant(true); |
| |
| |
| document.getElementById('user-input').value = '重新规划美食路线,增加更多甜品店'; |
| |
| |
| document.getElementById('route-guidance').classList.remove('active'); |
| }); |
| } |
| |
| |
| document.getElementById('search-btn').addEventListener('click', function() { |
| var keyword = document.getElementById('keyword').value.trim(); |
| if (keyword) { |
| currentKeyword = keyword; |
| searchPOI(); |
| |
| |
| document.querySelectorAll('.category').forEach(function(cat) { |
| cat.classList.remove('active'); |
| if (cat.dataset.keyword === keyword) { |
| cat.classList.add('active'); |
| } |
| }); |
| } |
| }); |
| |
| |
| document.getElementById('keyword').addEventListener('keyup', function(e) { |
| if (e.key === 'Enter') { |
| document.getElementById('search-btn').click(); |
| } |
| }); |
| |
| |
| document.querySelectorAll('.category').forEach(function(category) { |
| category.addEventListener('click', function() { |
| document.querySelectorAll('.category').forEach(function(cat) { |
| cat.classList.remove('active'); |
| }); |
| this.classList.add('active'); |
| |
| currentKeyword = this.dataset.keyword; |
| document.getElementById('keyword').value = currentKeyword; |
| searchPOI(); |
| }); |
| }); |
| |
| |
| document.getElementById('current-city').addEventListener('click', function() { |
| openModal('city-modal'); |
| }); |
| |
| |
| document.querySelectorAll('.city-item').forEach(function(cityItem) { |
| cityItem.addEventListener('click', function() { |
| var cityName = this.getAttribute('data-city'); |
| var cityAdcode = this.getAttribute('data-adcode'); |
| |
| currentCity = { |
| name: cityName, |
| adcode: cityAdcode |
| }; |
| |
| updateCityText(); |
| |
| |
| var geocoder = new AMap.Geocoder(); |
| geocoder.getLocation(cityName, function(status, result) { |
| if (status === 'complete' && result.info === 'OK') { |
| |
| var location = result.geocodes[0].location; |
| |
| |
| map.setCenter(location); |
| |
| |
| map.setZoom(11); |
| |
| |
| clearMarkers(); |
| |
| |
| searchPOI(); |
| |
| |
| loadCommunityContent(); |
| } else { |
| |
| searchPOI(); |
| loadCommunityContent(); |
| } |
| }); |
| |
| closeModal('city-modal'); |
| }); |
| }); |
| |
| |
| document.getElementById('favorites-btn').addEventListener('click', function() { |
| showFavorites(); |
| openModal('favorites-modal'); |
| }); |
| |
| |
| document.getElementById('fab-share').addEventListener('click', function() { |
| updateSharePreview(); |
| openModal('share-modal'); |
| hideFabMenu(); |
| }); |
| |
| |
| document.getElementById('fab-favorite').addEventListener('click', function() { |
| if (!currentPOI) return; |
| |
| |
| var isFavorite = favorites.some(function(fav) { |
| return fav.id === currentPOI.id; |
| }); |
| |
| if (isFavorite) { |
| |
| favorites = favorites.filter(function(fav) { |
| return fav.id !== currentPOI.id; |
| }); |
| showToast('已取消收藏'); |
| } else { |
| |
| favorites.push({ |
| id: currentPOI.id, |
| name: currentPOI.name, |
| address: currentPOI.address || currentPOI.location.toString(), |
| location: [currentPOI.location.lng, currentPOI.location.lat], |
| type: currentPOI.type || '特色美食' |
| }); |
| showToast('已添加到收藏'); |
| } |
| |
| |
| localStorage.setItem('foodExplorerFavorites', JSON.stringify(favorites)); |
| |
| |
| if (infoWindow.getIsOpen()) { |
| var favoriteBtn = document.querySelector('.info-window-favorite'); |
| if (isFavorite) { |
| favoriteBtn.classList.remove('active'); |
| } else { |
| favoriteBtn.classList.add('active'); |
| } |
| } |
| |
| |
| updateResultListFavoriteStatus(); |
| |
| hideFabMenu(); |
| }); |
| |
| |
| document.getElementById('fab-route').addEventListener('click', function() { |
| if (!currentPOI) return; |
| |
| |
| toggleAssistant(true); |
| |
| |
| document.getElementById('user-input').value = `以${currentPOI.name}为起点,帮我规划一个美食路线`; |
| |
| hideFabMenu(); |
| }); |
| |
| |
| document.getElementById('share-form').addEventListener('submit', function(e) { |
| e.preventDefault(); |
| shareFood(); |
| }); |
| |
| |
| document.querySelectorAll('.modal-close').forEach(function(closeBtn) { |
| closeBtn.addEventListener('click', function() { |
| var modal = this.closest('.modal'); |
| closeModal(modal.id); |
| }); |
| }); |
| |
| |
| document.querySelectorAll('.tab').forEach(function(tab) { |
| tab.addEventListener('click', function() { |
| var tabId = this.getAttribute('data-tab'); |
| switchTab(tabId); |
| }); |
| }); |
| |
| |
| document.querySelectorAll('.modal').forEach(function(modal) { |
| modal.addEventListener('click', function(e) { |
| if (e.target === this) { |
| closeModal(this.id); |
| } |
| }); |
| }); |
| |
| |
| document.getElementById('panel').addEventListener('scroll', function() { |
| var panel = this; |
| |
| if (panel.scrollHeight - panel.scrollTop - panel.clientHeight < 100) { |
| searchMorePOI(); |
| } |
| }); |
| |
| |
| document.getElementById('close-route').addEventListener('click', function() { |
| document.getElementById('route-guidance').classList.remove('active'); |
| }); |
| |
| |
| window.onload = function() { |
| initMap(); |
| initAIAssistant(); |
| |
| |
| document.getElementById('open-assistant').addEventListener('click', function() { |
| toggleAssistant(); |
| }); |
| |
| document.getElementById('close-chat').addEventListener('click', function() { |
| toggleAssistant(); |
| }); |
| |
| document.getElementById('send-message').addEventListener('click', function() { |
| sendUserMessage(); |
| }); |
| |
| document.getElementById('user-input').addEventListener('keypress', function(e) { |
| if (e.key === 'Enter') { |
| sendUserMessage(); |
| } |
| }); |
| }; |
| |
| |
| window.toggleFavorite = function(event) { |
| toggleFavorite(event); |
| }; |
| |
| |
| window.navigateTo = function() { |
| navigateTo(); |
| }; |
| |
| |
| window.openShareModal = function() { |
| updateSharePreview(); |
| openModal('share-modal'); |
| }; |
| </script> |
|
|
| </body> |
| </html> |