This commit is contained in:
大哥大哥的大哥哥
2026-09-09 14:47:29 +08:00
parent 4d9da40abd
commit 65755c9e96
832 changed files with 412085 additions and 0 deletions
@@ -0,0 +1,121 @@
const memoryCards = require('../../data/memoryCards')
const { getProgress } = require('../../utils/storage')
const { getCollectedMemories } = require('../../utils/memoryCollection')
const { chapterRoute } = require('../../utils/chapterRoute')
const {
getHomeLayoutMetrics,
getHomeLayoutStyle,
hasReadingProgress,
} = require('./homeLayout')
Page({
data: {
coverOpened: false,
completedCount: 0,
lastChapter: 1,
hasReadingProgress: false,
memoryCount: 0,
homeLayoutStyle: [
'height:390px',
'--home-top-inset:8px',
'--home-right-inset:10px',
'--home-bottom-inset:7px',
'--home-left-inset:10px',
].join(';'),
},
onLoad() {
this.applyHomeLayout()
},
onShow() {
const progress = getProgress()
this.setData({
completedCount: progress.completedChapters.length,
lastChapter: progress.lastChapter || 1,
hasReadingProgress: hasReadingProgress(progress),
memoryCount: getCollectedMemories(progress, memoryCards).length,
})
this.applyHomeLayout()
},
onResize(resizeInfo) {
this.applyHomeLayout(resizeInfo)
},
applyHomeLayout(resizeInfo = null) {
let windowInfo = {}
try {
windowInfo = wx.getWindowInfo
? wx.getWindowInfo()
: wx.getSystemInfoSync()
} catch (error) {
windowInfo = {}
}
const resizeSize = resizeInfo && resizeInfo.size
? resizeInfo.size
: resizeInfo
if (resizeSize) {
windowInfo = {
...windowInfo,
windowWidth: resizeSize.windowWidth || windowInfo.windowWidth,
windowHeight: resizeSize.windowHeight || windowInfo.windowHeight,
}
}
let menuRect = {}
try {
menuRect = wx.getMenuButtonBoundingClientRect
? wx.getMenuButtonBoundingClientRect()
: {}
} catch (error) {
menuRect = {}
}
const metrics = getHomeLayoutMetrics(windowInfo, menuRect)
this.setData({
homeLayoutStyle: getHomeLayoutStyle(metrics),
})
},
startGame() {
const chapter = this.data.lastChapter || 1
wx.navigateTo({
url: chapterRoute(chapter),
})
},
revealCover() {
if (this.data.coverOpened) return
this.setData({ coverOpened: true })
},
keepCoverOpen() {},
openCatalog() {
wx.navigateTo({ url: '/pages/catalog/catalog' })
},
openCast() {
wx.navigateTo({ url: '/pages/cast/cast' })
},
openMemories() {
wx.navigateTo({ url: '/pages/memories/memories' })
},
onShareAppMessage() {
return {
title: '唐侦探:在一桌饭里,看见被忽略的人',
path: '/pages/home/home',
}
},
onShareTimeline() {
return {
title: '唐侦探:在一桌饭里,看见被忽略的人',
query: '',
}
},
})
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "唐侦探",
"pageOrientation": "landscape"
}
@@ -0,0 +1,65 @@
<view class="safe-shell home-shell" style="{{homeLayoutStyle}}">
<view
class="home-comic-book {{coverOpened ? 'is-open' : 'is-closed'}}"
bindtap="revealCover"
aria-label="{{coverOpened ? '唐侦探漫画书已经翻开' : '唐侦探漫画封面,轻触翻开'}}"
>
<view class="home-cover-art" aria-label="桂香饭店人物群像漫画封面">
<image
class="home-cover-image"
src="/assets/comic/s01-c01/S01-C01-P02-missing-table-ensemble-v1.jpg"
mode="aspectFill"
></image>
<view class="home-cover-shade"></view>
<view class="home-cover-spine"><text>甄养堂 · 中国健康连环画</text></view>
<view class="home-cover-title-block">
<text class="home-cover-series">第一季</text>
<text class="home-cover-title">唐侦探</text>
<text class="home-cover-subtitle">桂香里的第十五桌</text>
</view>
<view wx:if="{{!coverOpened}}" class="home-cover-touch">
<text class="home-cover-touch-mark">›</text>
<text>翻开瞧瞧</text>
</view>
</view>
<view
wx:if="{{coverOpened}}"
class="home-flyleaf paper-panel"
catchtap="keepCoverOpen"
aria-label="漫画书扉页"
>
<view class="home-brand">
<view class="seal">甄</view>
<view class="home-brand-copy">
<text class="home-brand-name">甄养堂</text>
<text class="home-brand-kind">一本能看、能点、能带回家聊的中国健康连环画</text>
</view>
</view>
<text class="home-flyleaf-title">桂香里的第十五桌</text>
<text class="home-season">第一季 · 一桌饭里的三代人</text>
<text class="home-quote">“一桌饭,不应该只有坐下的人,还应该有被看见的人。”</text>
<button
class="home-primary"
catchtap="startGame"
aria-label="{{hasReadingProgress ? '继续阅读上次看到的地方' : '翻开第一回'}}"
>
{{hasReadingProgress ? '接着上回往下看' : '翻开第一回'}}
</button>
<view class="home-tabs">
<button class="home-tab" catchtap="openCatalog" aria-label="打开十五回目录">目录</button>
<button class="home-tab" catchtap="openCast" aria-label="打开人物画谱">人物</button>
<button class="home-tab home-memory-tab" catchtap="openMemories" aria-label="打开我的桂香岁月">
岁月册<text wx:if="{{memoryCount > 0}}"> · {{memoryCount}}页</text>
</button>
</view>
<text class="home-tagline">先看画、听故事;翻到背面,再聊聊这回事。</text>
</view>
</view>
<text wx:if="{{coverOpened}}" class="home-disclaimer">这里聊的是日常生活,不替代诊断、处方或个体化治疗建议。</text>
</view>
@@ -0,0 +1,678 @@
.home-shell {
--home-top-inset: calc(8px + constant(safe-area-inset-top));
--home-right-inset: calc(10px + constant(safe-area-inset-right));
--home-bottom-inset: calc(7px + constant(safe-area-inset-bottom));
--home-left-inset: calc(10px + constant(safe-area-inset-left));
--home-top-inset: calc(8px + env(safe-area-inset-top));
--home-right-inset: calc(10px + env(safe-area-inset-right));
--home-bottom-inset: calc(7px + env(safe-area-inset-bottom));
--home-left-inset: calc(10px + env(safe-area-inset-left));
display: flex;
height: 100vh;
min-height: 0;
padding: var(--home-top-inset) var(--home-right-inset)
var(--home-bottom-inset) var(--home-left-inset);
gap: 6px;
overflow: hidden;
flex-direction: column;
}
.home-comic-book {
position: relative;
display: grid;
width: 100%;
min-height: 0;
flex: 1 1 auto;
overflow: hidden;
color: #33251b;
background: #1d130f;
border: 3px solid #9e845e;
border-radius: 10px;
box-shadow: 0 15px 34px rgba(0, 0, 0, 0.36);
grid-template-columns: minmax(0, 1fr);
}
.home-comic-book.is-open {
grid-template-columns: minmax(0, 1.45fr) minmax(280px, 1fr);
}
.home-cover-art {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
background: #241811;
}
.home-comic-book.is-open .home-cover-art {
border-right: 7px solid #6f2b23;
box-shadow: 10px 0 24px rgba(32, 19, 12, 0.34);
}
.home-cover-image,
.home-cover-shade {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.home-cover-shade {
pointer-events: none;
background:
linear-gradient(90deg, rgba(29, 18, 12, 0.68), transparent 24%, transparent 72%, rgba(29, 18, 12, 0.2)),
linear-gradient(0deg, rgba(31, 18, 12, 0.82), transparent 53%);
box-shadow: inset 0 0 70px rgba(30, 16, 9, 0.3);
}
.home-cover-spine {
position: absolute;
z-index: 3;
top: 0;
bottom: 0;
left: 0;
display: flex;
width: 46px;
align-items: center;
justify-content: center;
color: #f0d8a7;
background: rgba(74, 35, 26, 0.94);
border-right: 2px solid #c59a58;
font-family: "Songti SC", "STSong", serif;
font-size: 15px;
font-weight: 850;
letter-spacing: 3px;
writing-mode: vertical-rl;
}
.home-cover-title-block {
position: absolute;
z-index: 3;
left: 72px;
bottom: 54px;
display: flex;
max-width: 64%;
padding: 13px 19px 15px;
color: #f7e7c4;
background: rgba(43, 27, 18, 0.86);
border-left: 7px solid #a53a30;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
flex-direction: column;
}
.home-cover-series {
color: #e4c277;
font-size: 16px;
font-weight: 800;
letter-spacing: 4px;
}
.home-cover-title {
margin-top: 3px;
font-family: "Songti SC", "STSong", serif;
font-size: 43px;
font-weight: 900;
letter-spacing: 8px;
line-height: 1.05;
}
.home-cover-subtitle {
margin-top: 6px;
color: #f1d18c;
font-family: "Songti SC", "STSong", serif;
font-size: 25px;
font-weight: 900;
line-height: 1.2;
}
.home-cover-touch {
position: absolute;
z-index: 4;
right: 24px;
bottom: 22px;
display: flex;
min-height: 50px;
align-items: center;
gap: 9px;
padding: 8px 15px;
color: #fff0ca;
background: rgba(54, 34, 23, 0.9);
border: 2px solid #d7b36d;
border-radius: 999px;
font-size: 18px;
font-weight: 850;
box-shadow: 0 7px 18px rgba(0, 0, 0, 0.28);
}
.home-cover-touch-mark {
display: flex;
width: 31px;
height: 31px;
align-items: center;
justify-content: center;
color: #6c271f;
background: #f0d28c;
border-radius: 50%;
font-size: 28px;
line-height: 1;
}
.home-flyleaf {
display: flex;
box-sizing: border-box;
min-width: 0;
min-height: 0;
justify-content: center;
overflow: hidden;
padding: 16px 20px;
background:
repeating-linear-gradient(0deg, rgba(92, 61, 35, 0.03) 0, rgba(92, 61, 35, 0.03) 1px, transparent 1px, transparent 5px),
#f2e4bf;
flex-direction: column;
}
.home-flyleaf-title {
display: block;
margin-top: 9px;
color: #8d2d26;
font-family: "Songti SC", "STSong", serif;
font-size: 29px;
font-weight: 900;
line-height: 1.18;
}
.home-flyleaf .home-quote {
position: static;
display: block;
margin-top: 10px;
color: #4e3a2b;
font-size: 18px;
font-weight: 750;
line-height: 1.45;
}
.home-flyleaf .home-primary {
margin-top: 12px;
}
.home-flyleaf .home-tabs {
width: 100%;
min-width: 0;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.home-flyleaf .home-tab {
box-sizing: border-box;
width: 100%;
min-width: 0;
overflow: hidden;
padding: 5px 7px;
font-size: 16px;
white-space: nowrap;
}
.home-comic-book button::after {
border: 0;
}
.home-book {
display: grid;
width: 100%;
min-height: 0;
flex: 1;
overflow: hidden;
grid-template-columns: minmax(0, 3fr) minmax(285px, 2fr);
border: 2px solid #9e845e;
border-radius: 9px;
}
.home-art {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
background:
repeating-linear-gradient(0deg, rgba(65, 42, 25, 0.04) 0, rgba(65, 42, 25, 0.04) 1px, transparent 1px, transparent 5px),
#241811;
border-right: 5px solid var(--cinnabar);
}
.home-art-image,
.home-art-shade {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.home-art-shade {
pointer-events: none;
background: linear-gradient(0deg, rgba(35, 23, 16, 0.82), transparent 48%);
}
.home-memory-ribbon {
position: absolute;
z-index: 3;
top: 12px;
left: 12px;
display: flex;
min-height: 48px;
align-items: center;
gap: 7px;
padding: 6px 10px 6px 7px;
color: #4a3225;
background: rgba(241, 221, 178, 0.96);
border: 2px solid #9e754a;
border-radius: 5px;
box-shadow: 0 5px 14px rgba(25, 15, 8, 0.28);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 17px;
font-weight: 850;
white-space: nowrap;
}
.home-memory-ribbon-mark {
display: flex;
width: 28px;
height: 35px;
align-items: center;
justify-content: center;
color: #ffe9bb;
background: #963128;
clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 78%, 0 100%);
font-size: 14px;
font-weight: 900;
}
.home-quote {
position: absolute;
z-index: 2;
right: 22px;
bottom: 18px;
left: 22px;
color: #f5e5c0;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 20px;
font-weight: 800;
line-height: 1.5;
}
.home-copy {
display: flex;
box-sizing: border-box;
min-width: 0;
min-height: 0;
justify-content: center;
overflow-x: hidden;
overflow-y: auto;
padding: 18px 24px;
flex-direction: column;
}
.home-brand {
display: flex;
align-items: center;
gap: 10px;
}
.home-brand .seal {
width: 48px;
height: 48px;
flex: none;
font-size: 27px;
}
.home-brand-copy {
display: flex;
color: #7f2a23;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 18px;
font-weight: 850;
line-height: 1.35;
}
.home-brand-kind {
color: #765f45;
font-size: 16px;
font-weight: 700;
}
.home-title {
display: block;
margin-top: 10px;
font-size: 45px;
font-weight: 900;
letter-spacing: 7px;
line-height: 1.05;
}
.home-subtitle {
display: block;
margin-top: 4px;
color: #8d2d26;
font-size: 30px;
font-weight: 900;
line-height: 1.2;
}
.home-season {
display: block;
margin-top: 5px;
color: #6f5942;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 18px;
font-weight: 750;
}
.home-primary {
display: flex;
width: 100%;
min-height: 64px;
align-items: center;
justify-content: center;
margin-top: 16px;
padding: 8px 14px;
color: #fff0cb;
background: var(--cinnabar);
border: 3px solid #74231d;
border-radius: 8px;
box-shadow: 0 8px 18px rgba(111, 32, 27, 0.24);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 21px;
font-weight: 900;
}
.home-tabs {
display: grid;
gap: 9px;
margin-top: 10px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.home-tab {
position: relative;
display: flex;
min-width: 0;
min-height: 48px;
align-items: center;
justify-content: center;
padding: 6px 10px;
color: #4b3829;
background: #ead9b2;
border: 2px solid #997b54;
border-radius: 7px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 18px;
font-weight: 850;
}
.home-memory-count {
display: flex;
min-width: 30px;
height: 26px;
align-items: center;
justify-content: center;
padding: 0 6px;
color: #f9edce;
background: #704b35;
border-radius: 999px;
font-size: 13px;
}
.home-tagline {
display: block;
margin-top: 10px;
color: #594431;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 18px;
font-weight: 700;
line-height: 1.48;
}
.home-disclaimer {
display: block;
flex: none;
color: #d7c49b;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 17px;
line-height: 1.35;
text-align: center;
}
@media (max-height: 620px) {
.home-copy {
justify-content: flex-start;
overflow-y: hidden;
padding: 12px 18px;
}
.home-brand .seal {
width: 42px;
height: 42px;
font-size: 24px;
}
.home-brand-copy {
font-size: 17px;
}
.home-brand-kind {
font-size: 15px;
}
.home-title {
margin-top: 7px;
font-size: 39px;
}
.home-subtitle {
font-size: 27px;
}
.home-season {
font-size: 17px;
}
.home-primary {
min-height: 60px;
margin-top: 11px;
font-size: 20px;
}
.home-tabs {
margin-top: 8px;
}
.home-tab {
min-height: 48px;
font-size: 17px;
}
.home-tagline {
display: block;
margin-top: 6px;
font-size: 16px;
line-height: 1.35;
}
.home-disclaimer {
font-size: 16px;
}
}
@media (max-width: 730px) {
.home-brand-kind {
display: none;
}
.home-brand {
max-width: calc(100% - 104px);
}
.home-title {
font-size: 39px;
letter-spacing: 5px;
}
.home-subtitle {
font-size: 26px;
}
.home-tagline {
display: block;
margin-top: 6px;
font-size: 16px;
line-height: 1.35;
}
.home-memory-ribbon {
top: 9px;
left: 9px;
min-height: 48px;
padding: 5px 8px 5px 6px;
font-size: 16px;
}
}
@media (max-height: 430px) {
.home-comic-book.is-open {
grid-template-columns: minmax(0, 1.3fr) minmax(292px, 1fr);
}
.home-cover-title-block {
left: 62px;
bottom: 34px;
padding: 9px 13px 10px;
}
.home-cover-series {
font-size: 13px;
}
.home-cover-title {
font-size: 34px;
letter-spacing: 6px;
}
.home-cover-subtitle {
font-size: 20px;
}
.home-cover-touch {
right: 16px;
bottom: 13px;
min-height: 48px;
font-size: 16px;
}
.home-flyleaf {
padding: 8px 12px;
}
.home-flyleaf .home-brand .seal {
width: 34px;
height: 34px;
font-size: 20px;
}
.home-flyleaf .home-brand-copy {
font-size: 14px;
}
.home-flyleaf-title {
margin-top: 5px;
font-size: 23px;
}
.home-flyleaf .home-season {
font-size: 14px;
}
.home-flyleaf .home-quote {
margin-top: 5px;
font-size: 15px;
line-height: 1.3;
}
.home-flyleaf .home-primary {
min-height: 50px;
margin-top: 6px;
font-size: 18px;
}
.home-flyleaf .home-tabs {
gap: 5px;
margin-top: 5px;
}
.home-flyleaf .home-tab {
min-height: 48px;
padding: 4px;
font-size: 14px;
}
.home-flyleaf .home-tagline {
margin-top: 4px;
font-size: 14px;
}
.home-copy {
justify-content: flex-start;
overflow-y: hidden;
padding: 8px 14px;
}
.home-brand .seal {
width: 36px;
height: 36px;
font-size: 22px;
}
.home-brand-copy {
font-size: 15px;
}
.home-brand-kind {
font-size: 13px;
}
.home-title {
margin-top: 4px;
font-size: 34px;
letter-spacing: 4px;
}
.home-subtitle {
margin-top: 2px;
font-size: 23px;
}
.home-season {
margin-top: 3px;
font-size: 15px;
}
.home-primary {
min-height: 60px;
margin-top: 7px;
padding: 6px 10px;
font-size: 19px;
}
.home-tabs {
margin-top: 5px;
}
.home-tagline {
display: block;
margin-top: 4px;
font-size: 15px;
line-height: 1.2;
}
.home-disclaimer {
font-size: 14px;
line-height: 1.2;
}
}
@@ -0,0 +1,122 @@
function finiteNumber(value, fallback = 0) {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
function clamp(value, minimum, maximum) {
return Math.min(maximum, Math.max(minimum, value))
}
/**
* 首页使用自定义导航栏,横屏时既要避开左右刘海与底部 Home
* Indicator,也要为微信右上角胶囊留下真实空间。CSS 的 env()
* 在部分微信横屏机型上只会返回系统安全区,不包含胶囊,因此这里
* 以运行时尺寸为准,CSS env() 只作为脚本尚未执行时的兜底。
*/
function getHomeLayoutMetrics(windowInfo = {}, menuRect = {}) {
const windowWidth = Math.max(320, finiteNumber(windowInfo.windowWidth, 844))
const windowHeight = Math.max(240, finiteNumber(windowInfo.windowHeight, 390))
const safeArea = windowInfo.safeArea || {}
const safeLeft = clamp(
finiteNumber(safeArea.left, 0),
0,
windowWidth / 3,
)
const safeRightEdge = clamp(
finiteNumber(safeArea.right, windowWidth),
windowWidth * 2 / 3,
windowWidth,
)
const safeRight = clamp(
windowWidth - safeRightEdge,
0,
windowWidth / 3,
)
const safeTop = Math.max(
0,
finiteNumber(windowInfo.statusBarHeight, 0),
finiteNumber(safeArea.top, 0),
)
const safeBottomEdge = clamp(
finiteNumber(safeArea.bottom, windowHeight),
windowHeight * 2 / 3,
windowHeight,
)
const safeBottom = clamp(
windowHeight - safeBottomEdge,
0,
windowHeight / 3,
)
const menuLeft = finiteNumber(menuRect.left, windowWidth)
const menuBottom = finiteNumber(menuRect.bottom, 0)
const hasMenuRect = (
menuLeft > windowWidth / 2
&& menuLeft < windowWidth
&& menuBottom > 0
)
const compactHeight = windowHeight <= 620
const horizontalPageGap = compactHeight ? 10 : 14
const capsuleGap = compactHeight ? 8 : 10
const leftInset = Math.ceil(safeLeft + horizontalPageGap)
const rightInset = Math.ceil(Math.max(
safeRight + horizontalPageGap,
hasMenuRect
? windowWidth - menuLeft + capsuleGap
: safeRight + horizontalPageGap,
))
const topInset = Math.ceil(safeTop + (compactHeight ? 8 : 12))
const bottomInset = Math.ceil(safeBottom + (compactHeight ? 7 : 12))
return {
windowWidth,
windowHeight,
compactHeight,
safeLeft,
safeRight,
safeTop,
safeBottom,
leftInset,
rightInset,
topInset,
bottomInset,
hasMenuRect,
}
}
function getHomeLayoutStyle(metrics = {}) {
return [
`height:${Math.max(240, finiteNumber(metrics.windowHeight, 390))}px`,
`--home-top-inset:${Math.max(0, finiteNumber(metrics.topInset, 8))}px`,
`--home-right-inset:${Math.max(0, finiteNumber(metrics.rightInset, 10))}px`,
`--home-bottom-inset:${Math.max(0, finiteNumber(metrics.bottomInset, 7))}px`,
`--home-left-inset:${Math.max(0, finiteNumber(metrics.leftInset, 10))}px`,
].join(';')
}
function hasReadingProgress(progress = {}) {
const completedChapters = Array.isArray(progress.completedChapters)
? progress.completedChapters
: []
const completedHotspots = progress.completedHotspots
&& typeof progress.completedHotspots === 'object'
? progress.completedHotspots
: {}
const hasCompletedHotspot = Object.values(completedHotspots).some(
(ids) => Array.isArray(ids) && ids.length > 0,
)
return (
completedChapters.length > 0
|| finiteNumber(progress.lastChapter, 1) > 1
|| hasCompletedHotspot
)
}
module.exports = {
getHomeLayoutMetrics,
getHomeLayoutStyle,
hasReadingProgress,
}