Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added frontend/web/public/brand/mobile-download-qr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions frontend/web/src/component/landing/LandingPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ const workflow = [
const faqs = [
{ question: "需要准备教材或写好稿子吗?", answer: "不需要。你可以直接选择一个场景开始,AI 老师会根据你的水平和回应自然推进对话。" },
{ question: "免费版可以练习什么?", answer: "免费版包含每日自由对话额度、每日普通场景和全部 AI 老师,适合先建立稳定开口的习惯。" },
{ question: "移动端什么时候可以下载?", answer: "移动端正在准备正式发布。商店链接配置完成后,这里会自动显示 iOS 和 Android 下载入口。" },
{ question: "移动端怎么下载安装?", answer: "扫描平台区域的二维码即可打开移动端下载页。也可以点击旁边的 Mobile app 标签,在新页面完成下载安装。" },
{ question: "AI 的反馈可以替代老师吗?", answer: "UniSpeaking 用于日常练习和即时反馈,不替代专业教师、考试官方评分或正式语言诊断。" },
];

const mobileDownloadUrl = "http://fir.qiniuapi.com/v1zyqajm?utm_source=fir&utm_medium=qr";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 不要通过明文 HTTP 分发移动端安装入口

这里的新链接同时被写入可点击入口和二维码,但目标仅通过 http:// 返回下载页面;在 HTTPS 落地页点击后会降级到明文连接,网络中的攻击者可以替换页面、下载地址或安装说明。尝试同域 HTTPS 也无法作为替代,因为当前证书已过期且域名不匹配。由于这是引导用户安装移动应用的路径,请改用证书有效、受项目控制的 HTTPS 下载地址(或先跳转到项目自己的 HTTPS 页面),再重新生成二维码。


function Brand() {
return (
<a className="landing-brand" href="#top" aria-label="返回 UniSpeaking 首页">
Expand Down Expand Up @@ -271,7 +273,7 @@ export function LandingPage({ onStart, onLogin, onWeb, onSpecialty }) {
<div className="landing-container"><div className="landing-section-heading landing-section-heading--compact"><div><p className="landing-eyebrow">PRACTICE FOR REAL LIFE</p><h2>你想说的,<br />都可以成为练习。</h2></div><p>从日常交流到重要目标,选择一个真实场景,马上进入更有方向的对话。</p></div><div className="landing-specialty-grid"><article><div className="landing-specialty-card__icon landing-tone-yellow"><BookOpenText weight="duotone" /></div><p className="landing-eyebrow">EVERYDAY ENGLISH</p><h3>日常场景</h3><p>点单、购物、出行、住宿和社交,让下次真实交流少一点犹豫。</p><a href="#capabilities">查看场景 <ArrowRight weight="bold" /></a></article><article><div className="landing-specialty-card__icon landing-tone-lavender"><TargetIcon /></div><p className="landing-eyebrow">IELTS SPEAKING</p><h3>IELTS 口语</h3><p>Part 1、Part 2、Part 3,从回答结构到表达表现,完整练习每一部分。</p><button type="button" aria-label="开始体验 IELTS 口语" onClick={() => onSpecialty("ielts")}>开始体验 <ArrowRight weight="bold" /></button></article><article><div className="landing-specialty-card__icon landing-tone-sky"><Briefcase weight="duotone" /></div><p className="landing-eyebrow">INTERVIEW PRACTICE</p><h3>英文面试</h3><p>用更接近真实面试的方式,练习自我介绍、追问和清晰表达。</p><button type="button" aria-label="开始体验英文面试" onClick={() => onSpecialty("interview")}>开始体验 <ArrowRight weight="bold" /></button></article></div></div>
</section>

<section className="landing-platform" id="platforms" data-reveal><div className="landing-container landing-platform__grid"><div className="landing-platform__copy"><p className="landing-eyebrow">PRACTICE WHEREVER YOU ARE</p><h2>电脑前深入练,<br />手机上随时说。</h2><p>Web 端适合完整训练和复盘,移动端适合把零碎时间变成开口时间。你的练习路径,会跟着你走。</p><div className="landing-platform__actions"><LandingButton onClick={onWeb} icon={<ArrowUpRight weight="bold" />}>进入 Web 端</LandingButton><span className="landing-platform__note"><CheckCircle weight="fill" />无需安装,即开即用</span></div></div><div className="landing-devices"><div className="landing-device landing-device--desktop"><div className="landing-device__bar"><i /><i /><i /><span>app.unispeaking.com</span></div><div className="landing-device__screen"><div className="landing-mini-sidebar"><span /><span /><span /><span /></div><div className="landing-mini-main"><small>自由对话</small><strong>今天想聊点什么?</strong><div className="landing-mini-wave">{[.25, .5, .8, .43, .68, .32, .58, .92, .44, .7, .35, .62].map((height, index) => <i key={index} style={{ "--wave-height": height }} />)}</div><span className="landing-mini-button">开始对话 <ArrowRight weight="bold" /></span></div></div></div><div className="landing-device landing-device--mobile"><div className="landing-device__speaker" /><div className="landing-device__screen"><img src="/brand/unispeaking-mark-user.jpg" alt="" /><strong>移动端即将上线</strong><span>把练习放进口袋</span><i><DeviceMobile weight="duotone" /></i></div></div><div className="landing-devices__badge"><DeviceMobile weight="duotone" /><span><strong>Mobile app</strong><small>Coming soon</small></span></div></div></div></section>
<section className="landing-platform" id="platforms" data-reveal><div className="landing-container landing-platform__grid"><div className="landing-platform__copy"><p className="landing-eyebrow">PRACTICE WHEREVER YOU ARE</p><h2>电脑前深入练,<br />手机上随时说。</h2><p>Web 端适合完整训练和复盘,移动端适合把零碎时间变成开口时间。扫描二维码,随时把练习带在身边。</p><div className="landing-platform__actions"><LandingButton onClick={onWeb} icon={<ArrowUpRight weight="bold" />}>进入 Web 端</LandingButton><span className="landing-platform__note"><CheckCircle weight="fill" />Web 与移动端练习同步</span></div></div><div className="landing-devices"><div className="landing-device landing-device--desktop"><div className="landing-device__bar"><i /><i /><i /><span>app.unispeaking.com</span></div><div className="landing-device__screen"><div className="landing-mini-sidebar"><span /><span /><span /><span /></div><div className="landing-mini-main"><small>自由对话</small><strong>今天想聊点什么?</strong><div className="landing-mini-wave">{[.25, .5, .8, .43, .68, .32, .58, .92, .44, .7, .35, .62].map((height, index) => <i key={index} style={{ "--wave-height": height }} />)}</div><span className="landing-mini-button">开始对话 <ArrowRight weight="bold" /></span></div></div></div><div className="landing-device landing-device--mobile"><div className="landing-device__speaker" /><a className="landing-device__screen landing-mobile-download" href={mobileDownloadUrl} target="_blank" rel="noreferrer" aria-label="打开移动端下载页"><img className="landing-mobile-download__qr" src="/brand/mobile-download-qr.png" alt="移动端下载二维码" /><strong>扫码下载移动端</strong><span>把练习放进口袋</span><i><DeviceMobile weight="duotone" /></i></a></div><a className="landing-devices__badge" href={mobileDownloadUrl} target="_blank" rel="noreferrer" aria-label="打开移动端下载页"><DeviceMobile weight="duotone" /><span><strong>Mobile app</strong><small>扫码下载</small></span><ArrowUpRight weight="bold" /></a></div></div></section>

<section className="landing-section landing-pricing" id="pricing" data-reveal><div className="landing-container"><div className="landing-section-heading landing-section-heading--compact"><div><p className="landing-eyebrow">MEMBERSHIP PREVIEW</p><h2>先免费开始,<br />再按你的节奏升级。</h2></div><p>免费版足够你建立习惯。需要更长时间、更完整的专项训练时,再选择适合自己的方案。</p></div><div className="landing-pricing-grid">{plans.map((plan) => <article key={plan.id} className={`landing-plan ${plan.id === "pro" ? "is-featured" : ""}`}><div className="landing-plan__heading"><div><span className="landing-plan__label">{plan.id === "pro" ? "最受欢迎" : plan.id === "free" ? "从这里开始" : "目标训练"}</span><h3>{plan.name}</h3></div>{plan.id === "pro" && <span className="landing-plan__badge">推荐</span>}</div><p>{plan.desc}</p><div className="landing-plan__price"><strong>{plan.price === "0" ? "免费" : `¥${plan.price}`}</strong>{plan.suffix && <span>{plan.suffix}</span>}</div><ul>{plan.features.map((feature) => <li key={feature}><CheckCircle weight="fill" />{feature}</li>)}</ul><LandingButton variant={plan.id === "pro" ? "primary" : "secondary"} onClick={onStart} icon={<ArrowRight weight="bold" />}>{plan.id === "free" ? "免费开始" : "加入候补"}</LandingButton></article>)}</div><p className="landing-pricing__footnote"><ShieldCheck weight="bold" /> 会员支付正在准备中,正式权益和价格以发布版本为准。</p></div></section>

Expand Down
12 changes: 9 additions & 3 deletions frontend/web/src/component/landing/landing.css
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,18 @@
.landing-device--mobile { right: 2%; bottom: 4px; width: 160px; height: 320px; padding: 8px; border: 4px solid #2f2f2d; border-radius: 24px; transform: rotate(4deg); background: #292927; }
.landing-device__speaker { position: absolute; z-index: 2; top: 8px; left: 50%; width: 42px; height: 5px; border-radius: 5px; transform: translateX(-50%); background: #30302e; }
.landing-device--mobile .landing-device__screen { width: 100%; height: 100%; padding: 44px 14px 22px; display: flex; flex-direction: column; align-items: center; justify-content: center; border-radius: 17px; color: #1c1c1b; text-align: center; background: #f4f4f0; }
.landing-mobile-download { text-decoration: none; }
.landing-device--mobile img { width: 42px; height: 42px; margin-bottom: 15px; border-radius: 8px; }
.landing-device--mobile .landing-mobile-download__qr { width: 82px; height: 82px; padding: 6px; object-fit: contain; border: 1px solid #deded8; border-radius: 6px; background: #fff; image-rendering: pixelated; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 保留二维码的四模块静区和整数缩放

这个源图没有 QR 规范要求的四模块白色静区,而这里在 box-sizing: border-box 下把 33×33 模块内容压到 68px(560px 以下为 46px),同时只留下约 3 个模块的白边并进行非整数缩放。我用提交的图片按这两个 CSS 尺寸渲染后,OpenCV 均无法检测或解码;把同一模块矩阵改为四模块静区、2px 整数缩放的 82px 图后即可立即解码。这样桌面端展示的“扫码下载”会在常见扫码器/摄像头上不可靠;请让资产自带四模块静区,并按整数模块尺寸渲染(或增大容器并相应调整移动断点)。

.landing-device--mobile strong { font-size: 11px; }
.landing-device--mobile span { margin-top: 6px; color: #83837d; font-size: 8px; }
.landing-device--mobile .landing-device__screen i { width: 40px; height: 40px; margin-top: 28px; display: grid; place-items: center; border: 1px solid #d6d6d0; border-radius: 50%; background: #fff; }
.landing-device--mobile .landing-device__screen i { width: 40px; height: 40px; margin-top: 18px; display: grid; place-items: center; border: 1px solid #d6d6d0; border-radius: 50%; background: #fff; }
.landing-device--mobile svg { width: 18px; height: 18px; }
.landing-devices__badge { position: absolute; z-index: 4; right: 32px; bottom: 22px; padding: 10px 12px; display: flex; align-items: center; gap: 9px; border: 1px solid #d6d6d0; border-radius: 7px; color: #282826; background: #fff; box-shadow: 0 16px 34px rgba(0,0,0,.24); }
.landing-devices__badge { position: absolute; z-index: 4; right: 32px; bottom: 22px; padding: 10px 12px; display: flex; align-items: center; gap: 9px; border: 1px solid #d6d6d0; border-radius: 7px; color: #282826; background: #fff; box-shadow: 0 16px 34px rgba(0,0,0,.24); text-decoration: none; transition: transform .2s ease, box-shadow .2s ease; }
.landing-devices__badge:hover { transform: translateY(-2px); box-shadow: 0 20px 40px rgba(0,0,0,.3); }
.landing-devices__badge:focus-visible, .landing-mobile-download:focus-visible { outline: 3px solid var(--landing-yellow); outline-offset: 3px; }
.landing-devices__badge svg { width: 21px; height: 21px; }
.landing-devices__badge > svg:last-child { width: 12px; height: 12px; margin-left: 2px; }
.landing-devices__badge strong, .landing-devices__badge small { display: block; }
.landing-devices__badge strong { font-size: 9px; }
.landing-devices__badge small { margin-top: 3px; color: #898983; font-size: 7px; }
Expand Down Expand Up @@ -470,8 +475,9 @@
.landing-mini-wave { height: 38px; }
.landing-device--mobile { right: -2%; width: 112px; height: 230px; border-radius: 20px; }
.landing-device--mobile .landing-device__screen { padding-inline: 8px; }
.landing-device--mobile .landing-mobile-download__qr { width: 58px; height: 58px; padding: 5px; margin-bottom: 10px; }
.landing-device--mobile strong { font-size: 8px; }
.landing-device--mobile .landing-device__screen i { width: 32px; height: 32px; margin-top: 20px; }
.landing-device--mobile .landing-device__screen i { width: 32px; height: 32px; margin-top: 12px; }
.landing-devices__badge { right: 4px; bottom: 2px; }
.landing-plan { min-height: 400px; padding: 24px; }
.landing-pricing__footnote { align-items: flex-start; text-align: left; line-height: 1.5; }
Expand Down
Loading