Implement mobile UI enhancements including a sidebar, mobile search functionality, and a floating results button. Add port checking and killing logic in main.go. Update HTML and CSS for mobile responsiveness and new UI elements.

This commit is contained in:
Ethanfly 2026-01-12 12:13:58 +08:00
parent d082c97823
commit a85fabc2d8
5 changed files with 1348 additions and 71 deletions

64
main.go
View File

@ -9,10 +9,14 @@ import (
"io/fs" "io/fs"
"log" "log"
"math" "math"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"os/exec"
"runtime"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -120,10 +124,65 @@ type AmapTipsResponse struct {
var config Config var config Config
// checkAndKillPort 检查端口是否被占用,如果被占用则尝试杀死占用进程
func checkAndKillPort(port string) {
addr := ":" + port
ln, err := net.Listen("tcp", addr)
if err == nil {
// 端口未被占用,关闭监听
ln.Close()
return
}
log.Printf("端口 %s 已被占用,正在尝试释放...", port)
// 根据操作系统执行不同的命令
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
// Windows: 查找并杀死占用端口的进程
// 先查找 PID
findCmd := exec.Command("cmd", "/c", fmt.Sprintf("netstat -ano | findstr :%s | findstr LISTENING", port))
output, err := findCmd.Output()
if err != nil {
log.Printf("查找端口占用进程失败: %v", err)
return
}
// 解析 PID
lines := strings.Split(string(output), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) >= 5 {
pid := fields[len(fields)-1]
if pid != "" && pid != "0" {
log.Printf("发现占用进程 PID: %s正在终止...", pid)
killCmd := exec.Command("taskkill", "/F", "/PID", pid)
killCmd.Run()
}
}
}
} else {
// Linux/Mac: 使用 lsof 和 kill
cmd = exec.Command("sh", "-c", fmt.Sprintf("lsof -ti:%s | xargs -r kill -9", port))
cmd.Run()
}
// 等待端口释放
time.Sleep(500 * time.Millisecond)
log.Printf("端口 %s 已释放", port)
}
func main() { func main() {
// 加载配置 // 加载配置
loadConfig() loadConfig()
// 检查并释放端口
port := config.Port
if port == "" {
port = "8080"
}
checkAndKillPort(port)
// 设置Gin模式 // 设置Gin模式
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
@ -145,11 +204,6 @@ func main() {
r.POST("/api/search", searchPOIHandler) r.POST("/api/search", searchPOIHandler)
r.GET("/api/tips", getTipsHandler) r.GET("/api/tips", getTipsHandler)
port := config.Port
if port == "" {
port = "8080"
}
log.Printf("========================================") log.Printf("========================================")
log.Printf(" 会面点 Meeting Point") log.Printf(" 会面点 Meeting Point")
log.Printf(" 服务启动在 http://localhost:%s", port) log.Printf(" 服务启动在 http://localhost:%s", port)

BIN
meeting-point.exe Normal file

Binary file not shown.

View File

@ -738,6 +738,276 @@ ul {
height: 100%; height: 100%;
} }
/* 移动端搜索框 */
.mobile-search-bar {
display: none;
position: absolute;
top: var(--spacing-md);
left: var(--spacing-md);
right: var(--spacing-md);
z-index: 50;
}
.mobile-search-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.mobile-search-bar input {
width: 100%;
padding: 12px 40px 12px 16px;
background: rgba(26, 26, 46, 0.95);
backdrop-filter: blur(10px);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
color: var(--text-primary);
font-size: 0.9rem;
box-shadow: var(--shadow-md);
}
.mobile-search-bar input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-glow);
}
.mobile-search-bar input::placeholder {
color: var(--text-muted);
}
.mobile-search-clear {
position: absolute;
right: 8px;
width: 28px;
height: 28px;
background: var(--bg-card-hover);
border: none;
border-radius: 50%;
color: var(--text-secondary);
font-size: 1.1rem;
cursor: pointer;
}
.mobile-search-tips {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background: rgba(26, 26, 46, 0.98);
backdrop-filter: blur(10px);
border: 1px solid var(--border);
border-radius: var(--radius-md);
max-height: 250px;
overflow-y: auto;
display: none;
box-shadow: var(--shadow-lg);
}
.mobile-search-tips.active {
display: block;
}
.mobile-search-tip-item {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
}
.mobile-search-tip-item:last-child {
border-bottom: none;
}
.mobile-search-tip-item:active {
background: var(--bg-card-hover);
}
.mobile-search-tip-item .tip-name {
font-weight: 500;
color: var(--text-primary);
font-size: 0.9rem;
margin-bottom: 2px;
}
.mobile-search-tip-item .tip-address {
font-size: 0.8rem;
color: var(--text-muted);
}
/* 地图点击确认弹框 */
.map-click-confirm {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-md);
}
.map-click-confirm .confirm-content {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
max-width: 320px;
width: 100%;
box-shadow: var(--shadow-lg);
animation: popIn 0.2s ease;
}
@keyframes popIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.map-click-confirm .confirm-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
text-align: center;
}
.map-click-confirm .confirm-address {
font-size: 0.9rem;
color: var(--text-secondary);
text-align: center;
padding: var(--spacing-md);
background: var(--bg-darker);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-lg);
line-height: 1.5;
max-height: 80px;
overflow-y: auto;
}
.map-click-confirm .confirm-actions {
display: flex;
gap: var(--spacing-md);
}
.map-click-confirm .confirm-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: var(--radius-md);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition-fast);
}
.map-click-confirm .confirm-btn.cancel {
background: var(--bg-elevated);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.map-click-confirm .confirm-btn.cancel:active {
background: var(--bg-card-hover);
}
.map-click-confirm .confirm-btn.ok {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: var(--bg-dark);
}
.map-click-confirm .confirm-btn.ok:active {
transform: scale(0.98);
}
/* 浮动结果按钮 */
.floating-result-btn {
position: fixed;
bottom: 24px;
right: 24px;
padding: 10px 16px;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
border: none;
border-radius: 25px;
color: #0f0f1a;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
z-index: 9999;
display: none;
align-items: center;
gap: 6px;
box-shadow: 0 4px 16px rgba(245, 158, 11, 0.5);
}
.floating-result-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.6);
}
.floating-result-btn.visible {
display: flex !important;
}
.floating-result-btn .result-icon {
display: none;
}
.floating-result-btn .result-text {
display: none;
}
.floating-result-btn .result-badge {
background: #0f0f1a;
color: #f59e0b;
font-weight: 700;
padding: 4px 8px;
border-radius: 10px;
font-size: 0.8rem;
min-width: 20px;
text-align: center;
}
/* PC端显示为紧凑按钮图标+数字 */
.floating-result-btn::before {
content: '📋';
font-size: 1rem;
}
@media (max-width: 768px) {
.mobile-search-bar {
display: block;
}
.floating-result-btn {
bottom: 90px;
left: 50%;
right: auto;
transform: translateX(-50%);
padding: 12px 20px;
font-size: 0.95rem;
box-shadow: 0 6px 24px rgba(245, 158, 11, 0.6);
}
.floating-result-btn::before {
content: '📋 查看结果';
font-size: 0.95rem;
}
.floating-result-btn .result-badge {
padding: 4px 10px;
font-size: 0.85rem;
}
}
/* 中心点信息 */ /* 中心点信息 */
.center-info { .center-info {
position: absolute; position: absolute;
@ -901,11 +1171,12 @@ ul {
padding: var(--spacing-md) var(--spacing-lg); padding: var(--spacing-md) var(--spacing-lg);
min-width: 220px; min-width: 220px;
max-width: 300px; max-width: 300px;
background: rgba(22, 33, 62, 0.92); background: rgba(22, 33, 62, 0.95);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
border: 1px solid rgba(245, 158, 11, 0.3); border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 20px rgba(245, 158, 11, 0.1); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 20px rgba(245, 158, 11, 0.1);
overflow: visible;
} }
.info-window h3 { .info-window h3 {
@ -982,32 +1253,571 @@ ul {
} }
} }
/* ========================================
移动端快捷操作栏
======================================== */
.mobile-action-bar {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: var(--spacing-sm) var(--spacing-md);
padding-bottom: calc(var(--spacing-sm) + env(safe-area-inset-bottom, 0));
background: rgba(26, 26, 46, 0.95);
backdrop-filter: blur(10px);
border-top: 1px solid var(--border);
z-index: 60;
gap: var(--spacing-sm);
flex-direction: column;
}
.mobile-action-bar .action-row {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.mobile-action-bar .action-info {
display: flex;
align-items: center;
gap: var(--spacing-sm);
color: var(--text-secondary);
font-size: 0.85rem;
padding: 0 var(--spacing-sm);
}
.mobile-action-bar .location-badge {
background: var(--primary);
color: var(--bg-dark);
font-weight: 700;
padding: 2px 8px;
border-radius: 12px;
min-width: 20px;
text-align: center;
font-size: 0.8rem;
}
.mobile-action-bar .action-search-btn {
flex: 1;
padding: 10px var(--spacing-md);
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border: none;
border-radius: var(--radius-md);
color: var(--bg-dark);
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
}
.mobile-action-bar .action-search-btn:disabled {
opacity: 0.5;
}
.mobile-action-bar .menu-btn {
width: 40px;
height: 40px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 1.2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
/* ========================================
移动端切换按钮隐藏改用底部菜单
======================================== */
.mobile-toggle {
display: none;
}
/* 侧边栏遮罩 */
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 99;
opacity: 0;
transition: opacity var(--transition-normal);
pointer-events: none;
}
.sidebar-overlay.active {
opacity: 1;
pointer-events: auto;
}
/* ========================================
平板端适配 (768px - 1024px)
======================================== */
@media (max-width: 1024px) {
:root {
--sidebar-width: 360px;
}
.floating-results {
width: 340px;
}
}
/* ========================================
移动端适配 (< 768px)
======================================== */
@media (max-width: 768px) { @media (max-width: 768px) {
:root {
--spacing-md: 12px;
--spacing-lg: 16px;
}
.app-container { .app-container {
flex-direction: column; flex-direction: column;
} }
/* 移动端显示操作栏 */
.mobile-action-bar {
display: flex;
}
/* 侧边栏改为底部弹出面板 */
.sidebar { .sidebar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: auto;
width: 100%; width: 100%;
min-width: 100%; min-width: 100%;
max-height: 50vh; max-height: 70vh;
z-index: 100;
transform: translateY(100%);
transition: transform var(--transition-normal);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
overflow: hidden;
} }
.sidebar.active {
transform: translateY(0);
}
.sidebar-overlay {
display: block;
}
/* 地图全屏 */
.map-area { .map-area {
height: 50vh; width: 100%;
height: 100vh;
} }
.location-list { /* Logo 简化 */
max-height: 120px; .logo {
padding: var(--spacing-md);
position: relative;
}
/* 添加拖动指示条 */
.logo::before {
content: '';
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 4px;
background: var(--border-light);
border-radius: 2px;
}
.logo-icon {
width: 36px;
height: 36px;
}
.logo-icon svg {
width: 22px;
height: 22px;
}
.logo h1 {
font-size: 1.1rem;
margin-top: 4px;
}
/* 侧边栏内容可滚动 */
.sidebar-content {
padding: var(--spacing-md);
gap: var(--spacing-md);
max-height: calc(70vh - 80px);
overflow-y: auto;
padding-bottom: calc(var(--spacing-lg) + env(safe-area-inset-bottom, 0));
}
.section {
padding: var(--spacing-md);
}
.section-header h2 {
font-size: 0.9rem;
}
/* 位置列表 */
.locations-section {
display: none; /* 移动端隐藏位置列表,只在地图上显示 */
}
.search-section .section-header {
display: none; /* 简化标题 */
}
/* 快捷标签 */
.quick-tags {
gap: 6px;
margin-top: var(--spacing-sm);
}
.tag {
padding: 8px 12px;
font-size: 0.85rem;
}
.form-group {
margin-bottom: var(--spacing-md);
}
.form-group label {
font-size: 0.85rem;
margin-bottom: 6px;
}
/* 隐藏侧边栏中的搜索按钮(使用底部操作栏的按钮) */
.btn-search {
display: none;
}
/* 中心点信息 */
.center-info {
top: var(--spacing-md);
left: 50%;
transform: translateX(-50%);
}
.center-badge {
padding: 6px var(--spacing-md);
font-size: 0.8rem;
}
/* 浮动结果面板 - 底部弹出,更紧凑 */
.floating-results {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
width: 100%;
max-height: 75vh;
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
animation: slideUp 0.3s ease;
background: var(--bg-card) !important;
backdrop-filter: blur(16px);
display: flex;
flex-direction: column;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.floating-results-header {
padding: var(--spacing-sm) var(--spacing-md);
padding-top: var(--spacing-lg);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
position: relative;
background: var(--bg-elevated);
}
/* 拖动指示条 */
.floating-results-header::before {
content: '';
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
width: 36px;
height: 4px;
background: var(--border-light);
border-radius: 2px;
}
.floating-results-header h3 {
font-size: 1rem;
display: flex;
align-items: center;
gap: 8px;
}
.floating-results-filter {
padding: 8px var(--spacing-md);
background: var(--bg-card);
}
.floating-results-filter input {
padding: 10px var(--spacing-md);
font-size: 0.9rem;
}
.floating-result-list {
padding: 10px 16px;
padding-bottom: calc(20px + env(safe-area-inset-bottom, 0));
gap: 10px;
background: var(--bg-card);
flex: 1;
overflow-y: auto;
}
/* 移动端列表项布局修复 - 使用 !important 覆盖基础样式 */
.floating-result-list > .floating-result-item {
display: grid !important;
grid-template-columns: 36px 1fr 65px !important;
gap: 10px !important;
align-items: center !important;
padding: 12px !important;
background: var(--bg-darker) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-md) !important;
}
.floating-result-list > .floating-result-item > .rank {
grid-column: 1 !important;
width: 36px !important;
height: 36px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.floating-result-list > .floating-result-item > .info {
grid-column: 2 !important;
display: flex !important;
flex-direction: column !important;
gap: 4px !important;
min-width: 0 !important;
overflow: hidden !important;
}
.floating-result-list > .floating-result-item > .info > .name {
font-size: 0.95rem !important;
font-weight: 600 !important;
color: var(--text-primary) !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.floating-result-list > .floating-result-item > .info > .address {
font-size: 0.8rem !important;
color: var(--text-muted) !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.floating-result-list > .floating-result-item > .info > .tel {
font-size: 0.8rem !important;
color: var(--info) !important;
}
.floating-result-list > .floating-result-item > .distance-badge {
grid-column: 3 !important;
width: 65px !important;
min-width: 65px !important;
max-width: 65px !important;
padding: 8px 4px !important;
text-align: center !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
background: rgba(16, 185, 129, 0.12) !important;
border-radius: var(--radius-sm) !important;
border: 1px solid rgba(16, 185, 129, 0.25) !important;
}
.floating-result-list > .floating-result-item > .distance-badge > .num {
font-size: 1.1rem !important;
font-weight: 700 !important;
color: var(--success) !important;
display: block !important;
}
.floating-result-list > .floating-result-item > .distance-badge > .unit {
font-size: 0.7rem !important;
color: var(--success) !important;
display: block !important;
}
/* 信息窗口更紧凑 */
.info-window {
min-width: 200px;
max-width: 280px;
padding: var(--spacing-md);
}
.info-window h3 {
font-size: 0.95rem;
padding-bottom: 8px;
margin-bottom: 8px;
}
.info-window p {
font-size: 0.82rem;
margin-bottom: 6px;
}
.info-window .nav-btn {
padding: 10px 14px;
font-size: 0.85rem;
margin-top: 10px;
}
.info-window .nav-btn svg {
width: 16px;
height: 16px;
}
/* 地图标记更小 */
.custom-marker {
width: 30px;
height: 30px;
font-size: 11px;
border-width: 2px;
}
.center-marker {
width: 36px;
height: 36px;
font-size: 18px;
border-width: 2px;
}
.poi-marker {
width: 26px;
height: 26px;
font-size: 11px;
border-width: 2px;
}
}
/* ========================================
小屏手机适配 (< 375px)
======================================== */
@media (max-width: 375px) {
.floating-results {
max-height: 60vh;
}
.tag {
padding: 6px 10px;
font-size: 0.8rem;
}
.mobile-action-bar .action-search-btn {
font-size: 0.85rem;
padding: 8px var(--spacing-md);
}
}
/* ========================================
横屏模式
======================================== */
@media (max-width: 768px) and (orientation: landscape) {
.sidebar {
max-height: 85vh;
}
.sidebar-content {
max-height: calc(85vh - 70px);
} }
.floating-results { .floating-results {
width: calc(100% - 32px); max-height: 80vh;
max-height: 40vh; width: 55%;
top: auto; right: 0;
bottom: var(--spacing-md); left: auto;
right: var(--spacing-md); border-radius: var(--radius-lg) 0 0 var(--radius-lg);
left: var(--spacing-md); }
.mobile-action-bar {
width: 45%;
left: 0;
right: auto;
border-radius: 0 var(--radius-lg) 0 0;
}
}
/* ========================================
触摸优化
======================================== */
@media (hover: none) and (pointer: coarse) {
/* 增大触摸目标 */
.tag {
min-height: 44px;
display: inline-flex;
align-items: center;
}
.location-remove {
width: 36px;
height: 36px;
}
.floating-result-item {
min-height: 64px;
}
/* 移除hover效果改用active */
.floating-result-item:hover {
background: var(--bg-darker);
border-color: transparent;
transform: none;
}
.floating-result-item:active {
background: var(--bg-card-hover);
border-color: var(--primary);
}
.tag:hover {
background: var(--bg-card);
border-color: var(--border);
color: var(--text-secondary);
}
.tag:active {
background: var(--bg-card-hover);
border-color: var(--primary);
color: var(--primary);
} }
} }
@ -1029,8 +1839,7 @@ ul {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: slideInRight 0.3s ease; animation: slideInRight 0.3s ease;
backdrop-filter: blur(10px); overflow: hidden;
background: rgba(26, 26, 46, 0.95);
} }
@keyframes slideInRight { @keyframes slideInRight {
@ -1128,6 +1937,7 @@ ul {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--spacing-sm); gap: var(--spacing-sm);
background: var(--bg-card);
} }
.floating-result-list::-webkit-scrollbar { .floating-result-list::-webkit-scrollbar {
@ -1145,7 +1955,7 @@ ul {
.floating-result-item { .floating-result-item {
display: flex; display: flex;
align-items: stretch; align-items: flex-start;
gap: var(--spacing-md); gap: var(--spacing-md);
padding: var(--spacing-md); padding: var(--spacing-md);
background: var(--bg-darker); background: var(--bg-darker);
@ -1206,6 +2016,7 @@ ul {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
overflow: visible;
} }
.floating-result-item .name { .floating-result-item .name {
@ -1220,10 +2031,7 @@ ul {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-muted); color: var(--text-muted);
line-height: 1.4; line-height: 1.4;
display: -webkit-box; word-break: break-word;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
} }
.floating-result-item .tel { .floating-result-item .tel {
@ -1249,11 +2057,13 @@ ul {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 52px; min-width: 50px;
max-width: 55px;
padding: var(--spacing-sm); padding: var(--spacing-sm);
background: rgba(16, 185, 129, 0.12); background: rgba(16, 185, 129, 0.12);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
border: 1px solid rgba(16, 185, 129, 0.25); border: 1px solid rgba(16, 185, 129, 0.25);
align-self: center;
} }
.floating-result-item .distance-badge .num { .floating-result-item .distance-badge .num {

View File

@ -34,8 +34,224 @@ document.addEventListener('DOMContentLoaded', async () => {
await loadConfig(); await loadConfig();
initMap(); initMap();
bindEvents(); bindEvents();
initMobileUI();
}); });
// ========================================
// 移动端UI初始化
// ========================================
function initMobileUI() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
const menuBtn = document.getElementById('menuBtn');
const mobileSearchBtn = document.getElementById('mobileSearchBtn');
const mobileActionBar = document.getElementById('mobileActionBar');
const mobileSearchInput = document.getElementById('mobileSearchInput');
const mobileSearchClear = document.getElementById('mobileSearchClear');
const mobileSearchTips = document.getElementById('mobileSearchTips');
const floatingResultBtn = document.getElementById('floatingResultBtn');
const floatingResultBadge = document.getElementById('floatingResultBadge');
if (!sidebar || !overlay) return;
// 切换侧边栏
function toggleSidebar() {
sidebar.classList.toggle('active');
overlay.classList.toggle('active');
// 打开侧边栏时隐藏浮动按钮
if (sidebar.classList.contains('active') && floatingResultBtn) {
floatingResultBtn.style.display = 'none';
}
}
// 关闭侧边栏
function closeSidebar() {
sidebar.classList.remove('active');
overlay.classList.remove('active');
// 关闭侧边栏时,如果有搜索结果且结果面板隐藏,显示浮动按钮
const panel = document.getElementById('floatingResults');
if (floatingResultBtn && state.currentPOIs && state.currentPOIs.length > 0 &&
(!panel || panel.style.display === 'none')) {
window.showFloatingResultBtn(state.currentPOIs.length);
}
}
// 菜单按钮点击
if (menuBtn) {
menuBtn.addEventListener('click', toggleSidebar);
}
// 遮罩点击关闭
overlay.addEventListener('click', closeSidebar);
// 移动端搜索按钮
if (mobileSearchBtn) {
mobileSearchBtn.addEventListener('click', handleSearch);
}
// 移动端地址搜索
if (mobileSearchInput) {
let searchTimeout;
mobileSearchInput.addEventListener('input', (e) => {
const value = e.target.value.trim();
mobileSearchClear.style.display = value ? 'block' : 'none';
clearTimeout(searchTimeout);
if (value.length >= 2) {
searchTimeout = setTimeout(() => fetchMobileSearchTips(value), 300);
} else {
mobileSearchTips.classList.remove('active');
}
});
mobileSearchInput.addEventListener('focus', () => {
if (mobileSearchInput.value.trim().length >= 2) {
mobileSearchTips.classList.add('active');
}
});
mobileSearchClear.addEventListener('click', () => {
mobileSearchInput.value = '';
mobileSearchClear.style.display = 'none';
mobileSearchTips.classList.remove('active');
});
// 点击外部关闭搜索提示
document.addEventListener('click', (e) => {
if (!e.target.closest('.mobile-search-bar')) {
mobileSearchTips.classList.remove('active');
}
});
}
// 浮动结果按钮点击 - 重新打开结果面板
if (floatingResultBtn) {
floatingResultBtn.addEventListener('click', () => {
const panel = document.getElementById('floatingResults');
if (panel && state.currentPOIs.length > 0) {
panel.style.display = 'flex';
floatingResultBtn.style.display = 'none';
if (mobileActionBar) {
mobileActionBar.style.display = 'none';
}
}
});
}
// 点击搜索结果后自动关闭侧边栏(移动端)
window.closeSidebarOnMobile = function() {
if (window.innerWidth <= 768) {
closeSidebar();
}
};
// 隐藏/显示移动端操作栏
window.toggleMobileActionBar = function(show) {
if (mobileActionBar && window.innerWidth <= 768) {
mobileActionBar.style.display = show ? 'flex' : 'none';
}
};
// 显示浮动结果按钮
window.showFloatingResultBtn = function(count) {
if (floatingResultBtn) {
// 清除所有内联样式让CSS控制
floatingResultBtn.style.cssText = 'display: flex;';
if (floatingResultBadge) {
floatingResultBadge.textContent = count;
}
console.log('显示浮动结果按钮,数量:', count);
}
};
// 隐藏浮动结果按钮
window.hideFloatingResultBtn = function() {
if (floatingResultBtn) {
floatingResultBtn.style.display = 'none';
}
};
// 监听窗口大小变化
window.addEventListener('resize', () => {
if (window.innerWidth > 768) {
closeSidebar();
if (mobileActionBar) {
mobileActionBar.style.display = 'none';
}
if (floatingResultBtn) {
floatingResultBtn.style.display = 'none';
}
} else {
const floatingResults = document.getElementById('floatingResults');
const resultsHidden = !floatingResults || floatingResults.style.display === 'none';
if (mobileActionBar && resultsHidden) {
mobileActionBar.style.display = 'flex';
}
// 如果有搜索结果且结果面板隐藏,显示浮动按钮
if (resultsHidden && state.currentPOIs && state.currentPOIs.length > 0) {
window.showFloatingResultBtn(state.currentPOIs.length);
}
}
});
}
// 移动端地址搜索提示
async function fetchMobileSearchTips(keywords) {
const mobileSearchTips = document.getElementById('mobileSearchTips');
try {
const response = await fetch(`/api/tips?keywords=${encodeURIComponent(keywords)}&city=`);
const data = await response.json();
if (data.tips && data.tips.length > 0) {
renderMobileSearchTips(data.tips);
} else {
mobileSearchTips.classList.remove('active');
}
} catch (error) {
console.error('获取搜索提示失败:', error);
}
}
function renderMobileSearchTips(tips) {
const mobileSearchTips = document.getElementById('mobileSearchTips');
const mobileSearchInput = document.getElementById('mobileSearchInput');
const mobileSearchClear = document.getElementById('mobileSearchClear');
mobileSearchTips.innerHTML = tips.map(tip => `
<div class="mobile-search-tip-item" data-lng="${tip.location.lng}" data-lat="${tip.location.lat}" data-name="${tip.name}" data-address="${tip.district}${tip.address}">
<div class="tip-name">${tip.name}</div>
<div class="tip-address">${tip.district}${tip.address}</div>
</div>
`).join('');
// 绑定点击事件
mobileSearchTips.querySelectorAll('.mobile-search-tip-item').forEach(item => {
item.addEventListener('click', () => {
const location = {
name: item.dataset.name,
address: item.dataset.address,
coordinate: {
lng: parseFloat(item.dataset.lng),
lat: parseFloat(item.dataset.lat)
}
};
addLocation(location);
// 清理搜索框
mobileSearchInput.value = '';
mobileSearchClear.style.display = 'none';
mobileSearchTips.classList.remove('active');
});
});
mobileSearchTips.classList.add('active');
}
async function loadConfig() { async function loadConfig() {
try { try {
const response = await fetch('/api/config'); const response = await fetch('/api/config');
@ -84,16 +300,17 @@ function initMap() {
// 地图点击事件 // 地图点击事件
state.map.on('click', handleMapClick); state.map.on('click', handleMapClick);
// 尝试定位到用户位置 // 使用高德IP定位只定位地图中心不自动添加位置
AMap.plugin('AMap.Geolocation', () => { AMap.plugin('AMap.CitySearch', () => {
const geolocation = new AMap.Geolocation({ const citySearch = new AMap.CitySearch();
enableHighAccuracy: true, citySearch.getLocalCity((status, result) => {
timeout: 10000 if (status === 'complete' && result.info === 'OK') {
}); // IP定位成功将地图移动到当前城市
const bounds = result.bounds;
geolocation.getCurrentPosition((status, result) => { if (bounds) {
if (status === 'complete') { state.map.setBounds(bounds);
state.map.setCenter([result.position.lng, result.position.lat]); }
console.log('已定位到城市:', result.city);
} }
}); });
}); });
@ -220,10 +437,13 @@ function renderSearchTips(tips) {
// 地图点击添加位置 // 地图点击添加位置
// ======================================== // ========================================
// 待确认的位置
let pendingLocation = null;
function handleMapClick(e) { function handleMapClick(e) {
const lnglat = e.lnglat; const lnglat = e.lnglat;
// 逆地理编码获取地址 // 逆地理编码获取地址,然后直接添加
AMap.plugin('AMap.Geocoder', () => { AMap.plugin('AMap.Geocoder', () => {
const geocoder = new AMap.Geocoder(); const geocoder = new AMap.Geocoder();
@ -238,12 +458,50 @@ function handleMapClick(e) {
lat: lnglat.lat lat: lnglat.lat
} }
}; };
// 直接添加位置
addLocation(location); addLocation(location);
} }
}); });
}); });
} }
// 显示地图点击确认弹框
function showMapClickConfirm(address) {
const confirmDialog = document.getElementById('mapClickConfirm');
const confirmAddress = document.getElementById('confirmAddress');
const confirmOk = document.getElementById('confirmOk');
const confirmCancel = document.getElementById('confirmCancel');
if (!confirmDialog) return;
confirmAddress.textContent = address;
confirmDialog.style.display = 'flex';
// 确认添加
confirmOk.onclick = () => {
if (pendingLocation) {
addLocation(pendingLocation);
pendingLocation = null;
}
confirmDialog.style.display = 'none';
};
// 取消
confirmCancel.onclick = () => {
pendingLocation = null;
confirmDialog.style.display = 'none';
};
// 点击背景关闭
confirmDialog.onclick = (e) => {
if (e.target === confirmDialog) {
pendingLocation = null;
confirmDialog.style.display = 'none';
}
};
}
// ======================================== // ========================================
// 位置管理 // 位置管理
// ======================================== // ========================================
@ -343,7 +601,27 @@ function updateLocationList() {
function updateSearchButton() { function updateSearchButton() {
const btn = document.getElementById('searchBtn'); const btn = document.getElementById('searchBtn');
btn.disabled = state.locations.length < 2; const mobileBtn = document.getElementById('mobileSearchBtn');
const mobileCount = document.getElementById('mobileLocationCount');
const count = state.locations.length;
const disabled = count < 2;
btn.disabled = disabled;
if (mobileBtn) {
mobileBtn.disabled = disabled;
// 更新按钮文字显示当前状态
if (count === 0) {
mobileBtn.textContent = '🔍 搜索会面点';
} else if (count === 1) {
mobileBtn.textContent = '🔍 再添加1个位置';
} else {
mobileBtn.textContent = '🔍 搜索会面点';
}
}
if (mobileCount) {
mobileCount.textContent = count;
}
} }
// ======================================== // ========================================
@ -355,7 +633,23 @@ async function handleSearch() {
const radius = parseInt(document.getElementById('searchRadius').value); const radius = parseInt(document.getElementById('searchRadius').value);
if (!keywords) { if (!keywords) {
alert('请输入搜索关键词'); // 自动打开侧边栏设置面板
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
if (sidebar && window.innerWidth <= 768) {
sidebar.classList.add('active');
if (overlay) overlay.classList.add('active');
}
// 聚焦到关键词输入框
const poiInput = document.getElementById('poiKeywords');
if (poiInput) {
poiInput.focus();
poiInput.setAttribute('placeholder', '⚠️ 请选择或输入搜索类型...');
// 3秒后恢复原来的placeholder
setTimeout(() => {
poiInput.setAttribute('placeholder', '例如咖啡馆、餐厅、KTV...');
}, 3000);
}
return; return;
} }
@ -396,6 +690,11 @@ async function handleSearch() {
// 显示搜索结果 // 显示搜索结果
showSearchResults(searchData.pois, radius); showSearchResults(searchData.pois, radius);
// 移动端自动关闭侧边栏
if (window.closeSidebarOnMobile) {
window.closeSidebarOnMobile();
}
} catch (error) { } catch (error) {
console.error('搜索失败:', error); console.error('搜索失败:', error);
alert('搜索失败,请稍后重试'); alert('搜索失败,请稍后重试');
@ -420,8 +719,8 @@ function showCenterMarker() {
state.map.add(state.centerMarker); state.map.add(state.centerMarker);
// 显示中心点信息 // 不显示中心点浮动提示
document.getElementById('centerInfo').style.display = 'block'; // document.getElementById('centerInfo').style.display = 'block';
} }
function showSearchResults(pois, radius) { function showSearchResults(pois, radius) {
@ -484,47 +783,92 @@ function renderResultList(pois) {
if (pois.length === 0) { if (pois.length === 0) {
list.innerHTML = ` list.innerHTML = `
<li class="floating-result-empty"> <li style="display:flex;flex-direction:column;align-items:center;padding:32px;color:#64748b;text-align:center;">
<span class="icon">😕</span> <span style="font-size:2.5rem;opacity:0.5;">😕</span>
<span>未找到相关地点</span> <span>未找到相关地点</span>
<span style="font-size: 0.8rem; opacity: 0.7;">尝试增大搜索半径或更换关键词</span> <span style="font-size:0.8rem;opacity:0.7;">尝试增大搜索半径或更换关键词</span>
</li> </li>
`; `;
} else { } else {
list.innerHTML = pois.map((poi, index) => { list.innerHTML = pois.map((poi, index) => {
const dist = parseDistance(poi.distance); const dist = parseDistance(poi.distance);
return ` const rankColors = [
<li class="floating-result-item" data-index="${index}" data-name="${poi.name}"> 'background:linear-gradient(135deg,#ffd700,#ffb800);color:#0f0f1a',
<div class="rank">${index + 1}</div> 'background:linear-gradient(135deg,#c0c0c0,#a0a0a0);color:#0f0f1a',
<div class="info"> 'background:linear-gradient(135deg,#cd7f32,#b87333);color:#0f0f1a'
<div class="name">${poi.name}</div> ];
<div class="address">${poi.address || poi.type}</div> const rankBg = rankColors[index] || 'background:#16213e;color:#64748b';
${poi.tel ? `<div class="tel">📞 <a href="tel:${poi.tel}" onclick="event.stopPropagation()">${poi.tel}</a></div>` : ''}
</div>
<div class="distance-badge">
<span class="num">${dist.value}</span>
<span class="unit">${dist.unit}</span>
</div>
</li>
`}).join('');
// 绑定点击事件 return `<li data-index="${index}" data-name="${poi.name}" onclick="focusPOI(${index})" style="
list.querySelectorAll('.floating-result-item').forEach(item => { display: flex;
item.addEventListener('click', (e) => { flex-direction: row;
e.preventDefault(); align-items: center;
e.stopPropagation(); padding: 12px;
const index = parseInt(item.dataset.index); margin-bottom: 8px;
focusPOI(index); background: #0a0a12;
}); border: 1px solid #2d2d44;
}); border-radius: 10px;
cursor: pointer;
">
<span style="
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.85rem;
margin-right: 12px;
${rankBg};
">${index + 1}</span>
<span style="
flex: 1;
min-width: 0;
overflow: hidden;
margin-right: 10px;
">
<span style="display:block;font-weight:600;font-size:0.95rem;color:#f8fafc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${poi.name}</span>
<span style="display:block;font-size:0.8rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${poi.address || poi.type}</span>
${poi.tel ? `<span style="display:block;font-size:0.8rem;color:#3b82f6;">📞 ${poi.tel}</span>` : ''}
</span>
<span style="
flex-shrink: 0;
width: 55px;
padding: 6px 4px;
text-align: center;
background: rgba(16,185,129,0.12);
border-radius: 6px;
border: 1px solid rgba(16,185,129,0.25);
">
<span style="display:block;font-size:1rem;font-weight:700;color:#10b981;">${dist.value}</span>
<span style="display:block;font-size:0.7rem;color:#10b981;">${dist.unit}</span>
</span>
</li>`;
}).join('');
// 点击事件已通过 onclick 内联绑定
} }
panel.style.display = 'flex'; panel.style.display = 'flex';
filterInput.value = ''; filterInput.value = '';
// 移动端隐藏操作栏
if (window.toggleMobileActionBar) {
window.toggleMobileActionBar(false);
}
// 绑定关闭按钮事件 // 绑定关闭按钮事件
document.getElementById('closeFloatingResults').onclick = () => { document.getElementById('closeFloatingResults').onclick = () => {
panel.style.display = 'none'; panel.style.display = 'none';
// 移动端显示操作栏和浮动结果按钮
if (window.toggleMobileActionBar) {
window.toggleMobileActionBar(true);
}
if (window.showFloatingResultBtn && state.currentPOIs.length > 0) {
window.showFloatingResultBtn(state.currentPOIs.length);
}
}; };
// 绑定筛选事件 // 绑定筛选事件
@ -592,6 +936,20 @@ function focusPOI(index) {
state.map.setZoom(16); state.map.setZoom(16);
showPOIInfoWindow(marker, poi); showPOIInfoWindow(marker, poi);
}, 300); }, 300);
// 移动端:关闭结果面板,显示浮动按钮
if (window.innerWidth <= 768) {
const panel = document.getElementById('floatingResults');
if (panel) {
panel.style.display = 'none';
}
if (window.showFloatingResultBtn) {
window.showFloatingResultBtn(state.currentPOIs.length);
}
if (window.toggleMobileActionBar) {
window.toggleMobileActionBar(true);
}
}
} }
} }
@ -656,6 +1014,14 @@ function clearSearchResults() {
document.getElementById('centerInfo').style.display = 'none'; document.getElementById('centerInfo').style.display = 'none';
document.getElementById('resultFilter').value = ''; document.getElementById('resultFilter').value = '';
// 移动端显示操作栏,隐藏浮动按钮
if (window.toggleMobileActionBar) {
window.toggleMobileActionBar(true);
}
if (window.hideFloatingResultBtn) {
window.hideFloatingResultBtn();
}
// 清除标记 // 清除标记
clearPOIMarkers(); clearPOIMarkers();

View File

@ -2,17 +2,23 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0f0f1a">
<title>会面点 - 寻找最佳聚会地点</title> <title>会面点 - 寻找最佳聚会地点</title>
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css?v=20260112d">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
</head> </head>
<body> <body>
<div class="app-container"> <div class="app-container">
<!-- 侧边栏遮罩 -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- 侧边栏 --> <!-- 侧边栏 -->
<aside class="sidebar"> <aside class="sidebar" id="sidebar">
<div class="logo"> <div class="logo">
<div class="logo-icon"> <div class="logo-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -81,8 +87,8 @@
<div class="form-group"> <div class="form-group">
<label for="searchRadius">搜索半径</label> <label for="searchRadius">搜索半径</label>
<div class="radius-slider"> <div class="radius-slider">
<input type="range" id="searchRadius" min="500" max="10000" step="500" value="3000"> <input type="range" id="searchRadius" min="500" max="10000" step="500" value="1000">
<span class="radius-value" id="radiusValue">3 公里</span> <span class="radius-value" id="radiusValue">1 公里</span>
</div> </div>
</div> </div>
@ -102,6 +108,34 @@
<main class="map-area"> <main class="map-area">
<div id="mapContainer"></div> <div id="mapContainer"></div>
<!-- 移动端搜索框 -->
<div class="mobile-search-bar" id="mobileSearchBar">
<div class="mobile-search-input-wrapper">
<input type="text" id="mobileSearchInput" placeholder="🔍 搜索地址添加位置...">
<button class="mobile-search-clear" id="mobileSearchClear" style="display:none;">×</button>
</div>
<div class="mobile-search-tips" id="mobileSearchTips"></div>
</div>
<!-- 地图点击确认弹框 -->
<div class="map-click-confirm" id="mapClickConfirm" style="display:none;">
<div class="confirm-content">
<div class="confirm-title">添加此位置?</div>
<div class="confirm-address" id="confirmAddress">加载中...</div>
<div class="confirm-actions">
<button class="confirm-btn cancel" id="confirmCancel">取消</button>
<button class="confirm-btn ok" id="confirmOk">添加</button>
</div>
</div>
</div>
<!-- 浮动结果按钮 -->
<button class="floating-result-btn" id="floatingResultBtn">
<span class="result-icon">📋</span>
<span class="result-text">查看结果</span>
<span class="result-badge" id="floatingResultBadge">0</span>
</button>
<!-- 中心点信息 --> <!-- 中心点信息 -->
<div class="center-info" id="centerInfo" style="display: none;"> <div class="center-info" id="centerInfo" style="display: none;">
<div class="center-badge"> <div class="center-badge">
@ -123,6 +157,19 @@
<ul class="floating-result-list" id="floatingResultList"></ul> <ul class="floating-result-list" id="floatingResultList"></ul>
</div> </div>
<!-- 移动端快捷操作栏 -->
<div class="mobile-action-bar" id="mobileActionBar">
<div class="action-row">
<button class="menu-btn" id="menuBtn" aria-label="设置">⚙️</button>
<button class="action-search-btn" id="mobileSearchBtn" disabled>
🔍 搜索会面点
</button>
<div class="action-info">
<span class="location-badge" id="mobileLocationCount">0</span>
</div>
</div>
</div>
<!-- 加载提示 --> <!-- 加载提示 -->
<div class="loading-overlay" id="loadingOverlay" style="display: none;"> <div class="loading-overlay" id="loadingOverlay" style="display: none;">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
@ -132,6 +179,6 @@
</div> </div>
<!-- 应用脚本 --> <!-- 应用脚本 -->
<script src="/static/js/app.js"></script> <script src="/static/js/app.js?v=20260112h"></script>
</body> </body>
</html> </html>