diff --git a/css/pages/towns.css b/css/pages/towns.css new file mode 100644 index 0000000..a8bc8a0 --- /dev/null +++ b/css/pages/towns.css @@ -0,0 +1,1644 @@ +/* Page-Specific Styles for Towns */ + +.towns-hero-bg { + background-image: url('https://img.lunadeer.cn/i/2025/11/26/69267755e14e3.png'); +} + +/* Container */ +.towns-container { + max-width: 1200px; + margin: 0 auto; + padding: 40px 20px; +} + +/* Controls - same as facilities */ +.towns-container .controls-section { + background: var(--card-bg); + padding: 30px; + border-radius: var(--radius-large); + box-shadow: 0 4px 20px rgba(0,0,0,0.05); + margin-bottom: 40px; +} + +.towns-container .controls-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + flex-wrap: wrap; + gap: 20px; +} + +.towns-container .section-title { + font-size: 24px; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.towns-container .search-box { + position: relative; + max-width: 400px; + width: 100%; +} + +.towns-container .search-box i { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + color: var(--text-secondary); +} + +.towns-container .search-box input { + width: 100%; + padding: 10px 16px 10px 44px; + border: 1px solid rgba(0,0,0,0.1); + border-radius: 12px; + font-size: 15px; + background: #f5f5f7; + transition: var(--transition); +} + +.towns-container .search-box input:focus { + outline: none; + background: #fff; + border-color: var(--accent-color); + box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.1); +} + +.towns-container .filters-wrapper { + display: flex; + flex-direction: column; + gap: 16px; + border-top: 1px solid rgba(0,0,0,0.05); + padding-top: 20px; +} + +.towns-container .filter-group { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; +} + +.towns-container .filter-label { + font-weight: 600; + font-size: 14px; + color: var(--text-secondary); + min-width: 70px; + display: flex; + align-items: center; + gap: 6px; +} + +.towns-container .filter-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.towns-container .filter-tag { + background: #fff; + border: 1px solid rgba(0,0,0,0.1); + padding: 6px 14px; + border-radius: 18px; + font-size: 13px; + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + gap: 6px; +} + +.towns-container .filter-tag:hover { + background: #f5f5f7; + color: var(--text-primary); + border-color: rgba(0,0,0,0.2); +} + +.towns-container .filter-tag.active { + background: var(--text-primary); + color: white; + border-color: var(--text-primary); +} + +/* ========== Town Cards Grid ========== */ +.towns-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 24px; +} + +.town-card { + background: var(--card-bg); + border-radius: var(--radius-medium); + box-shadow: 0 2px 12px rgba(0,0,0,0.04); + transition: var(--transition); + cursor: pointer; + display: flex; + flex-direction: column; + border: 1px solid rgba(0,0,0,0.03); + overflow: hidden; + position: relative; +} + +.town-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 32px rgba(0,0,0,0.08); +} + +/* Card logo background */ +.town-card-bg { + height: 140px; + background-size: cover; + background-position: center; + background-color: #e8ecf1; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.town-card-bg.no-logo { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.town-card-bg::after, +.town-modal-banner::after, +.town-preview-banner::after, +.town-preview-card-cover::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(to top, rgba(15, 23, 42, 0.28), transparent 55%); + pointer-events: none; +} + +.town-card-bg .town-logo-img { + max-height: 80px; + max-width: 80%; + object-fit: contain; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); +} + +.town-card-bg .town-logo-placeholder { + font-size: 48px; + color: rgba(255,255,255,0.6); + position: relative; + z-index: 1; +} + +/* Card icon badges overlay */ +.town-card-icons { + position: absolute; + bottom: -14px; + left: 16px; + display: flex; + gap: 8px; + z-index: 2; +} + +.town-icon-badge { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + color: #fff; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + border: 2px solid #fff; +} + +/* Scale icons */ +.icon-scale-small { background: #60a5fa; } +.icon-scale-medium { background: #f59e0b; } +.icon-scale-large { background: #ef4444; } + +/* Town type icons */ +.icon-type-building { background: #8b5cf6; } +.icon-type-adventure { background: #10b981; } +.icon-type-industry { background: #f97316; } + +/* Recruitment icons */ +.icon-recruit-welcome { background: #22c55e; } +.icon-recruit-closed { background: #ef4444; } +.icon-recruit-maybe { background: #eab308; } + +.town-card-body { + padding: 24px 20px 20px; + flex: 1; + display: flex; + flex-direction: column; +} + +.town-card-title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 10px; + line-height: 1.3; +} + +.town-card-meta { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: auto; + padding-top: 12px; + border-top: 1px solid #f0f0f0; +} + +.town-meta-tag { + font-size: 11px; + background: #f5f5f7; + padding: 4px 10px; + border-radius: 6px; + color: var(--text-secondary); + font-weight: 500; + display: flex; + align-items: center; + gap: 4px; +} + +/* ========== Town Detail Modal ========== */ +.town-modal-content { + background-color: #fff; + margin: 40px auto; + border-radius: var(--radius-large); + max-width: 720px; + width: 90%; + padding: 0; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.3); + position: relative; + max-height: 90vh; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.town-modal-content::-webkit-scrollbar { + width: 6px; +} + +.town-modal-content::-webkit-scrollbar-track { + background: transparent; + margin: 10px 0; +} + +.town-modal-content::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: 10px; +} + +.town-modal-content::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.25); +} + +/* Modal hero banner */ +.town-modal-banner { + height: 180px; + background-size: cover; + background-position: center; + background-color: #e8ecf1; + position: relative; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-large) var(--radius-large) 0 0; +} + +.town-modal-banner.no-logo { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.town-modal-banner .town-banner-logo { + max-height: 100px; + max-width: 80%; + object-fit: contain; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0,0,0,0.2); +} + +.town-modal-banner .town-banner-placeholder { + font-size: 64px; + color: rgba(255,255,255,0.5); + position: relative; + z-index: 1; +} + +.town-modal .close-modal { + position: absolute; + top: 20px; + right: 24px; + font-size: 24px; + color: var(--text-secondary); + cursor: pointer; + transition: 0.2s; + z-index: 10; + background: rgba(255,255,255,0.8); + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.town-modal .close-modal:hover { + background: #f0f0f0; + color: var(--text-primary); +} + +.town-modal-header { + padding: 24px 40px 20px; + background: linear-gradient(to bottom, #fff, #fafafa); + border-bottom: 1px solid rgba(0,0,0,0.05); +} + +.town-modal-title { + font-size: 32px; + font-weight: 700; + margin-bottom: 16px; + line-height: 1.2; +} + +.town-modal-badges-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.town-modal-badges { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.town-modal-actions { + display: flex; + gap: 8px; + align-items: center; + flex-shrink: 0; +} + +.town-badge { + padding: 6px 14px; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; +} + +.badge-scale-small { background: #dbeafe; color: #1d4ed8; } +.badge-scale-medium { background: #fef3c7; color: #b45309; } +.badge-scale-large { background: #fee2e2; color: #b91c1c; } + +.badge-type-building { background: #ede9fe; color: #6d28d9; } +.badge-type-adventure { background: #d1fae5; color: #047857; } +.badge-type-industry { background: #ffedd5; color: #c2410c; } + +.badge-recruit-welcome { background: #e8fceb; color: #15803d; } +.badge-recruit-closed { background: #feebeb; color: #b91c1c; } +.badge-recruit-maybe { background: #fef9c3; color: #a16207; } + +.town-modal-body { + padding: 30px 40px 50px; +} + +.town-modal-body .modal-section { + margin-top: 32px; +} + +.town-modal-body .modal-section-title { + font-size: 16px; + font-weight: 700; + margin-bottom: 16px; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 10px; + border-left: 4px solid var(--accent-color); + padding-left: 12px; +} + +.town-modal-body .modal-section-title i { + color: var(--accent-color); + width: 20px; + text-align: center; +} + +/* Btn styles (share/edit) - reuse from facilities */ +.btn-share-town { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 16px; + background: transparent; + color: var(--text-secondary); + border: 1.5px solid rgba(0,0,0,0.12); + border-radius: 18px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; + flex-shrink: 0; +} + +.btn-share-town:hover { + color: var(--accent-color); + border-color: var(--accent-color); + background: rgba(0,113,227,0.04); +} + +.btn-share-town.shared { + color: #15803d; + border-color: #34c759; + background: #e8fceb; +} + +.btn-edit-town { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 16px; + background: transparent; + color: var(--accent-color); + border: 1.5px solid var(--accent-color); + border-radius: 18px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; + flex-shrink: 0; +} + +.btn-edit-town:hover { + background: var(--accent-color); + color: #fff; +} + +.town-map-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: #fff; + background: var(--accent-color); + padding: 6px 16px; + border-radius: 20px; + text-decoration: none; + font-weight: 500; + font-size: 13px; + margin-left: 12px; + transition: 0.2s; +} + +.town-map-link:hover { + background: #005bb5; + transform: translateY(-1px); +} + +/* Contributors list */ +.town-modal-body .contributors-list { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.town-modal-body .contributor-tag { + display: flex; + align-items: center; + background: #ffffff; + border: 1px solid #eee; + padding: 6px 14px; + border-radius: 30px; + font-size: 14px; + color: var(--text-primary); + box-shadow: 0 2px 4px rgba(0,0,0,0.02); +} + +.town-modal-body .contributor-tag img { + width: 24px; + height: 24px; + border-radius: 50%; + margin-right: 10px; + background: #eee; +} + +/* Instructions & Notes */ +.town-modal-body .instruction-content, +.town-modal-body .notes-content { + background: #f9f9fa; + padding: 24px; + border-radius: 16px; + border: 1px solid rgba(0,0,0,0.03); +} + +.town-modal-body .instruction-content p, +.town-modal-body .notes-content p { + font-size: 15px; + margin-bottom: 12px; + color: var(--text-primary); + line-height: 1.7; +} + +.town-modal-body .instruction-content p:last-child, +.town-modal-body .notes-content p:last-child { + margin-bottom: 0; +} + +.town-modal-body .instruction-content img, +.town-modal-body .notes-content img { + max-width: 100%; + border-radius: 12px; + margin: 12px 0 20px; + border: 1px solid rgba(0,0,0,0.05); + box-shadow: 0 4px 12px rgba(0,0,0,0.05); +} + +/* Video Embed */ +.town-modal-body .video-embed-wrapper { + position: relative; + width: 100%; + padding-bottom: 56.25%; + margin: 12px 0 20px; + border-radius: 12px; + overflow: hidden; + background: #000; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} + +.town-modal-body .video-embed-wrapper iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; +} + +/* No results */ +.towns-container .no-results-message { + text-align: center; + padding: 60px; + color: var(--text-secondary); + font-size: 16px; + background: var(--card-bg); + border-radius: var(--radius-medium); +} + +.towns-container .is-hidden { + display: none !important; +} + +/* ========== Add & Editor shared buttons ========== */ +.towns-container .title-with-action { + display: flex; + align-items: center; + gap: 16px; +} + +.btn-add-town { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 18px; + background: var(--accent-color); + color: #fff; + border: none; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + white-space: nowrap; +} + +.btn-add-town:hover { + background: #005bb5; + transform: translateY(-1px); +} + +/* ========== Editor Modal ========== */ +.town-editor-modal-content { + background-color: #fff; + margin: 20px auto; + border-radius: var(--radius-large); + max-width: 1280px; + width: 95%; + padding: 0; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.3); + position: relative; + max-height: calc(100vh - 40px); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.town-editor-modal-content .close-editor-modal { + position: absolute; + top: 16px; + right: 20px; + font-size: 24px; + color: var(--text-secondary); + cursor: pointer; + transition: 0.2s; + z-index: 10; + background: rgba(255,255,255,0.9); + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.town-editor-modal-content .close-editor-modal:hover { + background: #f0f0f0; + color: var(--text-primary); +} + +.town-editor-modal-content .editor-modal-header { + padding: 20px 28px; + border-bottom: 1px solid rgba(0,0,0,0.08); + background: linear-gradient(to bottom, #fff, #fafafa); + border-radius: var(--radius-large) var(--radius-large) 0 0; + flex-shrink: 0; +} + +.town-editor-modal-content .editor-modal-header h3 { + font-size: 22px; + font-weight: 700; + display: flex; + align-items: center; + gap: 10px; +} + +.town-editor-modal-content .editor-layout { + display: flex; + flex: 1; + overflow: hidden; + min-height: 0; +} + +.town-editor-modal-content .editor-preview { + flex: 0 0 45%; + display: flex; + flex-direction: column; + border-right: 1px solid rgba(0,0,0,0.08); + background: #f5f5f7; +} + +.town-editor-modal-content .editor-panel-title { + font-size: 12px; + font-weight: 700; + color: var(--text-secondary); + padding: 14px 24px; + border-bottom: 1px solid rgba(0,0,0,0.05); + display: flex; + align-items: center; + gap: 8px; + text-transform: uppercase; + letter-spacing: 0.6px; + flex-shrink: 0; + background: rgba(255,255,255,0.6); +} + +.town-editor-modal-content .editor-preview-content { + flex: 1; + overflow-y: auto; + padding: 20px; +} + +.town-editor-modal-content .preview-stack { + display: flex; + flex-direction: column; + gap: 20px; +} + +.town-editor-modal-content .editor-preview-content::-webkit-scrollbar, +.town-editor-modal-content .editor-form-scroll::-webkit-scrollbar { + width: 5px; +} + +.town-editor-modal-content .editor-preview-content::-webkit-scrollbar-track, +.town-editor-modal-content .editor-form-scroll::-webkit-scrollbar-track { + background: transparent; +} + +.town-editor-modal-content .editor-preview-content::-webkit-scrollbar-thumb, +.town-editor-modal-content .editor-form-scroll::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.12); + border-radius: 10px; +} + +/* Preview Card in editor */ +.town-editor-modal-content .preview-facility { + background: #fff; + border-radius: 16px; + box-shadow: 0 2px 12px rgba(0,0,0,0.06); + overflow: hidden; +} + +.town-editor-modal-content .preview-card-shell { + background: #fff; + border-radius: 18px; + overflow: hidden; + box-shadow: 0 16px 36px rgba(15, 23, 42, 0.08); + border: 1px solid rgba(15, 23, 42, 0.06); +} + +.town-editor-modal-content .preview-card-cover { + height: 164px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + background-size: cover; + background-position: center; + background-color: #e8ecf1; +} + +.town-editor-modal-content .preview-card-cover.no-logo { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.town-editor-modal-content .preview-card-cover .town-logo-placeholder { + font-size: 52px; + color: rgba(255,255,255,0.62); + position: relative; + z-index: 1; +} + +.town-editor-modal-content .preview-card-body { + padding: 24px 20px 20px; +} + +.town-editor-modal-content .preview-card-title { + font-size: 19px; + font-weight: 700; + color: var(--text-primary); + margin: 0 0 12px; +} + +.town-editor-modal-content .preview-card-icons { + position: absolute; + left: 16px; + bottom: -14px; + display: flex; + gap: 8px; + z-index: 2; +} + +.town-editor-modal-content .preview-card-meta { + display: flex; + gap: 8px; + flex-wrap: wrap; + padding-top: 14px; + border-top: 1px solid #eef2f7; +} + +.town-editor-modal-content .preview-card-tag { + font-size: 11px; + background: #f5f5f7; + padding: 4px 10px; + border-radius: 6px; + color: var(--text-secondary); + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.town-editor-modal-content .preview-detail-shell { + background: #fff; + border-radius: 18px; + overflow: hidden; + box-shadow: 0 16px 36px rgba(15, 23, 42, 0.08); + border: 1px solid rgba(15, 23, 42, 0.06); +} + +.town-editor-modal-content .preview-detail-header { + padding: 24px 24px 20px; + background: linear-gradient(to bottom, #fff, #fafafa); + border-bottom: 1px solid rgba(0,0,0,0.05); +} + +.town-editor-modal-content .preview-detail-title { + font-size: 24px; + font-weight: 700; + margin: 0 0 14px; + line-height: 1.2; +} + +.town-editor-modal-content .preview-detail-body { + padding: 22px 24px 24px; +} + +.town-editor-modal-content .town-preview-banner { + height: 156px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + background-size: cover; + background-position: center; + background-color: #e8ecf1; +} + +.town-editor-modal-content .town-preview-banner.no-logo { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.town-editor-modal-content .town-preview-banner .town-banner-placeholder { + font-size: 60px; + color: rgba(255,255,255,0.56); + position: relative; + z-index: 1; +} + +.town-editor-modal-content .preview-section { + margin-top: 22px; +} + +.town-editor-modal-content .preview-section:first-child { + margin-top: 0; +} + +.town-editor-modal-content .preview-section-title { + font-size: 14px; + font-weight: 700; + margin-bottom: 12px; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 10px; + border-left: 4px solid var(--accent-color); + padding-left: 12px; +} + +.town-editor-modal-content .preview-inline-text { + font-size: 14px; + color: var(--text-primary); + line-height: 1.7; +} + +.town-editor-modal-content .preview-badges { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.town-editor-modal-content .contributors-list { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.town-editor-modal-content .contributor-tag { + display: flex; + align-items: center; + background: #ffffff; + border: 1px solid #eee; + padding: 6px 14px; + border-radius: 30px; + font-size: 14px; + color: var(--text-primary); + box-shadow: 0 2px 4px rgba(0,0,0,0.02); +} + +.town-editor-modal-content .contributor-tag img { + width: 24px; + height: 24px; + border-radius: 50%; + margin-right: 10px; + background: #eee; +} + +.town-editor-modal-content .instruction-content { + background: #f9f9fa; + padding: 20px; + border-radius: 16px; + border: 1px solid rgba(0,0,0,0.03); +} + +.town-editor-modal-content .instruction-content p { + font-size: 14px; + margin-bottom: 12px; + color: var(--text-primary); + line-height: 1.7; +} + +.town-editor-modal-content .instruction-content p:last-child { + margin-bottom: 0; +} + +.town-editor-modal-content .instruction-content img { + max-width: 100%; + border-radius: 12px; + margin: 12px 0 16px; + border: 1px solid rgba(0,0,0,0.05); + box-shadow: 0 4px 12px rgba(0,0,0,0.05); +} + +.town-editor-modal-content .video-embed-wrapper { + position: relative; + width: 100%; + padding-bottom: 56.25%; + margin: 12px 0 16px; + border-radius: 12px; + overflow: hidden; + background: #000; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} + +.town-editor-modal-content .video-embed-wrapper iframe { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border: none; +} + +.town-editor-modal-content .preview-header { + padding: 24px 24px 16px; + background: linear-gradient(to bottom, #fff, #fafafa); + border-bottom: 1px solid rgba(0,0,0,0.05); +} + +.town-editor-modal-content .preview-title { + font-size: 22px; + font-weight: 700; + margin-bottom: 12px; + line-height: 1.2; +} + +.town-editor-modal-content .preview-body { + padding: 20px 24px 24px; +} + +.town-editor-modal-content .preview-body .modal-section { + margin-top: 20px; +} + +.town-editor-modal-content .preview-body .modal-section-title { + font-size: 14px; +} + +.town-editor-modal-content .preview-intro { + font-size: 15px; + line-height: 1.6; + color: var(--text-primary); + margin-bottom: 20px; +} + +.town-editor-modal-content .text-secondary { + color: var(--text-secondary); +} + +/* Editor Form */ +.town-editor-modal-content .editor-form { + flex: 0 0 55%; + display: flex; + flex-direction: column; + min-width: 0; +} + +.town-editor-modal-content .editor-form-scroll { + flex: 1; + overflow-y: auto; + padding: 24px 28px 40px; +} + +.town-editor-modal-content .form-group { + margin-bottom: 22px; +} + +.town-editor-modal-content .form-group > label { + display: block; + font-size: 12px; + font-weight: 700; + color: var(--text-secondary); + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.town-editor-modal-content .form-group input[type="text"], +.town-editor-modal-content .form-group input[type="number"], +.town-editor-modal-content .form-group textarea { + width: 100%; + padding: 12px 16px; + border: 1.5px solid rgba(0,0,0,0.1); + border-radius: 12px; + font-size: 14px; + font-family: inherit; + background-color: #f9f9fa; + transition: all 0.2s ease; + color: var(--text-primary); + box-sizing: border-box; +} + +.town-editor-modal-content .form-group input:focus, +.town-editor-modal-content .form-group textarea:focus { + outline: none; + border-color: var(--accent-color); + background-color: #fff; + box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.1); +} + +/* Custom Select Dropdown */ +.town-editor-modal-content .custom-select { + position: relative; + width: 100%; + user-select: none; + font-size: 14px; +} + +.town-editor-modal-content .custom-select-trigger { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 12px 16px; + border: 1.5px solid rgba(0,0,0,0.1); + border-radius: 12px; + background-color: #f9f9fa; + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.town-editor-modal-content .custom-select-trigger i { + color: var(--text-secondary); + font-size: 12px; + transition: transform 0.3s ease; +} + +.town-editor-modal-content .custom-select:hover .custom-select-trigger { + background-color: #fff; + border-color: rgba(0,0,0,0.2); +} + +.town-editor-modal-content .custom-select.open .custom-select-trigger { + border-color: var(--accent-color); + background-color: #fff; + box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.1); +} + +.town-editor-modal-content .custom-select.open .custom-select-trigger i { + transform: rotate(180deg); +} + +.town-editor-modal-content .custom-select-options { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + background: #fff; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0,0,0,0.15); + border: 1px solid rgba(0,0,0,0.08); + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.2s cubic-bezier(0.25, 1, 0.5, 1); + z-index: 100; + padding: 8px; +} + +.town-editor-modal-content .custom-select.open .custom-select-options { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.town-editor-modal-content .custom-option { + padding: 10px 14px; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s, color 0.2s; + color: var(--text-primary); + margin-bottom: 2px; +} + +.town-editor-modal-content .custom-option:last-child { + margin-bottom: 0; +} + +.town-editor-modal-content .custom-option:hover { + background: #f5f5f7; +} + +.town-editor-modal-content .custom-option.selected { + background: #e0f2fe; + color: #0369a1; + font-weight: 600; +} + +.town-editor-modal-content .form-group textarea { + resize: vertical; + min-height: 60px; +} + +.town-editor-modal-content .form-row { + display: flex; + gap: 14px; +} + +.town-editor-modal-content .form-row .form-group { + flex: 1; +} + +.town-editor-modal-content .gradient-picker-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.town-editor-modal-content .color-picker-field { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border: 1.5px solid rgba(0,0,0,0.1); + border-radius: 12px; + background: #f9f9fa; +} + +.town-editor-modal-content .color-picker-field span { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.town-editor-modal-content .color-picker-field input[type="color"] { + width: 48px; + height: 36px; + padding: 0; + border: none; + border-radius: 10px; + background: transparent; + cursor: pointer; +} + +.town-editor-modal-content .color-picker-field input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +.town-editor-modal-content .color-picker-field input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 10px; +} + +.town-editor-modal-content .field-hint { + margin: 10px 0 0; + font-size: 12px; + line-height: 1.5; + color: var(--text-secondary); +} + +/* Tags Input */ +.town-editor-modal-content .tags-input-wrapper { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: 1.5px solid rgba(0,0,0,0.1); + border-radius: 12px; + background: #f9f9fa; + transition: border-color 0.2s, background 0.2s, box-shadow 0.2s; + cursor: text; +} + +.town-editor-modal-content .tags-input-wrapper:focus-within { + border-color: var(--accent-color); + background: #fff; + box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.1); +} + +.town-editor-modal-content .tags-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.town-editor-modal-content .editor-tag { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--accent-color); + color: #fff; + padding: 4px 10px; + border-radius: 14px; + font-size: 12px; + font-weight: 600; +} + +.town-editor-modal-content .editor-tag-remove { + cursor: pointer; + opacity: 0.7; + transition: 0.2s; + font-size: 10px; +} + +.town-editor-modal-content .editor-tag-remove:hover { + opacity: 1; +} + +.town-editor-modal-content .tags-input-wrapper input { + border: none !important; + background: transparent !important; + padding: 4px 0 !important; + font-size: 13px; + flex: 1; + min-width: 140px; + box-shadow: none !important; +} + +.town-editor-modal-content .tags-input-wrapper input:focus { + outline: none; +} + +/* Sortable List */ +.town-editor-modal-content .sortable-list { + min-height: 8px; + margin-bottom: 10px; +} + +.town-editor-modal-content .sortable-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + margin-bottom: 8px; + background: #fff; + border: 1.5px solid rgba(0,0,0,0.08); + border-radius: 12px; + transition: box-shadow 0.2s, border-color 0.2s, opacity 0.2s; +} + +.town-editor-modal-content .sortable-item:last-child { + margin-bottom: 0; +} + +.town-editor-modal-content .sortable-item:hover { + border-color: rgba(0,0,0,0.15); + box-shadow: 0 2px 8px rgba(0,0,0,0.04); +} + +.town-editor-modal-content .sortable-item.dragging { + opacity: 0.4; + border-color: var(--accent-color); +} + +.town-editor-modal-content .sortable-item.drag-over { + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(0, 113, 227, 0.15); +} + +.town-editor-modal-content .drag-handle { + cursor: grab; + color: var(--text-secondary); + padding: 6px 2px; + font-size: 14px; + opacity: 0.35; + transition: 0.2s; + flex-shrink: 0; +} + +.town-editor-modal-content .drag-handle:active { + cursor: grabbing; +} + +.town-editor-modal-content .sortable-item:hover .drag-handle { + opacity: 0.7; +} + +.town-editor-modal-content .item-type-badge { + font-size: 10px; + font-weight: 700; + padding: 3px 8px; + border-radius: 6px; + white-space: nowrap; + flex-shrink: 0; + margin-top: 6px; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.town-editor-modal-content .badge-text { + background: #e8f5e9; + color: #2e7d32; +} + +.town-editor-modal-content .badge-image { + background: #e3f2fd; + color: #1565c0; +} + +.town-editor-modal-content .badge-video { + background: #fce4ec; + color: #c62828; +} + +.town-editor-modal-content .sortable-item .item-content { + flex: 1; + border: 1px solid rgba(0,0,0,0.06) !important; + border-radius: 8px !important; + padding: 8px 10px !important; + font-size: 13px !important; + background: #fafafa !important; + min-height: unset; + font-family: inherit; + resize: vertical; +} + +.town-editor-modal-content .sortable-item .item-content:focus { + border-color: var(--accent-color) !important; + background: #fff !important; + box-shadow: none !important; +} + +.town-editor-modal-content .remove-item-btn { + background: none; + border: none; + color: #ccc; + cursor: pointer; + padding: 6px; + border-radius: 8px; + transition: 0.2s; + flex-shrink: 0; + margin-top: 3px; + font-size: 13px; +} + +.town-editor-modal-content .remove-item-btn:hover { + color: #ef4444; + background: #fef2f2; +} + +.town-editor-modal-content .add-item-row { + display: flex; + gap: 8px; +} + +.town-editor-modal-content .add-item-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: #f5f5f7; + border: 1.5px dashed rgba(0,0,0,0.12); + border-radius: 10px; + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + cursor: pointer; + transition: var(--transition); +} + +.town-editor-modal-content .add-item-btn:hover { + border-color: var(--accent-color); + color: var(--accent-color); + background: #f0f7ff; +} + +/* Editor Actions */ +.town-editor-modal-content .editor-actions { + margin-top: 28px; + padding-top: 20px; + border-top: 1px solid rgba(0,0,0,0.08); + display: flex; + justify-content: flex-end; +} + +.town-editor-modal-content .btn-save-town { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 28px; + background: var(--brand-green); + color: #fff; + border: none; + border-radius: 14px; + font-size: 15px; + font-weight: 700; + cursor: pointer; + transition: var(--transition); +} + +.town-editor-modal-content .btn-save-town:hover { + background: #2db84d; + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(52, 199, 89, 0.3); +} + +/* JSON Output Modal */ +.town-json-output-content { + background-color: #fff; + margin: 60px auto; + border-radius: var(--radius-large); + max-width: 640px; + width: 90%; + padding: 36px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.3); + position: relative; +} + +.town-json-output-content .close-json-modal { + position: absolute; + top: 16px; + right: 20px; + font-size: 24px; + color: var(--text-secondary); + cursor: pointer; + transition: 0.2s; + z-index: 10; + background: rgba(255,255,255,0.9); + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.town-json-output-content .close-json-modal:hover { + background: #f0f0f0; + color: var(--text-primary); +} + +.town-json-output-content h3 { + font-size: 20px; + font-weight: 700; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 10px; +} + +.town-json-output-content .json-output-hint { + font-size: 14px; + color: var(--text-secondary); + margin-bottom: 20px; + line-height: 1.5; +} + +.town-json-output-content #town-json-output { + width: 100%; + height: 300px; + padding: 16px; + border: 1.5px solid rgba(0,0,0,0.1); + border-radius: 12px; + font-family: 'Courier New', Courier, monospace; + font-size: 12px; + background: #f5f5f7; + color: var(--text-primary); + resize: vertical; + margin-bottom: 16px; +} + +.town-json-output-content #town-json-output:focus { + outline: none; + border-color: var(--accent-color); +} + +.town-json-output-content .btn-copy-json { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 24px; + background: var(--accent-color); + color: #fff; + border: none; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + width: 100%; + justify-content: center; +} + +.town-json-output-content .btn-copy-json:hover { + background: #005bb5; +} + +/* ========== Responsive ========== */ + +@media (max-width: 900px) { + .town-editor-modal-content { + margin: 0; + width: 100%; + max-width: 100%; + max-height: 100%; + height: 100%; + border-radius: 0; + } + + .town-editor-modal-content .editor-layout { + flex-direction: column; + } + + .town-editor-modal-content .editor-preview { + flex: none; + max-height: 35vh; + border-right: none; + border-bottom: 1px solid rgba(0,0,0,0.08); + } + + .town-editor-modal-content .editor-form { + flex: 1; + min-height: 0; + } + + .town-editor-modal-content .form-row { + flex-direction: column; + gap: 0; + } + + .town-editor-modal-content .gradient-picker-row { + grid-template-columns: 1fr; + } + + .town-editor-modal-content .close-editor-modal { + top: 12px; + right: 14px; + } +} + +@media (max-width: 768px) { + .towns-container .controls-header-row { + flex-direction: column; + align-items: stretch; + } + + .towns-container .search-box { + max-width: 100%; + } + + .towns-container .filter-label { + min-width: auto; + margin-bottom: 4px; + } + + .town-modal-content { + margin: 0; + width: 100%; + height: 100%; + max-height: 100%; + border-radius: 0; + } + + .town-modal-banner { + border-radius: 0; + } + + .town-modal .close-modal { + top: 15px; + right: 15px; + } + + .town-modal-body { + padding: 24px 24px 80px; + } + + .town-modal-header { + padding: 20px 24px; + } + + .towns-container .title-with-action { + flex-wrap: wrap; + gap: 10px; + } + + .town-json-output-content { + margin: 0; + width: 100%; + height: 100%; + max-height: 100%; + border-radius: 0; + } + + .town-editor-modal-content .editor-modal-header { + padding: 16px 20px; + } + + .town-editor-modal-content .editor-form-scroll { + padding: 20px; + } +} diff --git a/data/towns.json b/data/towns.json new file mode 100644 index 0000000..41b42e6 --- /dev/null +++ b/data/towns.json @@ -0,0 +1,3 @@ +[ + +] diff --git a/js/components.js b/js/components.js index 5872ae3..6291612 100644 --- a/js/components.js +++ b/js/components.js @@ -15,6 +15,7 @@ const Components = { 文档 地图 设施 + 城镇 公告 相册 数据 @@ -33,6 +34,7 @@ const Components = { 文档 地图 设施 + 城镇 公告 相册 数据 diff --git a/js/towns_script.js b/js/towns_script.js new file mode 100644 index 0000000..b46e524 --- /dev/null +++ b/js/towns_script.js @@ -0,0 +1,876 @@ +document.addEventListener('DOMContentLoaded', () => { + const DEFAULT_GRADIENT = { + from: '#667eea', + to: '#764ba2' + }; + + let townsData = []; + const grid = document.getElementById('towns-list'); + const noResults = document.getElementById('no-results'); + const scaleFilters = document.getElementById('scale-filters'); + const typeFilters = document.getElementById('type-filters'); + const recruitFilters = document.getElementById('recruit-filters'); + const searchInput = document.getElementById('town-search'); + + // Modal Elements + const modal = document.getElementById('town-modal'); + const closeModalBtn = modal.querySelector('.close-modal'); + + // Initial State + let currentFilters = { + scale: 'all', + townType: 'all', + recruitment: 'all', + search: '' + }; + + let currentDetailItem = null; + + // Generate stable anchor ID for a town + function generateTownId(item) { + var raw = (item.title || ''); + var hash = 0; + for (var i = 0; i < raw.length; i++) { + hash = ((hash << 5) - hash) + raw.charCodeAt(i); + hash |= 0; + } + return 't' + Math.abs(hash).toString(36); + } + + // Handle URL hash: auto-open town modal + function handleHashNavigation() { + var hash = location.hash.replace('#', ''); + if (!hash) return; + for (var i = 0; i < townsData.length; i++) { + if (generateTownId(townsData[i]) === hash) { + openModal(townsData[i]); + return; + } + } + } + + // 1. Fetch Data + fetch('data/towns.json') + .then(response => response.json()) + .then(data => { + townsData = data; + renderGrid(); + handleHashNavigation(); + }) + .catch(err => { + console.error('Error loading towns:', err); + grid.innerHTML = '

无法加载城镇数据。

'; + }); + + // 2. Event Listeners + + // Scale Filter + scaleFilters.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON') { + Array.from(scaleFilters.children).forEach(btn => btn.classList.remove('active')); + e.target.classList.add('active'); + currentFilters.scale = e.target.dataset.filter; + renderGrid(); + } + }); + + // Type Filter + typeFilters.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON') { + Array.from(typeFilters.children).forEach(btn => btn.classList.remove('active')); + e.target.classList.add('active'); + currentFilters.townType = e.target.dataset.filter; + renderGrid(); + } + }); + + // Recruit Filter + recruitFilters.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON') { + Array.from(recruitFilters.children).forEach(btn => btn.classList.remove('active')); + e.target.classList.add('active'); + currentFilters.recruitment = e.target.dataset.filter; + renderGrid(); + } + }); + + // Search + searchInput.addEventListener('input', (e) => { + currentFilters.search = e.target.value.toLowerCase().trim(); + renderGrid(); + }); + + // Modal Close + closeModalBtn.addEventListener('click', () => { + modal.style.display = 'none'; + document.body.style.overflow = 'auto'; + history.replaceState(null, '', location.pathname + location.search); + }); + + window.addEventListener('click', (e) => { + if (e.target === modal) { + modal.style.display = 'none'; + document.body.style.overflow = 'auto'; + history.replaceState(null, '', location.pathname + location.search); + } + }); + + // 3. Render Functions + function renderGrid() { + grid.innerHTML = ''; + + const filtered = townsData.filter(item => { + const matchScale = currentFilters.scale === 'all' || item.scale === currentFilters.scale; + const matchType = currentFilters.townType === 'all' || item.townType === currentFilters.townType; + const matchRecruit = currentFilters.recruitment === 'all' || item.recruitment === currentFilters.recruitment; + const matchSearch = !currentFilters.search || + item.title.toLowerCase().includes(currentFilters.search); + return matchScale && matchType && matchRecruit && matchSearch; + }); + + if (filtered.length === 0) { + noResults.classList.remove('is-hidden'); + return; + } else { + noResults.classList.add('is-hidden'); + } + + filtered.forEach(item => { + const card = document.createElement('div'); + card.className = 'town-card'; + card.onclick = () => openModal(item); + + const hasLogo = item.logo && item.logo.trim() !== ''; + const gradient = getTownGradient(item); + + // Build card icon badges (scale + type + recruitment) + let iconsHtml = ''; + iconsHtml += '
'; + iconsHtml += ''; + iconsHtml += ''; + iconsHtml += ''; + iconsHtml += '
'; + + card.innerHTML = + '' + + '
' + + '

' + escapeHtml(item.title) + '

' + + '
' + + ' ' + getScaleText(item.scale) + '' + + ' ' + getTownTypeText(item.townType) + '' + + ' ' + getRecruitText(item.recruitment) + '' + + '
' + + '
'; + + grid.appendChild(card); + }); + } + + function openModal(item) { + currentDetailItem = item; + + // Banner + var banner = document.getElementById('town-modal-banner'); + var hasLogo = item.logo && item.logo.trim() !== ''; + var gradient = getTownGradient(item); + banner.className = 'town-modal-banner' + (hasLogo ? '' : ' no-logo'); + if (hasLogo) { + banner.style.backgroundImage = "url('" + item.logo + "')"; + banner.style.background = ''; + banner.innerHTML = ''; + } else { + banner.style.backgroundImage = ''; + banner.style.background = buildGradientBackgroundValue(gradient); + banner.innerHTML = ''; + } + + // Title + document.getElementById('town-modal-title').innerText = item.title; + + // Badges + var badgesContainer = document.getElementById('town-modal-badges'); + badgesContainer.innerHTML = ''; + + var scaleBadge = document.createElement('span'); + scaleBadge.className = 'town-badge badge-scale-' + item.scale; + scaleBadge.innerHTML = ' ' + getScaleText(item.scale); + badgesContainer.appendChild(scaleBadge); + + var typeBadge = document.createElement('span'); + typeBadge.className = 'town-badge badge-type-' + item.townType; + typeBadge.innerHTML = ' ' + getTownTypeText(item.townType); + badgesContainer.appendChild(typeBadge); + + var recruitBadge = document.createElement('span'); + recruitBadge.className = 'town-badge badge-recruit-' + item.recruitment; + recruitBadge.innerHTML = ' ' + getRecruitText(item.recruitment); + badgesContainer.appendChild(recruitBadge); + + // Coordinates + var coords = item.coordinates; + document.getElementById('town-modal-coords').innerText = 'X: ' + coords.x + ', Y: ' + coords.y + ', Z: ' + coords.z; + + // Map Link + var mapLink = document.getElementById('town-modal-map-link'); + mapLink.href = 'https://mcmap.lunadeer.cn/#world:' + coords.x + ':' + coords.y + ':' + coords.z + ':500:0:0:0:1:flat'; + + // Founders + var foundersContainer = document.getElementById('town-modal-founders'); + foundersContainer.innerHTML = ''; + if (item.founders && item.founders.length > 0) { + item.founders.forEach(function(name) { + var tag = document.createElement('div'); + tag.className = 'contributor-tag'; + tag.innerHTML = '' + escapeHtml(name) + '' + escapeHtml(name); + foundersContainer.appendChild(tag); + }); + } else { + foundersContainer.innerHTML = '暂无记录'; + } + + // Members + var membersContainer = document.getElementById('town-modal-members'); + membersContainer.innerHTML = ''; + if (item.members && item.members.length > 0) { + item.members.forEach(function(name) { + var tag = document.createElement('div'); + tag.className = 'contributor-tag'; + tag.innerHTML = '' + escapeHtml(name) + '' + escapeHtml(name); + membersContainer.appendChild(tag); + }); + } else { + membersContainer.innerHTML = '暂无记录'; + } + + // Introduction + renderContentList(document.getElementById('town-modal-introduction'), item.introduction); + + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Update URL hash + var anchorId = generateTownId(item); + history.replaceState(null, '', '#' + anchorId); + } + + function renderContentList(container, list) { + container.innerHTML = ''; + if (!list || list.length === 0) { + container.innerHTML = '

'; + return; + } + list.forEach(function(block) { + if (block.type === 'text') { + var p = document.createElement('p'); + p.innerText = block.content; + container.appendChild(p); + } else if (block.type === 'image') { + var img = document.createElement('img'); + img.src = block.content; + img.loading = 'lazy'; + container.appendChild(img); + } else if (block.type === 'video') { + var bv = parseBVNumber(block.content); + if (bv) { + var wrapper = document.createElement('div'); + wrapper.className = 'video-embed-wrapper'; + var iframe = document.createElement('iframe'); + iframe.src = 'https://player.bilibili.com/player.html?bvid=' + bv + '&autoplay=0&high_quality=1'; + iframe.allowFullscreen = true; + iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups'); + iframe.loading = 'lazy'; + wrapper.appendChild(iframe); + container.appendChild(wrapper); + } else { + var p = document.createElement('p'); + p.className = 'text-secondary'; + p.innerText = '无效的视频 BV 号'; + container.appendChild(p); + } + } + }); + } + + function parseBVNumber(input) { + if (!input) return null; + input = input.trim(); + var bvPattern = /^(BV[A-Za-z0-9]+)$/; + var directMatch = input.match(bvPattern); + if (directMatch) return directMatch[1]; + var urlPattern = /bilibili\.com\/video\/(BV[A-Za-z0-9]+)/; + var urlMatch = input.match(urlPattern); + if (urlMatch) return urlMatch[1]; + var generalPattern = /(BV[A-Za-z0-9]{10,})/; + var generalMatch = input.match(generalPattern); + if (generalMatch) return generalMatch[1]; + return null; + } + + // Helpers + function getScaleText(scale) { + var map = { 'small': '小型(5人以下)', 'medium': '中型(2-10人)', 'large': '大型(10人以上)' }; + return map[scale] || scale; + } + + function getScaleIcon(scale) { + var map = { 'small': 'fa-user', 'medium': 'fa-users', 'large': 'fa-city' }; + return map[scale] || 'fa-users'; + } + + function getTownTypeText(type) { + var map = { 'building': '建筑', 'adventure': '冒险', 'industry': '工业' }; + return map[type] || type; + } + + function getTownTypeIcon(type) { + var map = { 'building': 'fa-building', 'adventure': 'fa-dragon', 'industry': 'fa-industry' }; + return map[type] || 'fa-building'; + } + + function getRecruitText(recruitment) { + var map = { 'welcome': '欢迎加入', 'closed': '暂不招人', 'maybe': '可以考虑' }; + return map[recruitment] || recruitment; + } + + function getRecruitIcon(recruitment) { + var map = { 'welcome': 'fa-door-open', 'closed': 'fa-door-closed', 'maybe': 'fa-question-circle' }; + return map[recruitment] || 'fa-info-circle'; + } + + function normalizeHexColor(value, fallback) { + if (!value || typeof value !== 'string') return fallback; + var trimmed = value.trim(); + if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed; + return fallback; + } + + function getTownGradient(item) { + var gradient = item && item.gradient ? item.gradient : {}; + return { + from: normalizeHexColor(gradient.from, DEFAULT_GRADIENT.from), + to: normalizeHexColor(gradient.to, DEFAULT_GRADIENT.to) + }; + } + + function buildGradientBackgroundValue(gradient) { + return 'linear-gradient(135deg, ' + gradient.from + ' 0%, ' + gradient.to + ' 100%)'; + } + + function buildGradientBackgroundStyle(gradient) { + return 'background:' + buildGradientBackgroundValue(gradient) + ';'; + } + + // Share town link + document.getElementById('btn-share-town').addEventListener('click', function() { + if (!currentDetailItem) return; + var anchorId = generateTownId(currentDetailItem); + var url = location.origin + location.pathname + '#' + anchorId; + var btn = document.getElementById('btn-share-town'); + navigator.clipboard.writeText(url).then(function() { + btn.innerHTML = ' 已复制链接'; + btn.classList.add('shared'); + setTimeout(function() { + btn.innerHTML = ' 分享'; + btn.classList.remove('shared'); + }, 2000); + }).catch(function() { + var tmp = document.createElement('input'); + tmp.value = url; + document.body.appendChild(tmp); + tmp.select(); + document.execCommand('copy'); + document.body.removeChild(tmp); + btn.innerHTML = ' 已复制链接'; + setTimeout(function() { + btn.innerHTML = ' 分享'; + }, 2000); + }); + }); + + // Open editor from detail modal + document.getElementById('btn-edit-town').addEventListener('click', function() { + if (currentDetailItem) { + modal.style.display = 'none'; + document.body.style.overflow = 'auto'; + openEditor(currentDetailItem); + } + }); + + // ========== Editor Modal Logic ========== + + var editorModal = document.getElementById('town-editor-modal'); + var jsonOutputModal = document.getElementById('town-json-output-modal'); + var closeEditorModalBtn = editorModal.querySelector('.close-editor-modal'); + var closeJsonModalBtn = jsonOutputModal.querySelector('.close-json-modal'); + + // Open empty editor for new town + document.getElementById('btn-add-town').addEventListener('click', function() { + openEditor(null); + }); + + // Close editor modal + closeEditorModalBtn.addEventListener('click', function() { + editorModal.style.display = 'none'; + document.body.style.overflow = 'auto'; + }); + window.addEventListener('click', function(e) { + if (e.target === editorModal) { + editorModal.style.display = 'none'; + document.body.style.overflow = 'auto'; + } + if (e.target === jsonOutputModal) { + jsonOutputModal.style.display = 'none'; + } + }); + closeJsonModalBtn.addEventListener('click', function() { + jsonOutputModal.style.display = 'none'; + }); + + // State for editor + var editorFounders = []; + var editorMembers = []; + var editorIntroduction = []; + + // Initialize custom selects + editorModal.querySelectorAll('.custom-select').forEach(function(select) { + var trigger = select.querySelector('.custom-select-trigger'); + var options = select.querySelectorAll('.custom-option'); + var input = select.querySelector('input[type="hidden"]'); + var text = select.querySelector('.custom-select-text'); + + trigger.addEventListener('click', function(e) { + e.stopPropagation(); + var isOpen = select.classList.contains('open'); + editorModal.querySelectorAll('.custom-select').forEach(function(s) { s.classList.remove('open'); }); + if (!isOpen) { + select.classList.add('open'); + } + }); + + options.forEach(function(option) { + option.addEventListener('click', function(e) { + e.stopPropagation(); + options.forEach(function(opt) { opt.classList.remove('selected'); }); + option.classList.add('selected'); + text.innerText = option.innerText; + input.value = option.dataset.value; + input.dispatchEvent(new Event('change')); + select.classList.remove('open'); + }); + }); + }); + + document.addEventListener('click', function() { + editorModal.querySelectorAll('.custom-select').forEach(function(s) { s.classList.remove('open'); }); + }); + + function setCustomSelectValue(id, value) { + var input = document.getElementById(id); + if (!input) return; + var select = input.closest('.custom-select'); + var option = select.querySelector('.custom-option[data-value="' + value + '"]'); + if (option) { + input.value = value; + select.querySelector('.custom-select-text').innerText = option.innerText; + select.querySelectorAll('.custom-option').forEach(function(opt) { opt.classList.remove('selected'); }); + option.classList.add('selected'); + } + } + + function openEditor(item) { + var gradient = getTownGradient(item || {}); + editorFounders = item ? item.founders.slice() : []; + editorMembers = item ? item.members.slice() : []; + editorIntroduction = item ? item.introduction.map(function(i) { return {type: i.type, content: i.content}; }) : []; + + document.getElementById('editor-town-title').value = item ? item.title : ''; + document.getElementById('editor-town-logo').value = item ? (item.logo || '') : ''; + document.getElementById('editor-town-gradient-from').value = gradient.from; + document.getElementById('editor-town-gradient-to').value = gradient.to; + + setCustomSelectValue('editor-town-scale', item ? item.scale : 'small'); + setCustomSelectValue('editor-town-type', item ? item.townType : 'building'); + setCustomSelectValue('editor-town-recruit', item ? item.recruitment : 'welcome'); + + document.getElementById('editor-town-x').value = item ? item.coordinates.x : ''; + document.getElementById('editor-town-y').value = item ? item.coordinates.y : ''; + document.getElementById('editor-town-z').value = item ? item.coordinates.z : ''; + + renderTagsList('editor-founders-tags', editorFounders); + renderTagsList('editor-members-tags', editorMembers); + renderSortableList('editor-introduction-list', editorIntroduction); + updatePreview(); + + editorModal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + } + + // --- Tags input helpers --- + function renderTagsList(containerId, list) { + var container = document.getElementById(containerId); + container.innerHTML = ''; + list.forEach(function(name, idx) { + var tag = document.createElement('span'); + tag.className = 'editor-tag'; + tag.innerHTML = escapeHtml(name) + ' '; + container.appendChild(tag); + }); + } + + function commitTagInput(inputId, list, tagsContainerId) { + var input = document.getElementById(inputId); + var value = input.value.trim(); + if (value && list.indexOf(value) === -1) { + list.push(value); + renderTagsList(tagsContainerId, list); + updatePreview(); + } + input.value = ''; + } + + // Founders tags + document.getElementById('editor-founders-tags').addEventListener('click', function(e) { + var removeBtn = e.target.closest('.editor-tag-remove'); + if (removeBtn) { + var idx = parseInt(removeBtn.dataset.idx); + editorFounders.splice(idx, 1); + renderTagsList('editor-founders-tags', editorFounders); + updatePreview(); + } + }); + + document.getElementById('editor-founder-input').addEventListener('keydown', function(e) { + if (e.isComposing) return; + if (e.key === 'Enter' || e.key === ' ' || e.code === 'Space') { + e.preventDefault(); + commitTagInput('editor-founder-input', editorFounders, 'editor-founders-tags'); + } + }); + document.getElementById('editor-founder-input').addEventListener('blur', function() { + commitTagInput('editor-founder-input', editorFounders, 'editor-founders-tags'); + }); + document.getElementById('editor-founders-wrapper').addEventListener('click', function() { + document.getElementById('editor-founder-input').focus(); + }); + + // Members tags + document.getElementById('editor-members-tags').addEventListener('click', function(e) { + var removeBtn = e.target.closest('.editor-tag-remove'); + if (removeBtn) { + var idx = parseInt(removeBtn.dataset.idx); + editorMembers.splice(idx, 1); + renderTagsList('editor-members-tags', editorMembers); + updatePreview(); + } + }); + + document.getElementById('editor-member-input').addEventListener('keydown', function(e) { + if (e.isComposing) return; + if (e.key === 'Enter' || e.key === ' ' || e.code === 'Space') { + e.preventDefault(); + commitTagInput('editor-member-input', editorMembers, 'editor-members-tags'); + } + }); + document.getElementById('editor-member-input').addEventListener('blur', function() { + commitTagInput('editor-member-input', editorMembers, 'editor-members-tags'); + }); + document.getElementById('editor-members-wrapper').addEventListener('click', function() { + document.getElementById('editor-member-input').focus(); + }); + + // --- Sortable Lists (drag-and-drop) --- + var dragState = { listId: null, fromIdx: null }; + + function renderSortableList(listId, items) { + var container = document.getElementById(listId); + container.innerHTML = ''; + items.forEach(function(item, idx) { + var div = document.createElement('div'); + div.className = 'sortable-item'; + div.draggable = true; + div.dataset.idx = idx; + div.dataset.listId = listId; + + var typeBadgeClass = item.type === 'text' ? 'badge-text' : item.type === 'image' ? 'badge-image' : 'badge-video'; + var typeBadgeLabel = item.type === 'text' ? '文字' : item.type === 'image' ? '图片' : '视频'; + var contentHtml; + if (item.type === 'text') { + contentHtml = ''; + } else if (item.type === 'image') { + contentHtml = ''; + } else { + contentHtml = ''; + } + div.innerHTML = + '' + + '' + typeBadgeLabel + '' + + contentHtml + + ''; + container.appendChild(div); + + div.addEventListener('dragstart', onDragStart); + div.addEventListener('dragover', onDragOver); + div.addEventListener('dragenter', onDragEnter); + div.addEventListener('dragleave', onDragLeave); + div.addEventListener('drop', onDrop); + div.addEventListener('dragend', onDragEnd); + + var contentEl = div.querySelector('.item-content'); + contentEl.addEventListener('input', function() { + items[idx].content = contentEl.value; + updatePreview(); + }); + + div.querySelector('.remove-item-btn').addEventListener('click', function() { + items.splice(idx, 1); + renderSortableList(listId, items); + updatePreview(); + }); + }); + } + + function onDragStart(e) { + var item = e.target.closest('.sortable-item'); + if (!item) return; + dragState.listId = item.dataset.listId; + dragState.fromIdx = parseInt(item.dataset.idx); + item.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', ''); + } + + function onDragOver(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + } + + function onDragEnter(e) { + var item = e.target.closest('.sortable-item'); + if (item && item.dataset.listId === dragState.listId) { + item.classList.add('drag-over'); + } + } + + function onDragLeave(e) { + var item = e.target.closest('.sortable-item'); + if (item) { + item.classList.remove('drag-over'); + } + } + + function onDrop(e) { + e.preventDefault(); + var item = e.target.closest('.sortable-item'); + if (!item || item.dataset.listId !== dragState.listId) return; + var toIdx = parseInt(item.dataset.idx); + var fromIdx = dragState.fromIdx; + if (fromIdx === toIdx) return; + + var listId = dragState.listId; + var items = editorIntroduction; + + var moved = items.splice(fromIdx, 1)[0]; + items.splice(toIdx, 0, moved); + + renderSortableList(listId, items); + updatePreview(); + } + + function onDragEnd() { + document.querySelectorAll('.sortable-item').forEach(function(el) { + el.classList.remove('dragging', 'drag-over'); + }); + dragState = { listId: null, fromIdx: null }; + } + + // --- Add item buttons --- + editorModal.querySelectorAll('.add-item-btn').forEach(function(btn) { + btn.addEventListener('click', function() { + var type = btn.dataset.type; + var newItem = { type: type, content: '' }; + editorIntroduction.push(newItem); + renderSortableList('editor-introduction-list', editorIntroduction); + updatePreview(); + }); + }); + + // --- Live Preview --- + ['editor-town-title', 'editor-town-logo', 'editor-town-scale', 'editor-town-type', + 'editor-town-recruit', 'editor-town-x', 'editor-town-y', 'editor-town-z'].forEach(function(id) { + var el = document.getElementById(id); + el.addEventListener('input', updatePreview); + el.addEventListener('change', updatePreview); + }); + + ['editor-town-gradient-from', 'editor-town-gradient-to'].forEach(function(id) { + var el = document.getElementById(id); + el.addEventListener('input', updatePreview); + el.addEventListener('change', updatePreview); + }); + + function updatePreview() { + var preview = document.getElementById('town-editor-preview-area'); + var title = document.getElementById('editor-town-title').value || '未命名城镇'; + var logo = document.getElementById('editor-town-logo').value.trim(); + var scale = document.getElementById('editor-town-scale').value; + var townType = document.getElementById('editor-town-type').value; + var recruit = document.getElementById('editor-town-recruit').value; + var x = document.getElementById('editor-town-x').value || '0'; + var y = document.getElementById('editor-town-y').value || '64'; + var z = document.getElementById('editor-town-z').value || '0'; + var gradient = { + from: normalizeHexColor(document.getElementById('editor-town-gradient-from').value, DEFAULT_GRADIENT.from), + to: normalizeHexColor(document.getElementById('editor-town-gradient-to').value, DEFAULT_GRADIENT.to) + }; + var hasLogo = logo !== ''; + + var html = '
'; + + html += '
'; + html += ''; + html += '
'; + html += '

' + escapeHtml(title) + '

'; + html += '
'; + html += ' ' + getScaleText(scale) + ''; + html += ' ' + getTownTypeText(townType) + ''; + html += ' ' + getRecruitText(recruit) + ''; + html += '
'; + html += '
'; + html += '
'; + + html += '
'; + html += '

位置信息

'; + html += '

主世界: X: ' + escapeHtml(x) + ', Y: ' + escapeHtml(y) + ', Z: ' + escapeHtml(z) + '

'; + html += '
'; + + html += '
'; + html += '

创始人

'; + if (editorFounders.length > 0) { + html += '
'; + editorFounders.forEach(function(name) { + html += '
' + escapeHtml(name) + '' + escapeHtml(name) + '
'; + }); + html += '
'; + } else { + html += '暂无记录'; + } + html += '
'; + + html += '
'; + html += '

主要成员

'; + if (editorMembers.length > 0) { + html += '
'; + editorMembers.forEach(function(name) { + html += '
' + escapeHtml(name) + '' + escapeHtml(name) + '
'; + }); + html += '
'; + } else { + html += '暂无记录'; + } + html += '
'; + + html += '
'; + html += '

城镇介绍

'; + html += '
'; + if (editorIntroduction.length > 0) { + editorIntroduction.forEach(function(block) { + if (block.type === 'text') { + html += '

' + (escapeHtml(block.content) || '空文字') + '

'; + } else if (block.type === 'image') { + html += block.content ? '' : '

空图片

'; + } else if (block.type === 'video') { + html += renderVideoPreviewHtml(block.content); + } + }); + } else { + html += '

'; + } + html += '
'; + + html += '
'; + html += '
'; + preview.innerHTML = html; + } + + // --- Save / Generate JSON --- + document.getElementById('btn-save-town').addEventListener('click', function() { + var title = document.getElementById('editor-town-title').value.trim(); + if (!title) { + alert('请填写城镇名称'); + document.getElementById('editor-town-title').focus(); + return; + } + + var townObj = { + title: title, + logo: document.getElementById('editor-town-logo').value.trim(), + gradient: { + from: normalizeHexColor(document.getElementById('editor-town-gradient-from').value, DEFAULT_GRADIENT.from), + to: normalizeHexColor(document.getElementById('editor-town-gradient-to').value, DEFAULT_GRADIENT.to) + }, + coordinates: { + x: parseInt(document.getElementById('editor-town-x').value) || 0, + y: parseInt(document.getElementById('editor-town-y').value) || 64, + z: parseInt(document.getElementById('editor-town-z').value) || 0 + }, + scale: document.getElementById('editor-town-scale').value, + townType: document.getElementById('editor-town-type').value, + recruitment: document.getElementById('editor-town-recruit').value, + founders: editorFounders.slice(), + members: editorMembers.slice(), + introduction: editorIntroduction.filter(function(i) { return i.content.trim() !== ''; }).map(function(i) { + return i.type === 'video' ? { type: 'video', content: parseBVNumber(i.content) || i.content } : { type: i.type, content: i.content }; + }) + }; + + var jsonStr = JSON.stringify(townObj, null, 4); + document.getElementById('town-json-output').value = jsonStr; + jsonOutputModal.style.display = 'block'; + }); + + // --- Copy JSON --- + document.getElementById('btn-copy-town-json').addEventListener('click', function() { + var textArea = document.getElementById('town-json-output'); + textArea.select(); + textArea.setSelectionRange(0, 99999); + + navigator.clipboard.writeText(textArea.value).then(function() { + var btn = document.getElementById('btn-copy-town-json'); + var originalHTML = btn.innerHTML; + btn.innerHTML = ' 已复制!'; + btn.style.background = '#34c759'; + setTimeout(function() { + btn.innerHTML = originalHTML; + btn.style.background = ''; + }, 2000); + }).catch(function() { + document.execCommand('copy'); + alert('已复制到剪贴板'); + }); + }); + + function renderVideoPreviewHtml(content) { + var bv = parseBVNumber(content); + if (bv) { + return '
'; + } + return '

请输入有效的 BV 号或 bilibili 视频地址

'; + } + + // --- Utility --- + function escapeHtml(text) { + if (!text) return ''; + var div = document.createElement('div'); + div.appendChild(document.createTextNode(text)); + return div.innerHTML; + } +}); diff --git a/towns.html b/towns.html new file mode 100644 index 0000000..3861753 --- /dev/null +++ b/towns.html @@ -0,0 +1,335 @@ + + + + + + 城镇介绍 - 白鹿原 Minecraft 服务器 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+
+

城镇列表

+ +
+ +
+ +
+
+
规模
+
+ + + + +
+
+ +
+
类型
+
+ + + + +
+
+ +
+
招募
+
+ + + + +
+
+
+
+ + +
+ +
+ + +
+ + + + + + + + + + + + + + +