mirror of
https://github.com/Coldsmiles/infstarweb.git
synced 2026-04-23 02:30:41 +08:00
feat: enhance ModalSection and FacilitiesPage with icons and improved layout
This commit is contained in:
@@ -4,6 +4,10 @@ defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -13,10 +17,10 @@ defineProps({
|
||||
|
||||
<template>
|
||||
<section class="modal-section">
|
||||
<div class="modal-section__head">
|
||||
<h4>{{ title }}</h4>
|
||||
<p v-if="subtitle">{{ subtitle }}</p>
|
||||
</div>
|
||||
<h4 class="modal-section-title">
|
||||
<i v-if="icon" :class="icon"></i>
|
||||
{{ title }}
|
||||
</h4>
|
||||
<div class="modal-section__body">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -25,23 +29,28 @@ defineProps({
|
||||
|
||||
<style scoped>
|
||||
.modal-section {
|
||||
padding: 18px 20px;
|
||||
border-radius: 18px;
|
||||
background: rgba(245, 245, 247, 0.8);
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.modal-section__head h4,
|
||||
.modal-section__head p {
|
||||
margin: 0;
|
||||
.modal-section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px;
|
||||
color: var(--bl-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-left: 4px solid var(--bl-accent, #0071e3);
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.modal-section__head p {
|
||||
margin-top: 6px;
|
||||
color: var(--bl-text-secondary);
|
||||
font-size: 0.85rem;
|
||||
.modal-section-title i {
|
||||
color: var(--bl-accent, #0071e3);
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-section__body {
|
||||
margin-top: 14px;
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,88 +1,50 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
brand: {
|
||||
type: String,
|
||||
default: '白鹿原',
|
||||
},
|
||||
year: {
|
||||
type: Number,
|
||||
default: new Date().getFullYear(),
|
||||
},
|
||||
});
|
||||
|
||||
const footerLinks = [
|
||||
{ label: '文档', href: '/doc' },
|
||||
{ label: '地图', href: '/map' },
|
||||
{ label: '赞助', href: '/sponsor' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="site-footer">
|
||||
<div class="site-footer__inner bl-shell">
|
||||
<div>
|
||||
<p class="site-footer__brand">{{ brand }}</p>
|
||||
<p class="site-footer__copy">© {{ year }} {{ brand }} Minecraft 服务器</p>
|
||||
<div class="site-footer__inner">
|
||||
<div class="footer-content">
|
||||
<div class="footer-logo">{{ brand }}</div>
|
||||
<p>© 2026 {{ brand }} Minecraft 服务器.</p>
|
||||
</div>
|
||||
<nav class="site-footer__links" aria-label="页脚导航">
|
||||
<RouterLink v-for="link in footerLinks" :key="link.href" :to="link.href">{{ link.label }}</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.site-footer {
|
||||
margin-top: 80px;
|
||||
padding: 28px 0 36px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
backdrop-filter: blur(16px);
|
||||
background: var(--bl-bg, #f5f5f7);
|
||||
padding: 40px 0;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
font-size: 12px;
|
||||
color: var(--bl-text-secondary);
|
||||
}
|
||||
|
||||
.site-footer__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
max-width: var(--bl-content-width, 1200px);
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.site-footer__brand,
|
||||
.site-footer__copy {
|
||||
margin: 0;
|
||||
.footer-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.site-footer__brand {
|
||||
.footer-logo {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.site-footer__copy {
|
||||
margin-top: 6px;
|
||||
color: var(--bl-text-secondary);
|
||||
}
|
||||
|
||||
.site-footer__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.site-footer__links a {
|
||||
color: var(--bl-text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-footer__links a:hover {
|
||||
margin-bottom: 6px;
|
||||
color: var(--bl-text);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.site-footer__inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.footer-content p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -53,7 +53,7 @@ function onOverlayClick(e) {
|
||||
.editor-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
z-index: 2000;
|
||||
background: rgba(0,0,0,0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { ref, computed, onMounted, nextTick } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import FilterPanel from '../components/shared/FilterPanel.vue';
|
||||
import BaseBadge from '../components/base/BaseBadge.vue';
|
||||
import BaseModal from '../components/base/BaseModal.vue';
|
||||
import ModalSection from '../components/detail/ModalSection.vue';
|
||||
import EmptyState from '../components/base/EmptyState.vue';
|
||||
@@ -84,6 +83,7 @@ const typeTextMap = { resource: '资源', xp: '经验', infrastructure: '基建'
|
||||
const dimensionTextMap = { overworld: '主世界', nether: '下界', end: '末地' };
|
||||
const statusTextMap = { online: '运行中', maintenance: '维护中', offline: '已停用' };
|
||||
const statusToneMap = { online: 'success', maintenance: 'warning', offline: 'danger' };
|
||||
const statusIconMap = { online: 'fa-check-circle', maintenance: 'fa-wrench', offline: 'fa-times-circle' };
|
||||
|
||||
const filtered = computed(() => {
|
||||
return facilities.value.filter(item => {
|
||||
@@ -306,9 +306,10 @@ function generateJson() {
|
||||
>
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ item.title }}</h3>
|
||||
<BaseBadge :tone="statusToneMap[item.status] || 'neutral'">
|
||||
{{ statusTextMap[item.status] || item.status }}
|
||||
</BaseBadge>
|
||||
<div :class="['status-indicator-badge', 'status-' + item.status]">
|
||||
<div class="status-dot"></div>
|
||||
<span>{{ statusTextMap[item.status] || item.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-intro">{{ item.intro }}</p>
|
||||
<div class="card-meta">
|
||||
@@ -329,9 +330,11 @@ function generateJson() {
|
||||
<div class="modal-badges-row">
|
||||
<div class="modal-badges">
|
||||
<span :class="['badge', 'large-badge', 'badge-status-' + selectedFacility.status]">
|
||||
<i class="fas" :class="statusIconMap[selectedFacility.status]"></i>
|
||||
{{ statusTextMap[selectedFacility.status] }}
|
||||
</span>
|
||||
<span class="badge large-badge badge-type">
|
||||
<i class="fas fa-cube"></i>
|
||||
{{ typeTextMap[selectedFacility.type] }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -352,7 +355,7 @@ function generateJson() {
|
||||
</template>
|
||||
|
||||
<template v-if="selectedFacility">
|
||||
<ModalSection title="位置信息">
|
||||
<ModalSection title="位置信息" icon="fas fa-map-marker-alt">
|
||||
<p>
|
||||
{{ dimensionTextMap[selectedFacility.dimension] }}
|
||||
<template v-if="selectedFacility.coordinates">
|
||||
@@ -370,7 +373,7 @@ function generateJson() {
|
||||
</p>
|
||||
</ModalSection>
|
||||
|
||||
<ModalSection v-if="selectedFacility.contributors?.length" title="贡献 / 维护人员">
|
||||
<ModalSection v-if="selectedFacility.contributors?.length" title="贡献 / 维护人员" icon="fas fa-users-cog">
|
||||
<div class="contributors-list">
|
||||
<span v-for="name in selectedFacility.contributors" :key="name" class="contributor-tag">
|
||||
<img :src="`https://minotar.net/avatar/${name}/20`" :alt="name" loading="lazy">
|
||||
@@ -379,7 +382,7 @@ function generateJson() {
|
||||
</div>
|
||||
</ModalSection>
|
||||
|
||||
<ModalSection v-if="selectedFacility.instructions?.length" title="使用说明">
|
||||
<ModalSection v-if="selectedFacility.instructions?.length" title="使用说明" icon="fas fa-book-open">
|
||||
<div class="content-blocks">
|
||||
<template v-for="(block, bi) in selectedFacility.instructions" :key="bi">
|
||||
<p v-if="block.type === 'text'">{{ block.content }}</p>
|
||||
@@ -396,7 +399,7 @@ function generateJson() {
|
||||
</div>
|
||||
</ModalSection>
|
||||
|
||||
<ModalSection v-if="selectedFacility.notes?.length" title="注意事项">
|
||||
<ModalSection v-if="selectedFacility.notes?.length" title="注意事项" icon="fas fa-exclamation-triangle">
|
||||
<div class="content-blocks">
|
||||
<template v-for="(block, bi) in selectedFacility.notes" :key="bi">
|
||||
<p v-if="block.type === 'text'">{{ block.content }}</p>
|
||||
@@ -422,8 +425,8 @@ function generateJson() {
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">{{ edTitle || '未命名设施' }}</div>
|
||||
<div class="modal-badges">
|
||||
<span :class="['badge', 'large-badge', 'badge-status-' + edStatus]">{{ statusTextMap[edStatus] }}</span>
|
||||
<span class="badge large-badge badge-type">{{ typeTextMap[edType] }}</span>
|
||||
<span :class="['badge', 'large-badge', 'badge-status-' + edStatus]"><i class="fas" :class="statusIconMap[edStatus]"></i> {{ statusTextMap[edStatus] }}</span>
|
||||
<span class="badge large-badge badge-type"><i class="fas fa-cube"></i> {{ typeTextMap[edType] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
@@ -687,6 +690,29 @@ function generateJson() {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.status-indicator-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-indicator-badge.status-online { background-color: #e8fceb; color: #15803d; }
|
||||
.status-indicator-badge.status-maintenance { background-color: #fff8d6; color: #b45309; }
|
||||
.status-indicator-badge.status-offline { background-color: #feebeb; color: #b91c1c; }
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.status-online .status-dot { background-color: #22c55e; }
|
||||
.status-maintenance .status-dot { background-color: #f59e0b; }
|
||||
.status-offline .status-dot { background-color: #ef4444; }
|
||||
|
||||
.card-intro {
|
||||
font-size: 14px;
|
||||
color: var(--bl-text-secondary);
|
||||
|
||||
@@ -11,6 +11,7 @@ const projectFilter = ref('all');
|
||||
const modalOpen = ref(false);
|
||||
|
||||
const isMobile = ref(false);
|
||||
const qrLoaded = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
isMobile.value = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth < 768;
|
||||
@@ -167,10 +168,16 @@ function setProject(p) {
|
||||
<!-- Desktop QR -->
|
||||
<div v-if="!isMobile" class="desktop-qr-view">
|
||||
<div class="qr-placeholder">
|
||||
<div v-if="!qrLoaded" class="qr-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<span>加载中…</span>
|
||||
</div>
|
||||
<img
|
||||
src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https%3A%2F%2Fqr.alipay.com%2F2cz0344fnaulnbybhp04"
|
||||
alt="支付宝二维码"
|
||||
class="qr-img"
|
||||
:style="{ display: qrLoaded ? 'block' : 'none' }"
|
||||
@load="qrLoaded = true"
|
||||
>
|
||||
</div>
|
||||
<p class="desktop-qr-hint">推荐使用支付宝扫码</p>
|
||||
@@ -549,6 +556,25 @@ function setProject(p) {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
min-width: 232px;
|
||||
min-height: 232px;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: var(--bl-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.qr-loading i {
|
||||
font-size: 24px;
|
||||
color: var(--bl-accent);
|
||||
}
|
||||
|
||||
.qr-img {
|
||||
|
||||
@@ -443,7 +443,7 @@ function generateJson() {
|
||||
|
||||
<template v-if="selectedTown">
|
||||
<!-- Location -->
|
||||
<ModalSection title="位置信息">
|
||||
<ModalSection title="位置信息" icon="fas fa-map-marker-alt">
|
||||
<p v-if="selectedTown.coordinatesSecret">保密</p>
|
||||
<p v-else>
|
||||
{{ dimensionTextMap[selectedTown.dimension] || '主世界' }}
|
||||
@@ -463,7 +463,7 @@ function generateJson() {
|
||||
</ModalSection>
|
||||
|
||||
<!-- Founders -->
|
||||
<ModalSection title="创始人">
|
||||
<ModalSection title="创始人" icon="fas fa-crown">
|
||||
<div v-if="selectedTown.founders?.length" class="contributors-list">
|
||||
<span v-for="name in selectedTown.founders" :key="name" class="contributor-tag">
|
||||
<img :src="`https://minotar.net/avatar/${encodeURIComponent(name)}/20`" :alt="name" loading="lazy">
|
||||
@@ -474,7 +474,7 @@ function generateJson() {
|
||||
</ModalSection>
|
||||
|
||||
<!-- Members -->
|
||||
<ModalSection title="成员">
|
||||
<ModalSection title="成员" icon="fas fa-users">
|
||||
<div v-if="selectedTown.members?.length" class="contributors-list">
|
||||
<span v-for="name in selectedTown.members" :key="name" class="contributor-tag">
|
||||
<img :src="`https://minotar.net/avatar/${encodeURIComponent(name)}/20`" :alt="name" loading="lazy">
|
||||
@@ -485,7 +485,7 @@ function generateJson() {
|
||||
</ModalSection>
|
||||
|
||||
<!-- Introduction -->
|
||||
<ModalSection v-if="selectedTown.introduction?.length" title="城镇介绍">
|
||||
<ModalSection v-if="selectedTown.introduction?.length" title="城镇介绍" icon="fas fa-scroll">
|
||||
<div class="content-blocks">
|
||||
<template v-for="(block, bi) in selectedTown.introduction" :key="bi">
|
||||
<p v-if="block.type === 'text'">{{ block.content }}</p>
|
||||
@@ -623,10 +623,13 @@ function generateJson() {
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" v-model="edSecret" class="toggle-checkbox">
|
||||
<span class="toggle-switch"></span>
|
||||
坐标保密
|
||||
<span>坐标保密</span>
|
||||
<div class="toggle-switch">
|
||||
<input type="checkbox" v-model="edSecret">
|
||||
<span class="toggle-slider"></span>
|
||||
</div>
|
||||
</label>
|
||||
<p class="field-hint">开启后将隐藏坐标信息,适用于不希望公开位置的城镇。</p>
|
||||
</div>
|
||||
<template v-if="!edSecret">
|
||||
<div class="form-group">
|
||||
@@ -782,13 +785,22 @@ function generateJson() {
|
||||
}
|
||||
|
||||
.town-card-bg {
|
||||
height: 180px;
|
||||
height: 140px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #e8ecf1;
|
||||
}
|
||||
|
||||
.town-card-bg::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.no-logo {
|
||||
@@ -802,27 +814,40 @@ function generateJson() {
|
||||
|
||||
.town-card-icons {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
bottom: -14px;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.town-icon-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
backdrop-filter: blur(4px);
|
||||
font-size: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
.icon-scale-small { background: #60a5fa; }
|
||||
.icon-scale-medium { background: #f59e0b; }
|
||||
.icon-scale-large { background: #ef4444; }
|
||||
|
||||
.icon-type-building { background: #8b5cf6; }
|
||||
.icon-type-adventure { background: #10b981; }
|
||||
.icon-type-industry { background: #f97316; }
|
||||
|
||||
.icon-recruit-welcome { background: #22c55e; }
|
||||
.icon-recruit-closed { background: #ef4444; }
|
||||
.icon-recruit-maybe { background: #eab308; }
|
||||
|
||||
.town-card-body {
|
||||
padding: 18px 20px;
|
||||
padding: 24px 20px 20px;
|
||||
}
|
||||
|
||||
.town-card-title {
|
||||
@@ -835,6 +860,9 @@ function generateJson() {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.town-meta-tag {
|
||||
@@ -1106,49 +1134,67 @@ function generateJson() {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
.form-group .toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--bl-text);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toggle-checkbox {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
background: #ccc;
|
||||
border-radius: 12px;
|
||||
transition: 0.3s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-switch::after {
|
||||
content: '';
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #ccc;
|
||||
border-radius: 24px;
|
||||
transition: 0.25s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-slider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: 0.3s;
|
||||
transition: 0.25s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked + .toggle-switch {
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background: var(--bl-accent);
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked + .toggle-switch::after {
|
||||
.toggle-switch input:checked + .toggle-slider::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
font-size: 12px;
|
||||
color: var(--bl-text-secondary);
|
||||
margin-top: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user