fix(web): 文章编辑器手机预览放大至 375px — 匹配真机阅读效果

- 屏幕宽度从 256px 放大到 375px(接近 iPhone CSS 逻辑像素)
- 内容字体全部改为小程序真实尺寸(16px 正文 / 22px 标题 / 15px 摘要)
- 间距 padding/margin 与小程序 article/detail 完全一致
- 手机高度改为自适应(内容撑开),不再固定截断
- Dynamic Island / 状态栏 / 按钮按比例放大
This commit is contained in:
iven
2026-05-11 03:24:15 +08:00
parent 00301d2528
commit 6269815046

View File

@@ -12,15 +12,16 @@ interface ArticlePhonePreviewProps {
/**
* iPhone 17 Pro Max 高保真仿真预览
*
* 真实参数比例:
* 屏幕宽度 375px接近 iPhone CSS 逻辑像素 430pt 的 87%
* 内容区文字大小和间距与小程序 article/detail 完全一致,
* 保证预览效果 = 实际手机阅读效果。
*
* iPhone 17 Pro Max 参数:
* 物理尺寸 163.4mm × 78.0mm (aspect 2.095)
* 铝合金一体化机身 (7000-series aerospace aluminum)
* 横向相机条 (horizontal camera bar) — 3 镜头
* Dynamic Island: 药丸形 + 前置摄像头 + 接近传感器
* 侧边按钮: 音量×2 / Action Button / 电源
* 横向相机条 — 3 镜头
* Dynamic Island: 药丸形
* Home Indicator: 底部圆角横条
*
* 内容始终亮色(不跟暗黑模式),外框背景跟随主题。
*/
export default function ArticlePhonePreview({
title,
@@ -45,47 +46,47 @@ export default function ArticlePhonePreview({
const today = useMemo(() => new Date().toLocaleDateString('zh-CN'), []);
// ── iPhone 17 Pro Max 缩放参数 ──
// 物理: 163.4 × 78.0 mm → 比例 2.095:1
const PHONE_W = 256;
const PHONE_H = Math.round(PHONE_W * 2.095); // 536
const PHONE_R = 50;
const BEZEL = 3; // 铝合金一体 → 更窄边框
const SCREEN_R = PHONE_R - BEZEL; // 47
// 屏幕 375px 宽 → 接近真机 CSS 像素,内容可读性保证
const BEZEL = 4;
const PHONE_W = 375;
const PHONE_R = 58;
const SCREEN_R = PHONE_R - BEZEL; // 54
// Dynamic Island (物理约 25.4mm × 8.8mm)
const ISLAND_W = 82;
const ISLAND_H = 25;
const ISLAND_R = 12.5;
// Dynamic Island
const ISLAND_W = 120;
const ISLAND_H = 32;
const ISLAND_R = 16;
const ISLAND_TOP = 14;
// 前置摄像头
const CAM_SIZE = 7;
const CAM_RIGHT = 16;
// 接近传感器
const SENSOR_SIZE = 3;
const SENSOR_LEFT = 16;
const CAM_SIZE = 10;
const CAM_RIGHT = 22;
const SENSOR_SIZE = 4;
const SENSOR_LEFT = 22;
// 状态栏
const STATUS_H = 50;
const STATUS_H = 56;
// Home Indicator
const HOME_W = 84;
const HOME_W = 120;
const HOME_H = 4;
const HOME_BOTTOM = 10;
// 侧边按钮突出距离
const BTN_OUT = 2.5;
// 侧边按钮
const BTN_OUT = 3;
// ── 铝合金颜色 ──
// iPhone 17 Pro Max: 航空级 7000 系铝合金,一体成型
const AL_BODY = '#B0B2B8'; // 银色铝合金
// 铝合金
const AL_BODY = '#B0B2B8';
const AL_DARK = '#9A9CA2';
const AL_LIGHT = '#C4C6CC';
// 外框总宽度(手机 + 左右 padding
const OUTER_W = PHONE_W + BEZEL * 2 + 32; // ~415
return (
<div
style={{
width: 400,
width: OUTER_W,
flexShrink: 0,
background: isDark ? '#0f172a' : '#e8e8ed',
borderLeft: `1px solid ${isDark ? '#1e293b' : '#d2d2d7'}`,
@@ -96,7 +97,6 @@ export default function ArticlePhonePreview({
overflow: 'auto',
}}
>
{/* 标签 */}
<div
style={{
fontSize: 11,
@@ -110,138 +110,95 @@ export default function ArticlePhonePreview({
</div>
{/* ══════ iPhone 17 Pro Max 外壳 ══════ */}
{/* 铝合金一体化机身 */}
<div
style={{
width: PHONE_W + BEZEL * 2,
height: PHONE_H + BEZEL * 2,
borderRadius: PHONE_R + BEZEL,
background: `linear-gradient(165deg, ${AL_LIGHT} 0%, ${AL_BODY} 30%, ${AL_DARK} 70%, ${AL_BODY} 100%)`,
padding: 0,
// 高度不设固定值,让屏幕撑开
position: 'relative',
background: `linear-gradient(165deg, ${AL_LIGHT} 0%, ${AL_BODY} 30%, ${AL_DARK} 70%, ${AL_BODY} 100%)`,
boxShadow: [
// 主阴影
'0 1px 2px rgba(0,0,0,0.06)',
'0 4px 12px rgba(0,0,0,0.08)',
'0 12px 32px rgba(0,0,0,0.10)',
'0 24px 64px rgba(0,0,0,0.06)',
// 铝合金高光 — 顶部
'inset 0 1px 0 rgba(255,255,255,0.35)',
'inset 1px 0 0 rgba(255,255,255,0.15)',
// 铝合金暗部 — 底部
'inset 0 -1px 0 rgba(0,0,0,0.15)',
'inset -1px 0 0 rgba(0,0,0,0.08)',
].join(', '),
}}
>
{/* ── 侧边按钮 ── */}
{/* 音量+ (左侧) */}
<div
style={{
position: 'absolute',
left: -BTN_OUT,
top: 118,
width: BTN_OUT + 1,
height: 26,
position: 'absolute', left: -BTN_OUT, top: 170,
width: BTN_OUT + 1, height: 36,
background: `linear-gradient(180deg, ${AL_LIGHT}, ${AL_DARK})`,
borderRadius: '2px 0 0 2px',
zIndex: 5,
borderRadius: '2px 0 0 2px', zIndex: 5,
}}
/>
{/* 音量- (左侧) */}
<div
style={{
position: 'absolute',
left: -BTN_OUT,
top: 152,
width: BTN_OUT + 1,
height: 26,
position: 'absolute', left: -BTN_OUT, top: 218,
width: BTN_OUT + 1, height: 36,
background: `linear-gradient(180deg, ${AL_LIGHT}, ${AL_DARK})`,
borderRadius: '2px 0 0 2px',
zIndex: 5,
borderRadius: '2px 0 0 2px', zIndex: 5,
}}
/>
{/* Action Button (左侧) */}
<div
style={{
position: 'absolute',
left: -BTN_OUT - 1,
top: 188,
width: 11,
height: 11,
position: 'absolute', left: -BTN_OUT - 1, top: 268,
width: 14, height: 14,
background: `radial-gradient(circle at 40% 40%, ${AL_LIGHT}, ${AL_DARK})`,
borderRadius: '50%',
zIndex: 5,
borderRadius: '50%', zIndex: 5,
}}
/>
{/* 电源键 (右侧) */}
<div
style={{
position: 'absolute',
right: -BTN_OUT,
top: 148,
width: BTN_OUT + 1,
height: 36,
position: 'absolute', right: -BTN_OUT, top: 210,
width: BTN_OUT + 1, height: 48,
background: `linear-gradient(180deg, ${AL_LIGHT}, ${AL_DARK})`,
borderRadius: '0 2px 2px 0',
zIndex: 5,
borderRadius: '0 2px 2px 0', zIndex: 5,
}}
/>
{/* ── 屏幕容器 (黑边 + 圆角) ── */}
{/* ── 屏幕容器 ── */}
<div
style={{
position: 'absolute',
top: BEZEL,
left: BEZEL,
right: BEZEL,
bottom: BEZEL,
margin: BEZEL,
borderRadius: SCREEN_R,
background: '#000',
overflow: 'hidden',
position: 'relative',
}}
>
{/* ── Dynamic Island ── */}
<div
style={{
position: 'absolute',
top: ISLAND_TOP,
left: '50%',
position: 'absolute', top: ISLAND_TOP, left: '50%',
transform: 'translateX(-50%)',
width: ISLAND_W,
height: ISLAND_H,
background: '#000',
borderRadius: ISLAND_R,
zIndex: 30,
boxShadow: 'inset 0 0.5px 1px rgba(0,0,0,0.5)',
width: ISLAND_W, height: ISLAND_H,
background: '#000', borderRadius: ISLAND_R,
zIndex: 30, boxShadow: 'inset 0 0.5px 1px rgba(0,0,0,0.5)',
}}
>
{/* 接近传感器 (左侧小点) */}
<div
style={{
position: 'absolute',
left: SENSOR_LEFT,
top: '50%',
position: 'absolute', left: SENSOR_LEFT, top: '50%',
transform: 'translateY(-50%)',
width: SENSOR_SIZE,
height: SENSOR_SIZE,
borderRadius: '50%',
background: '#1a1a2e',
width: SENSOR_SIZE, height: SENSOR_SIZE,
borderRadius: '50%', background: '#1a1a2e',
boxShadow: 'inset 0 0 1px rgba(80,80,120,0.4)',
}}
/>
{/* 前置摄像头 (右侧) */}
<div
style={{
position: 'absolute',
right: CAM_RIGHT,
top: '50%',
position: 'absolute', right: CAM_RIGHT, top: '50%',
transform: 'translateY(-50%)',
width: CAM_SIZE,
height: CAM_SIZE,
width: CAM_SIZE, height: CAM_SIZE,
borderRadius: '50%',
background:
'radial-gradient(circle at 35% 35%, #3a3a5c, #1a1a2e 50%, #0d0d1a)',
background: 'radial-gradient(circle at 35% 35%, #3a3a5c, #1a1a2e 50%, #0d0d1a)',
boxShadow: [
'0 0 0 1px rgba(60,60,80,0.5)',
'inset 0 0 2px rgba(100,100,180,0.3)',
@@ -251,232 +208,173 @@ export default function ArticlePhonePreview({
/>
</div>
{/* ── 状态栏 (iOS 26 风格) ── */}
{/* ── 状态栏 ── */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
position: 'absolute', top: 0, left: 0, right: 0,
height: STATUS_H,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 26px',
fontSize: 11.5,
fontWeight: 600,
color: '#fff',
zIndex: 20,
fontFamily:
'-apple-system, "SF Pro Text", "Helvetica Neue", sans-serif',
display: 'flex', alignItems: 'center',
justifyContent: 'space-between', padding: '0 30px',
fontSize: 14, fontWeight: 600, color: '#fff', zIndex: 20,
fontFamily: '-apple-system, "SF Pro Text", "Helvetica Neue", sans-serif',
}}
>
{/* 左侧: 时间 */}
<span style={{ width: 42, letterSpacing: 0.2 }}>9:41</span>
{/* 右侧: 信号 + WiFi + 电池 */}
<span style={{ width: 50, letterSpacing: 0.2 }}>9:41</span>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 5,
width: 78,
justifyContent: 'flex-end',
display: 'flex', alignItems: 'center', gap: 6,
width: 95, justifyContent: 'flex-end',
}}
>
{/* 蜂窝信号 (4格) */}
<svg width="15" height="11" viewBox="0 0 17 12" fill="none">
<svg width="17" height="12" viewBox="0 0 17 12" fill="none">
<rect x="0" y="9" width="3" height="3" rx="0.6" fill="#fff" />
<rect x="4.5" y="6" width="3" height="6" rx="0.6" fill="#fff" />
<rect x="9" y="3" width="3" height="9" rx="0.6" fill="#fff" />
<rect x="13.5" y="0" width="3" height="12" rx="0.6" fill="#fff" />
</svg>
{/* WiFi */}
<svg width="14" height="10" viewBox="0 0 20 15" fill="#fff">
<svg width="16" height="12" viewBox="0 0 20 15" fill="#fff">
<circle cx="10" cy="13" r="1.4" />
<path
d="M6 10.5a5.5 5.5 0 018 0"
stroke="#fff"
strokeWidth="1.6"
fill="none"
strokeLinecap="round"
/>
<path
d="M2.5 7a10 10 0 0115 0"
stroke="#fff"
strokeWidth="1.6"
fill="none"
strokeLinecap="round"
/>
<path d="M6 10.5a5.5 5.5 0 018 0" stroke="#fff" strokeWidth="1.6" fill="none" strokeLinecap="round" />
<path d="M2.5 7a10 10 0 0115 0" stroke="#fff" strokeWidth="1.6" fill="none" strokeLinecap="round" />
</svg>
{/* 电池 */}
<svg width="24" height="11" viewBox="0 0 27 13" fill="none">
<rect
x="0.5"
y="0.5"
width="22"
height="12"
rx="2.5"
stroke="rgba(255,255,255,0.55)"
strokeWidth="1"
/>
<rect
x="2"
y="2"
width="17"
height="9"
rx="1.2"
fill="#fff"
/>
<rect
x="23.5"
y="3.5"
width="2.5"
height="6"
rx="1"
fill="rgba(255,255,255,0.35)"
/>
<svg width="27" height="13" viewBox="0 0 27 13" fill="none">
<rect x="0.5" y="0.5" width="22" height="12" rx="2.5" stroke="rgba(255,255,255,0.55)" strokeWidth="1" />
<rect x="2" y="2" width="17" height="9" rx="1.2" fill="#fff" />
<rect x="23.5" y="3.5" width="2.5" height="6" rx="1" fill="rgba(255,255,255,0.35)" />
</svg>
</div>
</div>
{/* ── 内容区域 ── */}
{/* ── 内容区域 — 与小程序 article/detail 完全一致的样式 ── */}
<div
style={{
position: 'absolute',
top: STATUS_H - 4,
left: 0,
right: 0,
bottom: HOME_BOTTOM + HOME_H + 8,
overflowY: 'auto',
overflowX: 'hidden',
position: 'absolute', top: STATUS_H - 4, left: 0, right: 0,
bottom: HOME_BOTTOM + HOME_H + 12,
overflowY: 'auto', overflowX: 'hidden',
background: mpColors.bg,
borderRadius: `0 0 ${SCREEN_R - 4}px ${SCREEN_R - 4}px`,
WebkitOverflowScrolling: 'touch',
}}
>
{/* 作用域样式 — 匹配小程序 article/detail */}
<style>{`
.mp-preview {
background: ${mpColors.bg};
padding-bottom: 28px;
padding-bottom: 40px;
min-height: 100%;
}
.mp-preview .mp-header {
background: ${mpColors.card};
padding: 18px 14px;
padding: 24px 20px;
margin-bottom: 1px;
}
.mp-preview .mp-title {
font-size: 17px;
font-size: 22px;
font-weight: 700;
color: ${mpColors.text};
line-height: 1.45;
margin-bottom: 8px;
margin-bottom: 12px;
display: block;
}
.mp-preview .mp-meta {
display: flex;
align-items: center;
gap: 6px;
gap: 8px;
flex-wrap: wrap;
}
.mp-preview .mp-category {
font-size: 10px;
font-size: 12px;
color: ${mpColors.primary};
background: ${mpColors.primaryLight};
padding: 2px 7px;
border-radius: 8px;
padding: 3px 10px;
border-radius: 10px;
}
.mp-preview .mp-author {
font-size: 10px;
font-size: 13px;
color: ${mpColors.textSecondary};
}
.mp-preview .mp-date {
font-size: 10px;
font-size: 13px;
color: #9ca3af;
}
.mp-preview .mp-cover {
width: 100%;
border-radius: 6px;
margin: 0 0 8px;
max-height: 110px;
border-radius: 10px;
margin: 0 0 12px;
max-height: 200px;
object-fit: cover;
}
.mp-preview .mp-summary {
background: ${mpColors.card};
padding: 12px 14px;
padding: 16px 20px;
margin-bottom: 1px;
}
.mp-preview .mp-summary-text {
font-size: 12px;
font-size: 15px;
color: ${mpColors.textSecondary};
line-height: 1.7;
border-left: 2.5px solid ${mpColors.primary};
padding-left: 10px;
line-height: 1.75;
border-left: 3px solid ${mpColors.primary};
padding-left: 14px;
}
.mp-preview .mp-content {
background: ${mpColors.card};
padding: 14px;
padding: 20px;
}
.mp-preview .mp-content p {
font-size: 13px;
font-size: 16px;
color: ${mpColors.text};
line-height: 1.85;
margin-bottom: 10px;
margin-bottom: 14px;
}
.mp-preview .mp-content h1,
.mp-preview .mp-content h2,
.mp-preview .mp-content h3 {
font-weight: 700;
color: ${mpColors.text};
margin: 14px 0 8px;
margin: 18px 0 10px;
}
.mp-preview .mp-content h1 { font-size: 16px; }
.mp-preview .mp-content h2 { font-size: 14px; }
.mp-preview .mp-content h3 { font-size: 13px; }
.mp-preview .mp-content h1 { font-size: 20px; }
.mp-preview .mp-content h2 { font-size: 18px; }
.mp-preview .mp-content h3 { font-size: 16px; }
.mp-preview .mp-content img {
max-width: 100%;
border-radius: 6px;
margin: 8px 0;
border-radius: 8px;
margin: 10px 0;
}
.mp-preview .mp-content blockquote {
border-left: 2.5px solid ${mpColors.primary};
padding: 5px 10px;
border-left: 3px solid ${mpColors.primary};
padding: 8px 14px;
color: ${mpColors.textSecondary};
margin: 10px 0;
margin: 14px 0;
}
.mp-preview .mp-content ul,
.mp-preview .mp-content ol {
padding-left: 16px;
margin: 8px 0;
font-size: 13px;
padding-left: 22px;
margin: 10px 0;
font-size: 16px;
line-height: 1.9;
color: ${mpColors.text};
}
.mp-preview .mp-content table {
width: 100%;
border-collapse: collapse;
margin: 8px 0;
font-size: 12px;
margin: 10px 0;
font-size: 14px;
}
.mp-preview .mp-content td,
.mp-preview .mp-content th {
border: 1px solid #e5e7eb;
padding: 5px 7px;
padding: 8px 10px;
}
.mp-preview .mp-content hr {
border: none;
border-top: 1px dashed #d1d5db;
margin: 12px 0;
margin: 16px 0;
}
.mp-preview .mp-empty {
padding: 40px 14px;
padding: 60px 20px;
text-align: center;
color: #9ca3af;
font-size: 12px;
font-size: 15px;
line-height: 1.8;
}
.mp-preview .mp-content div[data-w-e-type="styled-block"] {
@@ -517,9 +415,7 @@ export default function ArticlePhonePreview({
<div className="mp-content">
{content && content !== '<p><br></p>' ? (
<div
dangerouslySetInnerHTML={{ __html: content }}
/>
<div dangerouslySetInnerHTML={{ __html: content }} />
) : (
<div className="mp-empty">
@@ -534,85 +430,28 @@ export default function ArticlePhonePreview({
{/* ── Home Indicator ── */}
<div
style={{
position: 'absolute',
bottom: HOME_BOTTOM,
left: '50%',
transform: 'translateX(-50%)',
width: HOME_W,
height: HOME_H,
position: 'absolute', bottom: HOME_BOTTOM,
left: '50%', transform: 'translateX(-50%)',
width: HOME_W, height: HOME_H,
background: 'rgba(255,255,255,0.22)',
borderRadius: 99,
zIndex: 20,
borderRadius: 99, zIndex: 20,
}}
/>
</div>
{/* ── 背面横向相机条 (iPhone 17 Pro Max 标志性设计) ── */}
{/* 铝合金外壳上方的相机条凸起暗示 */}
{/* ── 背面横向相机条 ── */}
<div
style={{
position: 'absolute',
top: BEZEL + 10,
left: BEZEL + 6,
width: 110,
height: 28,
borderRadius: 8,
boxShadow:
'inset 0 0 4px rgba(0,0,0,0.06), 0 0 2px rgba(0,0,0,0.03)',
zIndex: 0,
pointerEvents: 'none',
position: 'absolute', top: BEZEL + 12, left: BEZEL + 8,
width: 150, height: 36, borderRadius: 10,
boxShadow: 'inset 0 0 4px rgba(0,0,0,0.05), 0 0 2px rgba(0,0,0,0.02)',
zIndex: 0, pointerEvents: 'none',
}}
>
{/* 三个镜头位置 — 横向排列 */}
<div
style={{
position: 'absolute',
top: 5,
left: 10,
width: 18,
height: 18,
borderRadius: '50%',
background:
'radial-gradient(circle, rgba(0,0,0,0.07), transparent)',
}}
/>
<div
style={{
position: 'absolute',
top: 5,
left: 34,
width: 18,
height: 18,
borderRadius: '50%',
background:
'radial-gradient(circle, rgba(0,0,0,0.07), transparent)',
}}
/>
<div
style={{
position: 'absolute',
top: 5,
left: 58,
width: 18,
height: 18,
borderRadius: '50%',
background:
'radial-gradient(circle, rgba(0,0,0,0.07), transparent)',
}}
/>
{/* 闪光灯位置 */}
<div
style={{
position: 'absolute',
top: 9,
right: 8,
width: 10,
height: 10,
borderRadius: '50%',
background:
'radial-gradient(circle, rgba(0,0,0,0.04), transparent)',
}}
/>
<div style={{ position: 'absolute', top: 7, left: 12, width: 22, height: 22, borderRadius: '50%', background: 'radial-gradient(circle, rgba(0,0,0,0.06), transparent)' }} />
<div style={{ position: 'absolute', top: 7, left: 42, width: 22, height: 22, borderRadius: '50%', background: 'radial-gradient(circle, rgba(0,0,0,0.06), transparent)' }} />
<div style={{ position: 'absolute', top: 7, left: 72, width: 22, height: 22, borderRadius: '50%', background: 'radial-gradient(circle, rgba(0,0,0,0.06), transparent)' }} />
<div style={{ position: 'absolute', top: 11, right: 10, width: 14, height: 14, borderRadius: '50%', background: 'radial-gradient(circle, rgba(0,0,0,0.03), transparent)' }} />
</div>
</div>
</div>