feat(home): 门户式主侧栏布局,联系我们置底,侧栏关注我们

Made-with: Cursor
This commit is contained in:
whm
2026-03-23 10:57:43 +08:00
parent eb6923998f
commit 52991d1e49

View File

@@ -65,8 +65,54 @@
</div> </div>
</section> </section>
<div class="portal-shell">
<div class="portal-inner">
<main class="portal-main">
<!-- 参考政务门户左主栏首行 = 轮播 + 右侧列表 -->
<div class="portal-row-split portal-card">
<div class="portal-carousel-wrap">
<h3 class="portal-block-title">产品亮点</h3>
<div class="portal-carousel">
<router-link v-if="currentSpotlight" :to="currentSpotlight.to" class="portal-carousel-slide">
<div class="portal-carousel-visual">
<div class="portal-carousel-icon">
<svg viewBox="0 0 24 24"><path :d="currentSpotlight.icon"/></svg>
</div>
</div>
<div class="portal-carousel-caption">
<h4>{{ currentSpotlight.title }}</h4>
<p>{{ currentSpotlight.desc }}</p>
<span class="portal-carousel-more">进入宣传册{{ currentSpotlight.linkLabel }} </span>
</div>
</router-link>
<button type="button" class="portal-carousel-nav prev" aria-label="上一则" @click="spotlightPrev"></button>
<button type="button" class="portal-carousel-nav next" aria-label="下一则" @click="spotlightNext"></button>
<div class="portal-carousel-dots">
<button
v-for="(_, i) in spotlightSlides"
:key="i"
type="button"
:class="{ active: i === spotlightIndex }"
:aria-label="'切换到第' + (i + 1) + ''"
@click="spotlightIndex = i"
/>
</div>
</div>
</div>
<div class="portal-news-panel">
<h3 class="portal-block-title">宣传册 · 快速入口</h3>
<ul class="portal-news-list">
<li v-for="(b, idx) in brochureEntryLinks" :key="b.topic">
<router-link :to="'/brochure/' + b.topic">{{ b.label }}</router-link>
<span class="portal-news-date">{{ formatBrochureListDate(idx) }}</span>
</li>
</ul>
<router-link to="/brochure/company" class="portal-read-all">阅读完整宣传册 </router-link>
</div>
</div>
<!-- 统计数据 promotion/index.html 一致 --> <!-- 统计数据 promotion/index.html 一致 -->
<section class="stats-section"> <section class="stats-section portal-card-lite">
<div class="stats-container"> <div class="stats-container">
<div class="stat-item"><div class="stat-number">50+</div><div class="stat-label">活跃用户</div></div> <div class="stat-item"><div class="stat-number">50+</div><div class="stat-label">活跃用户</div></div>
<div class="stat-item"><div class="stat-number">99.9%</div><div class="stat-label">服务可用性</div></div> <div class="stat-item"><div class="stat-number">99.9%</div><div class="stat-label">服务可用性</div></div>
@@ -114,111 +160,6 @@
</div> </div>
</section> </section>
<section class="testimonials-section" id="testimonials">
<div class="section-title">
<h2>用户评价</h2>
<p>听听来自各行业用户的心声</p>
</div>
<div class="testimonials-grid">
<div class="testimonial-card" v-for="(t, i) in testimonials" :key="i">
<p class="testimonial-text">"{{ t.text }}"</p>
<div class="testimonial-author">
<div class="author-avatar">{{ t.av }}</div>
<div class="author-info">
<h5>{{ t.name }}</h5>
<span>{{ t.role }}</span>
</div>
</div>
</div>
</div>
</section>
<section class="social-section" id="social-follow">
<h3 class="social-title">关注我们</h3>
<div class="social-links">
<button
v-for="item in socialFollowItems"
:key="item.id"
type="button"
class="social-pill"
@click="openSocialFollow(item)"
>
<span class="social-pill-icon-wrap">
<img
:src="socialListImgSrc(item)"
:alt="item.label"
class="social-pill-icon"
width="32"
height="32"
loading="lazy"
@error="onSocialFollowListImgError(item)"
/>
</span>
<span class="social-pill-label">{{ item.label }}</span>
</button>
</div>
</section>
<section class="faq-section" id="faq">
<div class="section-title">
<h2>常见问题</h2>
<p>关于宇恒一号您可能想了解这些</p>
</div>
<div class="faq-container">
<div
v-for="(item, i) in faqList"
:key="i"
class="faq-item"
:class="{ active: openFaq === i }"
>
<div class="faq-question" @click="toggleFaq(i)">
<h4>{{ item.q }}</h4>
<div class="faq-icon">+</div>
</div>
<div class="faq-answer"><p>{{ item.a }}</p></div>
</div>
</div>
</section>
<section class="contact-section" id="contact">
<div class="contact-container">
<div class="section-title">
<h2>联系我们</h2>
<p>有任何问题或建议我们随时为您服务</p>
</div>
<div class="contact-info">
<div class="contact-item">
<div class="contact-text">
<h5>公司地址</h5>
<p>成都市金牛区华丰路217号<br>2栋2单元12层11号</p>
</div>
</div>
<div class="contact-item">
<div class="contact-text">
<h5>电子邮箱</h5>
<a href="mailto:1406339793@qq.com">1406339793@qq.com</a>
</div>
</div>
<div class="contact-item">
<div class="contact-text">
<h5>官方网站</h5>
<a href="https://yuheng.yuxindazhineng.com/" target="_blank" rel="noopener">yuheng.yuxindazhineng.com</a>
</div>
</div>
</div>
</div>
</section>
<section class="partners-section">
<h3>技术支持与合作</h3>
<div class="partners-grid">
<span class="partner-logo">成都宇信达智能科技</span>
<span class="partner-logo">宇恒一号</span>
<span class="partner-logo">AI 智能助手</span>
<span class="partner-logo">智能解决方案</span>
</div>
</section>
<section class="video-section" id="videos"> <section class="video-section" id="videos">
<div class="section-title"> <div class="section-title">
<h2>产品视频</h2> <h2>产品视频</h2>
@@ -268,10 +209,125 @@
</div> </div>
</section> </section>
<section class="testimonials-section" id="testimonials">
<div class="section-title">
<h2>用户评价</h2>
<p>听听来自各行业用户的心声</p>
</div>
<div class="testimonials-grid">
<div class="testimonial-card" v-for="(t, i) in testimonials" :key="i">
<p class="testimonial-text">"{{ t.text }}"</p>
<div class="testimonial-author">
<div class="author-avatar">{{ t.av }}</div>
<div class="author-info">
<h5>{{ t.name }}</h5>
<span>{{ t.role }}</span>
</div>
</div>
</div>
</div>
</section>
<section class="faq-section" id="faq">
<div class="section-title">
<h2>常见问题</h2>
<p>关于宇恒一号您可能想了解这些</p>
</div>
<div class="faq-container">
<div
v-for="(item, i) in faqList"
:key="i"
class="faq-item"
:class="{ active: openFaq === i }"
>
<div class="faq-question" @click="toggleFaq(i)">
<h4>{{ item.q }}</h4>
<div class="faq-icon">+</div>
</div>
<div class="faq-answer"><p>{{ item.a }}</p></div>
</div>
</div>
</section>
<section class="partners-section">
<h3>技术支持与合作</h3>
<div class="partners-grid">
<span class="partner-logo">成都宇信达智能科技</span>
<span class="partner-logo">宇恒一号</span>
<span class="partner-logo">AI 智能助手</span>
<span class="partner-logo">智能解决方案</span>
</div>
</section>
<section v-if="bodyBuilderBlocks.length" class="home-body-builder"> <section v-if="bodyBuilderBlocks.length" class="home-body-builder">
<BlockRenderer :blocks="bodyBuilderBlocks" /> <BlockRenderer :blocks="bodyBuilderBlocks" />
</section> </section>
<section class="contact-section portal-card-lite" id="contact">
<div class="contact-container">
<div class="section-title">
<h2>联系我们</h2>
<p>有任何问题或建议我们随时为您服务</p>
</div>
<div class="contact-info">
<div class="contact-item">
<div class="contact-text">
<h5>公司地址</h5>
<p>成都市金牛区华丰路217号<br>2栋2单元12层11号</p>
</div>
</div>
<div class="contact-item">
<div class="contact-text">
<h5>电子邮箱</h5>
<a href="mailto:1406339793@qq.com">1406339793@qq.com</a>
</div>
</div>
<div class="contact-item">
<div class="contact-text">
<h5>官方网站</h5>
<a href="https://yuheng.yuxindazhineng.com/" target="_blank" rel="noopener">yuheng.yuxindazhineng.com</a>
</div>
</div>
</div>
</div>
</section>
</main>
<aside class="portal-aside" id="social-follow" aria-label="关注我们">
<div class="portal-aside-sticky">
<h3 class="portal-aside-title">关注我们</h3>
<p class="portal-aside-hint">扫码或点击查看各平台账号</p>
<div class="portal-aside-social">
<button
v-for="item in socialFollowItems"
:key="item.id"
type="button"
class="portal-social-tile"
@click="openSocialFollow(item)"
>
<span class="portal-social-tile-img">
<img
:src="socialListImgSrc(item)"
:alt="item.label"
width="72"
height="72"
loading="lazy"
@error="onSocialFollowListImgError(item)"
/>
</span>
<span class="portal-social-tile-label">{{ item.label }}</span>
</button>
</div>
<a
href="#download"
class="portal-aside-download"
@click.prevent="scrollToSel('#download')"
>前往下载 </a>
</div>
</aside>
</div>
</div>
<footer> <footer>
<div class="footer-content"> <div class="footer-content">
<div class="footer-logo">宇恒一号</div> <div class="footer-logo">宇恒一号</div>
@@ -393,6 +449,7 @@ const defaultData = () => ({
{ label: '应用场景', url: '#scenarios' }, { label: '应用场景', url: '#scenarios' },
{ label: '产品视频', url: '#videos' }, { label: '产品视频', url: '#videos' },
{ label: '宣传册', url: '/brochure/company' }, { label: '宣传册', url: '/brochure/company' },
{ label: '关注我们', url: '#social-follow' },
{ label: '联系我们', url: '#contact' } { label: '联系我们', url: '#contact' }
], ],
title: '宇恒一号', title: '宇恒一号',
@@ -468,6 +525,30 @@ const highlightCards = [
{ title: '免费使用', desc: '基础能力免费开放,无隐藏收费', icon: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z', to: '/brochure/support', linkLabel: '服务支持' } { title: '免费使用', desc: '基础能力免费开放,无隐藏收费', icon: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z', to: '/brochure/support', linkLabel: '服务支持' }
] ]
/** 首行门户轮播:取前若干条亮点宣传册入口 */
const SPOTLIGHT_COUNT = 4
const spotlightSlides = computed(() => highlightCards.slice(0, SPOTLIGHT_COUNT))
const spotlightIndex = ref(0)
const currentSpotlight = computed(() => spotlightSlides.value[spotlightIndex.value] || null)
function spotlightPrev() {
const n = spotlightSlides.value.length
if (!n) return
spotlightIndex.value = (spotlightIndex.value - 1 + n) % n
}
function spotlightNext() {
const n = spotlightSlides.value.length
if (!n) return
spotlightIndex.value = (spotlightIndex.value + 1) % n
}
/** 宣传册列表右侧日期(按条目递减,便于排版对齐) */
function formatBrochureListDate(idx) {
const d = new Date()
d.setDate(d.getDate() - idx)
const mm = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return mm + '-' + dd
}
const scenarioCards = [ const scenarioCards = [
{ title: '企业办公', desc: '文档管理、数据报表、会议纪要、任务追踪', icon: 'M20 6h-4V4c0-1.1-.9-2-2-2h-4c-1.1 0-2 .9-2 2v2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-6 0h-4v2h4v2h-4v2h4v2H8V6h6v2h4V6z', to: '/brochure/scenarios' }, { title: '企业办公', desc: '文档管理、数据报表、会议纪要、任务追踪', icon: 'M20 6h-4V4c0-1.1-.9-2-2-2h-4c-1.1 0-2 .9-2 2v2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-6 0h-4v2h4v2h-4v2h4v2H8V6h6v2h4V6z', to: '/brochure/scenarios' },
{ title: '数据分析', desc: '销售分析、用户统计、财务对账、批量处理', icon: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z', to: '/brochure/scenarios' }, { title: '数据分析', desc: '销售分析、用户统计、财务对账、批量处理', icon: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z', to: '/brochure/scenarios' },
@@ -887,8 +968,286 @@ onUnmounted(() => {
.feature-space h3 { font-family: 'Exo 2', sans-serif; font-size: 18px; color: var(--star-white); margin-bottom: 12px; letter-spacing: 1px; } .feature-space h3 { font-family: 'Exo 2', sans-serif; font-size: 18px; color: var(--star-white); margin-bottom: 12px; letter-spacing: 1px; }
.feature-space p { color: rgba(255,255,255,0.5); font-size: 14px; line-height: 1.7; } .feature-space p { color: rgba(255,255,255,0.5); font-size: 14px; line-height: 1.7; }
/* 门户主栏 + 侧栏(约 75% / 25% */
.portal-shell {
position: relative;
z-index: 10;
background: linear-gradient(180deg, rgba(10, 12, 22, 0.4) 0%, rgba(18, 22, 38, 0.92) 8%, rgba(20, 24, 40, 0.98) 100%);
padding: 36px 0 8px;
}
.portal-inner {
max-width: 1320px;
margin: 0 auto;
padding: 0 20px 32px;
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 28px;
align-items: start;
}
.portal-main { min-width: 0; }
.portal-block-title {
font-family: 'Exo 2', 'Noto Sans SC', sans-serif;
font-size: 15px;
letter-spacing: 2px;
color: var(--star-white);
margin: 0 0 14px;
padding-left: 10px;
border-left: 3px solid var(--plasma-cyan);
}
.portal-card {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(0, 212, 255, 0.12);
border-radius: 16px;
padding: 22px 20px 24px;
margin-bottom: 28px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.portal-card-lite {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(0, 212, 255, 0.08);
border-radius: 16px;
padding: 24px 20px;
margin-bottom: 24px;
}
.portal-row-split {
display: grid;
grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr);
gap: 24px;
align-items: stretch;
}
.portal-carousel-wrap { min-width: 0; }
.portal-carousel {
position: relative;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(145deg, rgba(30, 58, 95, 0.35), rgba(10, 10, 18, 0.9));
border: 1px solid rgba(0, 212, 255, 0.15);
min-height: 220px;
}
.portal-carousel-slide {
display: block;
text-decoration: none;
color: inherit;
padding: 0;
}
.portal-carousel-visual {
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, rgba(0, 212, 255, 0.08), transparent);
}
.portal-carousel-icon {
width: 80px;
height: 80px;
border-radius: 20px;
background: linear-gradient(135deg, var(--plasma-cyan), var(--plasma-pink));
display: flex;
align-items: center;
justify-content: center;
margin: 24px 0 8px;
}
.portal-carousel-icon svg { width: 40px; height: 40px; fill: var(--space-dark); }
.portal-carousel-caption {
padding: 16px 20px 48px;
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.55));
}
.portal-carousel-caption h4 {
font-family: 'Exo 2', sans-serif;
font-size: 18px;
margin: 0 0 8px;
color: var(--star-white);
}
.portal-carousel-caption p {
margin: 0;
font-size: 13px;
line-height: 1.65;
color: rgba(255, 255, 255, 0.72);
}
.portal-carousel-more {
display: inline-block;
margin-top: 12px;
font-size: 12px;
color: var(--plasma-cyan);
letter-spacing: 0.5px;
}
.portal-carousel-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(0, 0, 0, 0.45);
color: #fff;
font-size: 22px;
line-height: 1;
cursor: pointer;
z-index: 2;
transition: background 0.2s, border-color 0.2s;
}
.portal-carousel-nav:hover {
background: rgba(0, 212, 255, 0.25);
border-color: var(--plasma-cyan);
}
.portal-carousel-nav.prev { left: 10px; }
.portal-carousel-nav.next { right: 10px; }
.portal-carousel-dots {
position: absolute;
left: 0;
right: 0;
bottom: 12px;
display: flex;
justify-content: center;
gap: 8px;
z-index: 2;
}
.portal-carousel-dots button {
width: 8px;
height: 8px;
border-radius: 50%;
border: none;
padding: 0;
background: rgba(255, 255, 255, 0.35);
cursor: pointer;
transition: transform 0.2s, background 0.2s;
}
.portal-carousel-dots button.active {
background: var(--plasma-cyan);
transform: scale(1.15);
}
.portal-news-panel { min-width: 0; display: flex; flex-direction: column; }
.portal-news-list {
list-style: none;
margin: 0;
padding: 0;
flex: 1;
}
.portal-news-list li {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
font-size: 14px;
}
.portal-news-list li:last-child { border-bottom: none; }
.portal-news-list a {
color: rgba(255, 255, 255, 0.88);
text-decoration: none;
flex: 1;
min-width: 0;
line-height: 1.5;
}
.portal-news-list a:hover { color: var(--plasma-cyan); }
.portal-news-date {
flex-shrink: 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.38);
font-family: 'Exo 2', monospace;
}
.portal-read-all {
margin-top: 14px;
font-size: 13px;
color: var(--plasma-cyan);
text-decoration: none;
align-self: flex-start;
}
.portal-read-all:hover { text-decoration: underline; }
.portal-aside { min-width: 0; }
.portal-aside-sticky {
position: sticky;
top: 88px;
padding: 20px 16px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(0, 212, 255, 0.12);
}
.portal-aside-title {
font-family: 'Exo 2', 'Noto Sans SC', sans-serif;
font-size: 15px;
letter-spacing: 3px;
color: var(--star-white);
margin: 0 0 6px;
text-align: center;
}
.portal-aside-hint {
margin: 0 0 16px;
font-size: 12px;
color: rgba(255, 255, 255, 0.45);
text-align: center;
line-height: 1.5;
}
.portal-aside-social {
display: flex;
flex-direction: column;
gap: 14px;
}
.portal-social-tile {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100%;
padding: 14px 10px;
border-radius: 12px;
border: 1px solid rgba(0, 212, 255, 0.2);
background: rgba(10, 14, 28, 0.6);
color: rgba(255, 255, 255, 0.92);
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
}
.portal-social-tile:hover {
border-color: var(--plasma-cyan);
box-shadow: 0 0 20px rgba(0, 212, 255, 0.15);
transform: translateY(-2px);
}
.portal-social-tile-img {
width: 72px;
height: 72px;
border-radius: 8px;
overflow: hidden;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
}
.portal-social-tile-img img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.portal-social-tile-label {
font-size: 12px;
text-align: center;
line-height: 1.4;
}
.portal-aside-download {
display: block;
margin-top: 18px;
padding: 12px 16px;
text-align: center;
border-radius: 10px;
font-size: 13px;
font-family: 'Exo 2', sans-serif;
letter-spacing: 1px;
color: var(--space-dark);
text-decoration: none;
background: linear-gradient(135deg, var(--plasma-cyan), var(--plasma-pink));
transition: filter 0.2s, transform 0.2s;
}
.portal-aside-download:hover {
filter: brightness(1.08);
transform: translateY(-1px);
}
/* 统计 / 亮点 / 场景 … */ /* 统计 / 亮点 / 场景 … */
.stats-section { padding: 80px 20px; position: relative; z-index: 10; } .stats-section { padding: 80px 20px; position: relative; z-index: 10; }
.stats-section.portal-card-lite { padding: 28px 20px; margin-bottom: 28px; }
.stats-container { .stats-container {
max-width: 1200px; margin: 0 auto; max-width: 1200px; margin: 0 auto;
display: grid; grid-template-columns: repeat(4, 1fr); gap: 40px; display: grid; grid-template-columns: repeat(4, 1fr); gap: 40px;
@@ -1145,6 +1504,9 @@ onUnmounted(() => {
.landing footer .beian a:hover { text-decoration: underline; } .landing footer .beian a:hover { text-decoration: underline; }
@media (max-width: 1024px) { @media (max-width: 1024px) {
.portal-inner { grid-template-columns: 1fr; }
.portal-row-split { grid-template-columns: 1fr; }
.portal-aside-sticky { position: static; }
.video-container { grid-template-columns: 1fr; } .video-container { grid-template-columns: 1fr; }
.highlight-grid { grid-template-columns: 1fr; } .highlight-grid { grid-template-columns: 1fr; }
.stats-container { grid-template-columns: repeat(2, 1fr); } .stats-container { grid-template-columns: repeat(2, 1fr); }