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,119 @@
const rawCast = require('../../data/cast')
const {
getSettings,
saveSettings,
} = require('../../utils/storage')
const readerDetails = {
'tang-shouan': {
relationship: '唐明远的父亲、乐乐的爷爷,也是桂香老邻居信得过的唐大夫。',
visual: '常穿素色深靛蓝外套,背着旧棕布卫生包。衣着朴素,像每天都会遇见的邻家大夫。',
poseCount: 5,
},
'qin-zhicheng': {
relationship: '秦小满的长辈,桂香食堂和饭店里掌勺多年的秦师傅。',
visual: '宽头、粗颈、宽肩,前臂厚实;穿中式粗棉厨工褂、白围裙和低矮布帽。',
poseCount: 5,
},
'lin-xiulan': {
relationship: '桂香厂老工友,也是饭票、账本和旧物的保管人。',
visual: '常穿枣红或深棕工作外套。她不急着训人,更愿意用行动把事情说明白。',
poseCount: 5,
},
'zhao-jianguo': {
relationship: '大家叫他赵伯,是桂香厂出了名的劳动骨干和老工友。',
visual: '青年时穿结实工装,老年戴深蓝便帽、捧盖碗茶;豪爽有劲,也保留普通人的体面。',
poseCount: 5,
},
'tang-mingyuan': {
relationship: '唐守安的儿子、乐乐的父亲,和许多中年人一样忙工作也顾家。',
visual: '衣着随年代变化,腰腹和颈围也慢慢有了变化。这是多年生活留下的痕迹,不拿他的身形开玩笑。',
poseCount: 4,
},
'qin-xiaoman': {
relationship: '秦师傅的晚辈,如今接手照看桂香饭店。',
visual: '穿简洁的现代中式工作装,常带着软尺、纸质座位单和调台记录。',
poseCount: 4,
},
lele: {
relationship: '唐明远的儿子、唐守安的孙子,是家里最敢把问题问出来的人。',
visual: '圆润、活泼、讨人喜欢。大人从家庭习惯上找原因,不把责任推给孩子。',
poseCount: 4,
},
xiaozhen: {
relationship: '在现代生活里陪大家整理健康信息的人,需要时提醒大家联系专业医护。',
visual: '灰绿色开衫、白衬衫、低马尾,手里拿着记录夹;总在需要时陪一程,不抢别人的故事。',
poseCount: 4,
},
}
const cast = rawCast.map((character) => ({
...character,
relationship: readerDetails[character.id].relationship,
readerRole: character.id === 'xiaozhen'
? '在故事需要回顾生活习惯时陪大家理一理;遇到拿不准的健康问题,会提醒大家联系专业医护,不替医生作决定。'
: character.role,
readerVisual: readerDetails[character.id].visual,
poseCount: readerDetails[character.id].poseCount,
}))
Page({
data: {
cast,
selected: null,
fontScale: 'large',
castListEnded: false,
},
onLoad() {
const settings = getSettings()
this.setData({
fontScale: settings.fontScale === 'xlarge' ? 'xlarge' : 'large',
castListEnded: false,
})
},
selectCharacter(event) {
const id = event.currentTarget.dataset.id
const selected = cast.find((character) => character.id === id)
this.setData({ selected })
},
closeCharacter() {
this.setData({ selected: null })
},
markListEnd() {
if (!this.data.castListEnded) {
this.setData({ castListEnded: true })
}
},
toggleFont() {
const fontScale = this.data.fontScale === 'large' ? 'xlarge' : 'large'
const settings = getSettings()
settings.fontScale = fontScale
saveSettings(settings)
this.setData({ fontScale })
},
noop() {},
goBack() {
wx.reLaunch({ url: '/pages/home/home' })
},
onShareAppMessage() {
return {
title: '唐侦探人物画谱:认识桂香故事里的人',
path: '/pages/cast/cast',
}
},
onShareTimeline() {
return {
title: '唐侦探人物画谱:认识桂香故事里的人',
query: '',
}
},
})
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "人物画谱",
"pageOrientation": "landscape"
}
@@ -0,0 +1,119 @@
<view class="safe-shell cast-shell font-{{fontScale}}">
<view class="cast-head paper-panel">
<button class="quiet-button cast-back" bindtap="goBack" aria-label="返回封面">‹ 封面</button>
<view class="cast-heading-copy">
<text class="eyebrow">桂香里的熟面孔</text>
<text class="cast-title">人物画谱</text>
</view>
<view class="cast-head-actions">
<button
class="quiet-button font-button"
bindtap="toggleFont"
aria-label="{{fontScale === 'large' ? '把人物画谱文字再放大' : '恢复标准大字'}}"
>
{{fontScale === 'large' ? '再大一点' : '标准大字'}}
</button>
<view class="capsule-safe-space" aria-hidden="true"></view>
</view>
</view>
<scroll-view
class="cast-scroll"
scroll-y
enhanced
show-scrollbar="{{true}}"
lower-threshold="24"
bindscrolltolower="markListEnd"
aria-label="桂香人物列表,可上下滑动"
>
<view class="cast-intro">
<text>点开一位人物,先认清他和家里人的关系,再看看他在桂香里的故事。</text>
</view>
<view class="cast-grid">
<view
wx:for="{{cast}}"
wx:key="id"
class="cast-card paper-panel"
aria-role="button"
aria-label="{{item.name}},{{item.title}}"
hover-class="cast-card-hover"
data-id="{{item.id}}"
bindtap="selectCharacter"
>
<view class="cast-image-wrap">
<view class="cast-portrait-window poses-{{item.poseCount}}">
<image
class="cast-image"
src="{{item.asset}}"
mode="aspectFit"
aria-label="{{item.name}}的全身像"
></image>
</view>
</view>
<view class="cast-copy">
<view class="name-row">
<text class="cast-name">{{item.name}}</text>
<text class="eras">{{item.eras}}</text>
</view>
<text class="cast-role-title">{{item.title}}</text>
<text class="cast-relationship">{{item.relationship}}</text>
<text class="cast-hint">点开看故事</text>
</view>
</view>
</view>
</scroll-view>
<view class="list-scroll-guide" aria-live="polite">
<text>{{castListEnded ? '人物都看见了' : '向下翻,还有更多人物'}}</text>
<text class="scroll-guide-mark" aria-hidden="true">{{castListEnded ? '✓' : '⌄'}}</text>
</view>
<view wx:if="{{selected}}" class="modal-backdrop" bindtap="closeCharacter">
<view
class="character-modal paper-panel"
catchtap="noop"
aria-role="dialog"
aria-label="{{selected.name}}的人物故事"
>
<button class="modal-close" bindtap="closeCharacter" aria-label="关闭人物故事">关闭</button>
<view class="modal-image-wrap">
<view class="modal-portrait-window poses-{{selected.poseCount}}">
<image
class="modal-image"
src="{{selected.asset}}"
mode="aspectFit"
aria-label="{{selected.name}}的全身像"
></image>
</view>
</view>
<view class="modal-copy">
<view class="modal-head">
<view class="modal-heading">
<text class="eyebrow">{{selected.eras}} · {{selected.title}}</text>
<text class="modal-name">{{selected.name}}</text>
</view>
</view>
<scroll-view
class="modal-story-scroll"
scroll-y
enhanced
bounces="{{false}}"
show-scrollbar="{{true}}"
aria-label="{{selected.name}}的人物故事正文,可上下滑动"
>
<view class="modal-story">
<text class="section-label">他和大家的关系</text>
<text class="modal-text">{{selected.relationship}}</text>
<text class="section-label">他在桂香里的故事</text>
<text class="modal-text">{{selected.readerRole}}</text>
<text class="section-label">衣着和模样</text>
<text class="modal-text">{{selected.readerVisual}}</text>
</view>
</scroll-view>
<view class="modal-footer">
<button class="primary-button modal-done" bindtap="closeCharacter">看完了,返回人物谱</button>
</view>
</view>
</view>
</view>
</view>
@@ -0,0 +1,660 @@
.cast-shell {
display: flex;
height: 100vh;
gap: 14rpx;
flex-direction: column;
overflow: hidden;
}
.cast-head {
display: grid;
min-height: 62px;
flex: 0 0 auto;
grid-template-columns: 110px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 6px 10px;
}
.cast-back,
.font-button,
.modal-close,
.modal-done {
min-width: 48px;
min-height: 48px;
}
.cast-back {
width: 100%;
min-width: 0;
padding-right: 8px;
padding-left: 8px;
}
.cast-heading-copy {
display: flex;
min-width: 0;
flex-direction: column;
}
.cast-title {
overflow: hidden;
font-size: 27px;
font-weight: 850;
letter-spacing: 2px;
text-overflow: ellipsis;
white-space: nowrap;
}
.cast-heading-copy .eyebrow {
overflow: hidden;
font-size: 15px;
letter-spacing: 1px;
text-overflow: ellipsis;
white-space: nowrap;
}
.cast-head-actions {
display: flex;
align-items: center;
gap: 10rpx;
justify-self: end;
}
.font-button {
white-space: nowrap;
}
/* The WeChat capsule occupies this part of a custom landscape title bar. */
.capsule-safe-space {
width: 102px;
height: 40px;
flex: 0 0 102px;
}
.cast-scroll {
min-height: 0;
flex: 1;
}
.cast-intro {
padding: 2px 6px 10px;
color: #f4e5bd;
font-size: 17px;
line-height: 1.5;
}
.cast-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
padding: 0 2px 20px;
}
.cast-card {
display: grid;
width: 100%;
min-width: 0;
min-height: 144px;
overflow: hidden;
grid-template-columns: 132px minmax(0, 1fr);
text-align: left;
border-radius: 8px;
}
.cast-card-hover {
transform: translateY(2rpx);
opacity: 0.88;
}
.cast-image-wrap {
display: flex;
min-height: 144px;
align-items: center;
justify-content: center;
overflow: hidden;
background: #d4c29a;
border-right: 5px solid var(--cinnabar);
}
.cast-portrait-window,
.modal-portrait-window {
position: relative;
overflow: hidden;
background: #dfcfaa;
border: 1px solid rgba(98, 65, 40, 0.35);
}
.cast-portrait-window {
height: 126px;
}
.cast-portrait-window.poses-5 {
width: 45px;
}
.cast-portrait-window.poses-4 {
width: 56px;
}
.cast-image {
position: absolute;
top: 0;
left: 0;
width: 224px;
height: 126px;
}
.cast-copy {
display: flex;
justify-content: center;
padding: 10px 13px;
flex-direction: column;
}
.name-row {
display: flex;
align-items: baseline;
gap: 8rpx;
flex-wrap: wrap;
}
.cast-name {
font-size: 23px;
font-weight: 850;
line-height: 1.2;
white-space: nowrap;
}
.eras {
color: var(--muted);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 14px;
white-space: nowrap;
}
.cast-role-title {
margin-top: 4px;
color: #8d3028;
font-size: 17px;
font-weight: 750;
line-height: 1.3;
}
.cast-relationship {
display: -webkit-box;
margin-top: 5px;
overflow: hidden;
color: #554333;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 15px;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.cast-hint {
margin-top: 7px;
color: var(--muted);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 15px;
font-weight: 700;
line-height: 1.35;
}
.list-scroll-guide {
display: flex;
min-height: 40px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
gap: 10px;
color: #f4e5bd;
background: rgba(58, 40, 28, 0.94);
border: 1px solid rgba(222, 194, 139, 0.55);
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 17px;
font-weight: 750;
}
.scroll-guide-mark {
color: #f0c86b;
font-size: 24px;
line-height: 1;
}
.modal-backdrop {
position: fixed;
z-index: 100;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
padding:
calc(10px + constant(safe-area-inset-top))
calc(112px + constant(safe-area-inset-right))
calc(10px + constant(safe-area-inset-bottom))
calc(10px + constant(safe-area-inset-left));
padding:
calc(10px + env(safe-area-inset-top))
calc(112px + env(safe-area-inset-right))
calc(10px + env(safe-area-inset-bottom))
calc(10px + env(safe-area-inset-left));
background: rgba(19, 13, 9, 0.82);
}
.character-modal {
position: relative;
display: grid;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
grid-template-columns: minmax(240px, 42%) minmax(0, 58%);
grid-template-rows: minmax(0, 1fr);
border-radius: 10rpx;
}
.modal-image-wrap {
position: relative;
display: flex;
min-width: 0;
min-height: 0;
align-items: center;
justify-content: center;
overflow: hidden;
background: #d1bf96;
border-right: 6rpx solid #7d2a23;
}
.modal-portrait-window {
height: 560px;
}
.modal-portrait-window.poses-5 {
width: 200px;
}
.modal-portrait-window.poses-4 {
width: 249px;
}
.modal-image {
position: absolute;
top: 0;
left: 0;
width: 996px;
height: 560px;
}
.modal-copy {
position: relative;
display: grid;
min-width: 0;
min-height: 0;
overflow: hidden;
grid-template-rows: minmax(76px, auto) minmax(0, 1fr) minmax(68px, auto);
}
.modal-head {
display: flex;
min-height: 76px;
align-items: center;
padding: 12rpx 88px 8rpx 22rpx;
border-bottom: 2rpx solid rgba(125, 42, 35, 0.22);
}
.modal-heading {
display: flex;
min-width: 0;
overflow: hidden;
flex: 1;
flex-direction: column;
}
.modal-heading .eyebrow {
overflow: hidden;
font-size: 18rpx;
text-overflow: ellipsis;
white-space: nowrap;
}
.modal-name {
margin-top: 2rpx;
font-size: 42rpx;
font-weight: 900;
line-height: 1.12;
}
.modal-close {
position: absolute;
z-index: 3;
top: 12px;
right: 12px;
display: flex;
width: 68px;
height: 48px;
min-width: 68px;
min-height: 48px;
align-items: center;
justify-content: center;
padding: 0 8px;
color: #6f201b;
background: #fff8e5;
border: 2rpx solid #9f6d54;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(48, 37, 28, 0.18);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 18px;
font-weight: 800;
}
.modal-story-scroll {
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
}
.modal-story {
display: flex;
padding: 12rpx 24rpx 28rpx;
flex-direction: column;
}
.section-label {
margin-top: 10rpx;
color: #8d3028;
font-size: 22rpx;
font-weight: 850;
}
.modal-text {
margin-top: 6rpx;
color: #4f3e30;
font-size: 23rpx;
line-height: 1.55;
}
.modal-done {
width: 100%;
min-width: 48px;
min-height: 48px;
height: 48px;
padding: 0 14px;
font-size: 18px;
}
.modal-footer {
display: flex;
min-height: 68px;
align-items: center;
padding: 8px 14px 12px;
background: rgba(243, 229, 189, 0.96);
border-top: 2rpx solid rgba(125, 42, 35, 0.22);
}
.font-xlarge .cast-title {
font-size: 35rpx;
}
.font-xlarge .cast-intro,
.font-xlarge .cast-role-title,
.font-xlarge .section-label {
font-size: 20px;
}
.font-xlarge .cast-name {
font-size: 26px;
}
.font-xlarge .eras,
.font-xlarge .cast-hint,
.font-xlarge .modal-heading .eyebrow {
font-size: 17px;
}
.font-xlarge .cast-relationship,
.font-xlarge .list-scroll-guide {
font-size: 18px;
}
.font-xlarge .modal-name {
font-size: 46rpx;
}
.font-xlarge .modal-text {
font-size: 26rpx;
}
@media (max-height: 620px) {
.cast-shell {
gap: 8px;
}
.cast-scroll {
padding-left: 18px;
}
.cast-head {
min-height: 62px;
grid-template-columns: 96px minmax(0, 1fr) auto;
gap: 8px;
padding: 6px 8px;
}
.cast-back,
.font-button {
min-height: 48px;
font-size: 17px;
}
.font-button {
padding: 0 12px;
}
.cast-title {
font-size: 25px;
}
.cast-heading-copy .eyebrow {
font-size: 15px;
letter-spacing: 2px;
}
.cast-intro {
padding: 0 4px 8px;
font-size: 17px;
}
.cast-grid {
gap: 10px;
padding: 0 2px 16px;
}
.cast-card {
min-height: 144px;
grid-template-columns: 132px minmax(0, 1fr);
}
.cast-image-wrap {
min-height: 144px;
}
.cast-copy {
padding: 8px 10px;
}
.cast-name {
font-size: 22px;
}
.eras {
font-size: 14px;
}
.cast-role-title {
font-size: 17px;
}
.cast-relationship {
font-size: 15px;
}
.cast-hint {
margin-top: 6px;
font-size: 15px;
}
.modal-head {
min-height: 68px;
padding: 8px 82px 6px 14px;
}
.modal-copy {
grid-template-rows: minmax(68px, auto) minmax(0, 1fr) minmax(64px, auto);
}
.modal-heading .eyebrow {
font-size: 14px;
letter-spacing: 1px;
}
.modal-name {
font-size: 30px;
}
.modal-story {
padding: 7px 16px 16px;
}
.modal-close {
top: 10px;
right: 10px;
width: 64px;
min-width: 64px;
height: 48px;
min-height: 48px;
font-size: 17px;
}
.modal-footer {
min-height: 64px;
padding: 8px 12px;
}
.modal-done {
height: 48px;
min-height: 48px;
}
.section-label {
margin-top: 7px;
font-size: 18px;
}
.modal-text {
margin-top: 4px;
font-size: 18px;
line-height: 1.5;
}
.modal-backdrop {
padding-right: calc(98px + constant(safe-area-inset-right));
padding-left: calc(32px + constant(safe-area-inset-left));
padding-right: calc(98px + env(safe-area-inset-right));
padding-left: calc(32px + env(safe-area-inset-left));
}
.modal-portrait-window {
height: 280px;
}
.modal-portrait-window.poses-5 {
width: 100px;
}
.modal-portrait-window.poses-4 {
width: 125px;
}
.modal-image {
width: 498px;
height: 280px;
}
.font-xlarge .cast-title {
font-size: 28px;
}
.font-xlarge .cast-intro,
.font-xlarge .cast-role-title,
.font-xlarge .section-label {
font-size: 20px;
}
.font-xlarge .cast-name {
font-size: 25px;
}
.font-xlarge .eras,
.font-xlarge .cast-hint,
.font-xlarge .modal-heading .eyebrow {
font-size: 16px;
}
.font-xlarge .cast-relationship,
.font-xlarge .list-scroll-guide {
font-size: 17px;
}
.font-xlarge .modal-name {
font-size: 33px;
}
.font-xlarge .modal-text {
font-size: 21px;
}
}
@media (max-width: 720px) and (orientation: landscape) {
.cast-head {
grid-template-columns: 96px minmax(0, 1fr) auto;
gap: 8px;
}
.capsule-safe-space {
width: 88px;
flex-basis: 88px;
}
.font-button {
min-width: 84px;
padding-right: 9px;
padding-left: 9px;
}
.cast-heading-copy .eyebrow {
display: none;
}
.cast-grid {
grid-template-columns: minmax(0, 1fr);
}
.cast-card {
grid-template-columns: 150px minmax(0, 1fr);
}
.modal-backdrop {
padding-right: calc(98px + env(safe-area-inset-right));
}
.character-modal {
grid-template-columns: minmax(210px, 40%) minmax(0, 60%);
}
}
@@ -0,0 +1,102 @@
const chapters = require('../../data/chapters')
const {
getProgress,
saveProgress,
resetStoryProgress,
getSettings,
} = require('../../utils/storage')
const { chapterRoute } = require('../../utils/chapterRoute')
Page({
data: {
chapters: [],
fontScale: 'large',
catalogListEnded: false,
},
onShow() {
const progress = getProgress()
const settings = getSettings()
const completed = new Set(progress.completedChapters)
const lastChapter = Number(progress.lastChapter) || 1
const completedHotspots = progress.completedHotspots || {}
this.setData({
chapters: chapters.map((chapter) => ({
...chapter,
completed: completed.has(chapter.chapterId),
current: chapter.number === lastChapter,
readingLabel: completed.has(chapter.chapterId)
? '重新翻看这一回'
: chapter.number === lastChapter
? '上次读到这里'
: '翻开这一回',
eventCount: Array.isArray(completedHotspots[chapter.chapterId])
? completedHotspots[chapter.chapterId].length
: 0,
})),
lastChapter,
fontScale: settings.fontScale === 'xlarge' ? 'xlarge' : 'large',
catalogListEnded: false,
})
},
markListEnd() {
if (!this.data.catalogListEnded) {
this.setData({ catalogListEnded: true })
}
},
openChapter(event) {
const chapter = Number(event.currentTarget.dataset.chapter)
const progress = getProgress()
const chapterMeta = chapters.find((item) => item.number === chapter)
const replay = Boolean(
chapterMeta
&& progress.completedChapters.includes(chapterMeta.chapterId),
)
progress.lastChapter = chapter
saveProgress(progress)
wx.navigateTo({
url: chapterRoute(chapter, { replay }),
})
},
restartStory() {
wx.showModal({
title: '从第一回重新开始?',
content: '关卡进度会清空;已经收藏的“我的桂香岁月”和大字设置会保留。',
confirmText: '重新开始',
cancelText: '先不清空',
confirmColor: '#9f3028',
success(result) {
if (!result || !result.confirm) return
if (!resetStoryProgress()) {
wx.showToast({
title: '进度暂时没有清空,请稍后再试',
icon: 'none',
})
return
}
wx.redirectTo({ url: chapterRoute(1) })
},
})
},
goBack() {
wx.reLaunch({ url: '/pages/home/home' })
},
onShareAppMessage() {
return {
title: '唐侦探:翻开十五回桂香故事',
path: '/pages/catalog/catalog',
}
},
onShareTimeline() {
return {
title: '唐侦探:翻开十五回桂香故事',
query: '',
}
},
})
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "十五回目录",
"pageOrientation": "landscape"
}
@@ -0,0 +1,63 @@
<view class="safe-shell catalog-shell font-{{fontScale}}">
<view class="catalog-head paper-panel">
<button class="quiet-button" bindtap="goBack" aria-label="返回封面">‹ 封面</button>
<view class="catalog-heading-copy">
<text class="eyebrow">桂香里的第十五桌</text>
<text class="catalog-title catalog-title-full">第一季 · 十五回目录</text>
<text class="catalog-title catalog-title-short">十五回目录</text>
</view>
<button
class="quiet-button restart-story-button"
bindtap="restartStory"
aria-label="从第一回重新开始,清空关卡进度但保留收藏"
>从头再看</button>
</view>
<scroll-view
class="chapter-scroll"
scroll-y
enhanced
show-scrollbar="{{true}}"
lower-threshold="24"
bindscrolltolower="markListEnd"
aria-label="十五回故事目录,可上下滑动"
>
<view class="chapter-grid">
<view
wx:for="{{chapters}}"
wx:key="chapterId"
class="chapter-card paper-panel {{item.completed ? 'completed' : ''}} {{item.current ? 'current' : ''}}"
aria-role="button"
aria-label="第{{item.number}}回,{{item.title}},{{item.readingLabel}}"
hover-class="chapter-card-hover"
data-chapter="{{item.number}}"
bindtap="openChapter"
>
<view class="chapter-number">
<text>第</text>
<text class="number">{{item.number < 10 ? '0' + item.number : item.number}}</text>
<text>回</text>
</view>
<view class="chapter-card-copy">
<text class="year">{{item.year}}</text>
<text class="chapter-name">{{item.title}}</text>
<view class="chapter-badges">
<text class="chapter-badge {{item.completed ? 'done-badge' : item.current ? 'current-badge' : 'read-badge'}}">
{{item.readingLabel}}
</text>
<text wx:if="{{item.current && item.eventCount > 0 && !item.completed}}" class="chapter-progress">
已看见 {{item.eventCount}} 处线索
</text>
</view>
</view>
<text class="open-arrow">›</text>
</view>
</view>
<text class="scroll-ending">十五回故事,都在这本画册里。</text>
</scroll-view>
<view class="list-scroll-guide" aria-live="polite">
<text>{{catalogListEnded ? '已经翻到最后一回' : '向下翻,还有更多故事'}}</text>
<text class="scroll-guide-mark" aria-hidden="true">{{catalogListEnded ? '✓' : '⌄'}}</text>
</view>
</view>
@@ -0,0 +1,364 @@
.catalog-shell {
display: flex;
height: 100vh;
gap: 12px;
flex-direction: column;
overflow: hidden;
}
.catalog-head {
display: grid;
flex: 0 0 auto;
min-height: 64px;
grid-template-columns: 132px minmax(0, 1fr) 132px;
align-items: center;
gap: 14px;
padding: 8px 112px 8px 14px;
}
.restart-story-button {
width: 100%;
min-width: 0;
min-height: 48px;
padding-right: 8px;
padding-left: 8px;
color: #7b2c25;
border-color: #a85d4e;
}
.catalog-heading-copy {
display: flex;
min-width: 0;
flex-direction: column;
}
.catalog-head > .quiet-button {
width: 100%;
min-width: 0;
min-height: 48px;
padding-right: 8px;
padding-left: 8px;
}
.catalog-title {
overflow: hidden;
font-size: 25px;
font-weight: 850;
letter-spacing: 2px;
text-overflow: ellipsis;
white-space: nowrap;
}
.catalog-title-short {
display: none;
}
.catalog-heading-copy .eyebrow {
overflow: hidden;
font-size: 16px;
letter-spacing: 1px;
text-overflow: ellipsis;
white-space: nowrap;
}
.chapter-scroll {
min-height: 0;
flex: 1;
}
.list-scroll-guide {
display: flex;
min-height: 40px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
gap: 10px;
color: #f4e5bd;
background: rgba(58, 40, 28, 0.94);
border: 1px solid rgba(222, 194, 139, 0.55);
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 17px;
font-weight: 750;
}
.scroll-guide-mark {
color: #f0c86b;
font-size: 24px;
line-height: 1;
}
.chapter-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
padding: 0 2px 22px;
}
.chapter-card {
position: relative;
display: grid;
width: 100%;
min-height: 112px;
overflow: hidden;
grid-template-columns: 68px minmax(0, 1fr) 24px;
align-items: center;
gap: 14px;
padding: 12px 16px 12px 12px;
text-align: left;
border-radius: 8px;
}
.chapter-card.completed {
border-color: #50745f;
}
.chapter-card.current {
border: 3px solid #a23a2d;
box-shadow: 0 10px 25px rgba(94, 32, 24, 0.24);
}
.chapter-card-hover {
transform: translateY(2px);
opacity: 0.88;
}
.chapter-number {
display: flex;
height: 88px;
align-items: center;
justify-content: center;
flex-direction: column;
color: #f4e4be;
background: #432b1e;
border-left: 7px solid var(--cinnabar);
font-size: 16px;
}
.chapter-number .number {
font-family: Georgia, serif;
font-size: 32px;
font-weight: 800;
line-height: 1.1;
}
.chapter-card-copy {
display: flex;
min-width: 0;
flex-direction: column;
}
.year {
color: #9a332b;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 17px;
font-weight: 700;
}
.chapter-name {
display: -webkit-box;
margin-top: 4px;
overflow: hidden;
font-size: 22px;
font-weight: 800;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.chapter-badges {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 15px;
}
.chapter-badge {
padding: 5px 9px;
border-radius: 999px;
}
.done-badge {
color: #eff7ef;
background: #37624f;
}
.current-badge {
color: #fff1cf;
background: #943128;
}
.read-badge {
color: #725c42;
background: #e5d3a9;
}
.chapter-progress {
color: #6e5942;
font-size: 15px;
font-weight: 700;
}
.open-arrow {
color: #8e392f;
font-size: 44px;
}
.scroll-ending {
display: block;
padding: 4px 0 18px;
color: #d9c69e;
font-size: 17px;
text-align: center;
}
.font-xlarge .catalog-title {
font-size: 28px;
}
.font-xlarge .catalog-heading-copy .eyebrow {
font-size: 18px;
}
.font-xlarge .chapter-card {
min-height: 126px;
}
.font-xlarge .chapter-number {
height: 100px;
font-size: 18px;
}
.font-xlarge .chapter-number .number {
font-size: 36px;
}
.font-xlarge .year {
font-size: 19px;
}
.font-xlarge .chapter-name {
font-size: 25px;
}
.font-xlarge .chapter-badges,
.font-xlarge .chapter-progress {
font-size: 17px;
}
.font-xlarge .scroll-ending {
font-size: 19px;
}
.font-xlarge .list-scroll-guide {
font-size: 19px;
}
@media (max-width: 730px), (max-height: 400px) {
.catalog-shell {
gap: 8px;
}
.catalog-head {
min-height: 58px;
grid-template-columns: 96px minmax(0, 1fr) 116px;
gap: 9px;
padding: 6px 108px 6px 8px;
}
.catalog-title {
font-size: 21px;
}
.catalog-heading-copy .eyebrow {
display: none;
}
.catalog-title-full {
display: none;
}
.catalog-title-short {
display: block;
}
.chapter-grid {
gap: 9px;
padding-bottom: 16px;
}
.chapter-card {
min-height: 102px;
grid-template-columns: 58px minmax(0, 1fr) 20px;
gap: 10px;
padding: 9px 10px 9px 8px;
}
.chapter-number {
height: 80px;
font-size: 14px;
}
.chapter-number .number {
font-size: 27px;
}
.year {
font-size: 15px;
}
.chapter-name {
font-size: 19px;
}
.chapter-badges,
.chapter-progress {
font-size: 13px;
}
.open-arrow {
font-size: 36px;
}
.list-scroll-guide {
min-height: 36px;
font-size: 16px;
}
.scroll-guide-mark {
font-size: 21px;
}
.font-xlarge .catalog-title {
font-size: 24px;
}
.font-xlarge .chapter-card {
min-height: 116px;
}
.font-xlarge .chapter-number {
height: 94px;
font-size: 16px;
}
.font-xlarge .chapter-number .number {
font-size: 31px;
}
.font-xlarge .year {
font-size: 17px;
}
.font-xlarge .chapter-name {
font-size: 22px;
}
.font-xlarge .chapter-badges,
.font-xlarge .chapter-progress {
font-size: 15px;
}
}
@@ -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,
}
@@ -0,0 +1,77 @@
const memoryCards = require('../../data/memoryCards')
const {
getProgress,
getSettings,
saveSettings,
} = require('../../utils/storage')
const {
getCollectedMemories,
} = require('../../utils/memoryCollection')
const { chapterRoute } = require('../../utils/chapterRoute')
function twoDigits(value) {
return String(value).padStart(2, '0')
}
Page({
data: {
memories: [],
memoryCount: 0,
fontScale: 'large',
},
onShow() {
const progress = getProgress()
const settings = getSettings()
const memories = getCollectedMemories(progress, memoryCards).map(
(memory, index) => ({
...memory,
chapterLabel: `第${twoDigits(memory.chapterNumber)}回`,
pageLabel: `第${index + 1}页`,
characterInitial: memory.characterName.slice(0, 1),
}),
)
this.setData({
memories,
memoryCount: memories.length,
fontScale: settings.fontScale === 'xlarge' ? 'xlarge' : 'large',
})
},
goBack() {
wx.reLaunch({ url: '/pages/home/home' })
},
openCatalog() {
wx.navigateTo({ url: '/pages/catalog/catalog' })
},
openChapter(event) {
const chapter = Number(event.currentTarget.dataset.chapter) || 1
wx.navigateTo({
url: chapterRoute(chapter),
})
},
toggleFont() {
const fontScale = this.data.fontScale === 'large' ? 'xlarge' : 'large'
const settings = getSettings()
settings.fontScale = fontScale
saveSettings(settings)
this.setData({ fontScale })
},
onShareAppMessage() {
return {
title: '我的桂香岁月:饭桌边,也有值得带回家说的话',
path: '/pages/home/home',
}
},
onShareTimeline() {
return {
title: '我的桂香岁月:饭桌边,也有值得带回家说的话',
query: '',
}
},
})
@@ -0,0 +1,4 @@
{
"navigationStyle": "custom",
"disableScroll": true
}
@@ -0,0 +1,91 @@
<view class="safe-shell memories-shell font-{{fontScale}}">
<view class="memories-book paper-panel">
<view class="memories-head">
<button class="memory-back" bindtap="goBack" aria-label="回到封面">
<text class="memory-back-long">‹ 封面</text>
<text class="memory-back-short">封面</text>
</button>
<view class="memories-heading">
<text class="memories-title">我的桂香岁月</text>
<text class="memories-subtitle">
{{memoryCount > 0 ? '收下的故事,慢慢翻、慢慢聊' : '一本等你慢慢装满的故事册'}}
</text>
</view>
<view class="memories-head-actions">
<button
class="memory-font"
bindtap="toggleFont"
aria-label="{{fontScale === 'large' ? '把文字再放大一些' : '恢复较大文字'}}"
>
<text class="memory-font-long">{{fontScale === 'large' ? '再大一点' : '标准大字'}}</text>
<text class="memory-font-short">大字</text>
</button>
<view class="capsule-safe-space" aria-hidden="true"></view>
</view>
</view>
<view wx:if="{{memoryCount === 0}}" class="empty-memory">
<view class="empty-bookmark">桂香</view>
<view class="empty-copy">
<text class="empty-title">这本册子,还等着第一段回忆</text>
<text class="empty-text">每读完一回,把最后那一页“收进画册”,这里就会多一段人物、一件旧物和一句带回家聊的话。</text>
<button class="empty-action" bindtap="openCatalog">去翻一回故事</button>
</view>
</view>
<scroll-view
wx:else
class="memory-scroll"
scroll-x
enhanced
enable-flex
show-scrollbar="{{true}}"
aria-label="已收藏的桂香故事页"
>
<view class="memory-pages">
<view
wx:for="{{memories}}"
wx:key="cardId"
class="memory-sheet"
aria-label="{{item.pageLabel}},{{item.chapterLabel}},{{item.chapterTitle}}"
>
<view class="memory-sheet-spine" aria-hidden="true"></view>
<view class="memory-left">
<text class="memory-page-number">{{item.pageLabel}}</text>
<view class="memory-person-seal">{{item.characterInitial}}</view>
<text class="memory-person">{{item.characterName}}</text>
<text class="memory-era">{{item.eraLine}}</text>
<view class="memory-object">
<text class="memory-label">这一回留下的物件</text>
<text>{{item.eraObject || '桂香饭桌边的一件旧物'}}</text>
</view>
</view>
<scroll-view class="memory-right" scroll-y enhanced show-scrollbar="{{true}}">
<text class="memory-chapter">{{item.chapterLabel}} · {{item.chapterTitle}}</text>
<text class="memory-echo">“{{item.tableEcho}}”</text>
<view class="memory-note">
<text class="memory-label">今天可以这样做</text>
<text>{{item.lifeAction}}</text>
</view>
<view class="memory-note family-note">
<text class="memory-label">带回家聊一聊</text>
<text>“{{item.familyLine}}”</text>
</view>
<button
class="memory-reread"
data-chapter="{{item.chapterNumber}}"
bindtap="openChapter"
aria-label="再读第{{item.chapterNumber}}回,{{item.chapterTitle}}"
>再读这一回</button>
</scroll-view>
</view>
<view class="memory-end-card">
<text class="memory-end-title">往后的页,还在饭桌边等你</text>
<text>每收下一页,不是为了攒数字,只为记住一个被看见的人。</text>
<button class="memory-end-action" bindtap="openCatalog">继续翻故事</button>
</view>
</view>
</scroll-view>
</view>
</view>
@@ -0,0 +1,568 @@
/* Personal storybook: horizontal pages with elder-friendly reading controls. */
.memories-shell {
height: 100vh;
min-height: 0;
overflow: hidden;
}
.memories-book {
display: flex;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
border-radius: 9px;
flex-direction: column;
}
.memories-head {
position: relative;
display: flex;
min-height: 66px;
flex: 0 0 auto;
align-items: center;
justify-content: space-between;
padding: 8px 10px 8px 12px;
border-bottom: 3px solid #9b332a;
}
.memory-back,
.memory-font,
.empty-action,
.memory-reread,
.memory-end-action {
display: flex;
min-height: 48px;
align-items: center;
justify-content: center;
padding: 6px 13px;
border: 2px solid #997a54;
border-radius: 7px;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 18px;
font-weight: 850;
}
.memory-back,
.memory-font {
color: #52392a;
background: #ead8af;
}
.memories-heading {
position: absolute;
z-index: 1;
top: 8px;
right: 246px;
left: 134px;
display: flex;
min-width: 0;
align-items: center;
pointer-events: none;
flex-direction: column;
}
.memories-title {
overflow: hidden;
font-size: 29px;
font-weight: 900;
letter-spacing: 3px;
line-height: 1.16;
text-overflow: ellipsis;
white-space: nowrap;
}
.memories-subtitle {
overflow: hidden;
color: #725b43;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 16px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.capsule-safe-space {
width: 102px;
height: 48px;
flex: 0 0 102px;
}
.memories-head-actions {
position: absolute;
z-index: 2;
top: 8px;
right: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.memory-font {
min-width: 112px;
white-space: nowrap;
}
.memory-back-short,
.memory-font-short {
display: none;
}
.memory-back {
position: absolute;
z-index: 2;
top: 8px;
left: 12px;
width: 112px;
min-width: 112px;
}
.empty-memory {
display: grid;
width: calc(100% - 48px);
max-width: 680px;
min-height: 230px;
align-self: center;
grid-template-columns: 132px minmax(0, 1fr);
align-items: stretch;
margin: auto;
overflow: hidden;
background: #f8ecc9;
border: 3px double #97784f;
border-radius: 6px;
box-shadow: 0 12px 28px rgba(67, 40, 23, 0.18);
}
.empty-bookmark {
display: flex;
align-items: center;
justify-content: center;
color: #f8e7bb;
background: #913128;
font-size: 30px;
font-weight: 900;
letter-spacing: 7px;
writing-mode: vertical-rl;
}
.empty-copy {
display: flex;
justify-content: center;
padding: 22px 28px;
flex-direction: column;
}
.empty-title {
font-size: 27px;
font-weight: 900;
}
.empty-text {
margin-top: 9px;
color: #604b36;
font-size: 18px;
font-weight: 650;
line-height: 1.55;
}
.empty-action {
width: 190px;
margin-top: 14px;
color: #fff0cc;
background: #943128;
border-color: #74231d;
}
.memory-scroll {
width: 100%;
min-height: 0;
flex: 1;
padding: 12px 0 10px;
}
.memory-pages {
display: inline-flex;
min-width: 100%;
height: 100%;
align-items: stretch;
gap: 16px;
padding: 0 18px;
}
.memory-sheet {
position: relative;
display: grid;
width: 710px;
height: 100%;
min-height: 0;
flex: none;
overflow: hidden;
grid-template-columns: 225px minmax(0, 1fr);
background:
repeating-linear-gradient(0deg, rgba(84, 55, 31, 0.025) 0, rgba(84, 55, 31, 0.025) 1px, transparent 1px, transparent 6px),
#f8edcc;
border: 3px double #91724b;
border-radius: 6px;
box-shadow: 0 10px 24px rgba(58, 35, 21, 0.2);
scroll-snap-align: center;
}
.memory-sheet-spine {
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 219px;
width: 7px;
pointer-events: none;
background: linear-gradient(90deg, rgba(81, 50, 27, 0.1), rgba(255, 255, 255, 0.58), rgba(81, 50, 27, 0.12));
}
.memory-left {
position: relative;
display: flex;
min-width: 0;
align-items: center;
padding: 17px 20px;
background: #263a3e;
flex-direction: column;
color: #f5e5bd;
}
.memory-page-number {
align-self: flex-start;
color: #d6ba7e;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 16px;
font-weight: 750;
}
.memory-person-seal {
display: flex;
width: 74px;
height: 74px;
align-items: center;
justify-content: center;
margin-top: 4px;
color: #8d2d25;
background: #efe0b8;
border: 5px double #ad4d3a;
border-radius: 50%;
font-size: 37px;
font-weight: 900;
}
.memory-person {
margin-top: 5px;
font-size: 25px;
font-weight: 900;
}
.memory-era {
margin-top: 4px;
color: #e3cf9f;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 16px;
font-weight: 650;
line-height: 1.4;
text-align: center;
}
.memory-object {
display: flex;
width: 100%;
margin-top: auto;
padding: 8px 10px;
color: #f5e5bd;
background: rgba(0, 0, 0, 0.18);
border-left: 4px solid #caa65e;
flex-direction: column;
font-size: 17px;
font-weight: 750;
line-height: 1.4;
}
.memory-right {
min-width: 0;
height: 100%;
padding: 14px 22px 16px 25px;
}
.memory-chapter {
display: block;
color: #8d2f27;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 17px;
font-weight: 850;
}
.memory-echo {
display: block;
margin-top: 7px;
padding-bottom: 7px;
font-size: 23px;
font-weight: 900;
line-height: 1.45;
border-bottom: 1px solid #c9af7f;
}
.memory-note {
display: flex;
margin-top: 8px;
color: #4f3c2b;
font-size: 17px;
font-weight: 700;
line-height: 1.42;
flex-direction: column;
}
.memory-label {
display: block;
margin-bottom: 2px;
color: #a03b31;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
font-size: 15px;
font-weight: 850;
}
.family-note {
padding-left: 10px;
border-left: 4px solid #35634f;
}
.memory-reread {
width: 156px;
margin-top: 10px;
color: #fff0cc;
background: #8d3028;
border-color: #70231e;
}
.memory-end-card {
display: flex;
width: 310px;
height: 100%;
min-height: 0;
flex: none;
justify-content: center;
padding: 24px;
color: #f2e2b8;
background: #3a2a20;
border: 3px double #ad8d5e;
border-radius: 6px;
flex-direction: column;
font-size: 18px;
font-weight: 650;
line-height: 1.55;
}
.memory-end-title {
margin-bottom: 10px;
font-size: 26px;
font-weight: 900;
}
.memory-end-action {
width: 170px;
margin-top: 18px;
color: #34251c;
background: #e9d6a8;
}
.font-xlarge .memory-echo {
font-size: 26px;
}
.font-xlarge .memory-note,
.font-xlarge .memory-object,
.font-xlarge .memory-end-card {
font-size: 19px;
}
@media (max-width: 730px), (max-height: 400px) {
.memories-head {
min-height: 58px;
padding: 5px 4px;
}
.memories-heading {
top: 17px;
right: 158px;
left: 82px;
}
.memory-back,
.memory-font,
.empty-action,
.memory-reread,
.memory-end-action {
min-height: 48px;
padding-right: 8px;
padding-left: 8px;
font-size: 16px;
}
.memories-title {
font-size: 18px;
letter-spacing: 0;
}
.memories-subtitle {
display: none;
}
.capsule-safe-space {
width: 76px;
flex-basis: 76px;
}
.memory-font {
width: 72px;
min-width: 72px;
padding-right: 4px;
padding-left: 4px;
}
.memory-back {
top: 5px;
left: 4px;
width: 74px;
min-width: 0;
padding-right: 4px;
padding-left: 4px;
}
.memories-head-actions {
top: 5px;
right: 4px;
gap: 4px;
}
.memory-back-long,
.memory-font-long {
display: none;
}
.memory-back-short,
.memory-font-short {
display: inline;
}
.memory-scroll {
padding-top: 8px;
padding-bottom: 8px;
}
.memory-pages {
gap: 12px;
padding: 0 10px;
}
.memory-sheet {
width: 565px;
grid-template-columns: 170px minmax(0, 1fr);
}
.memory-sheet-spine {
left: 164px;
}
.memory-left {
padding: 10px 12px;
}
.memory-page-number {
font-size: 14px;
}
.memory-person-seal {
width: 58px;
height: 58px;
margin-top: 1px;
font-size: 29px;
}
.memory-person {
margin-top: 2px;
font-size: 21px;
}
.memory-era {
font-size: 14px;
line-height: 1.3;
}
.memory-object {
padding: 6px 8px;
font-size: 15px;
}
.memory-right {
padding: 10px 14px 12px 18px;
}
.memory-chapter {
font-size: 15px;
}
.memory-echo {
margin-top: 4px;
padding-bottom: 5px;
font-size: 20px;
line-height: 1.32;
}
.memory-note {
margin-top: 5px;
font-size: 15.5px;
line-height: 1.32;
}
.memory-label {
font-size: 14px;
}
.memory-reread {
width: 138px;
margin-top: 7px;
}
.font-xlarge .memory-echo {
font-size: 22px;
}
.font-xlarge .memory-note,
.font-xlarge .memory-object,
.font-xlarge .memory-end-card {
font-size: 17px;
}
.empty-memory {
width: calc(100% - 24px);
max-width: 590px;
min-height: 220px;
grid-template-columns: 100px minmax(0, 1fr);
}
.empty-bookmark {
font-size: 25px;
}
.empty-copy {
padding: 15px 20px;
}
.empty-title {
font-size: 23px;
}
.empty-text {
margin-top: 5px;
font-size: 16px;
line-height: 1.42;
}
.empty-action {
margin-top: 8px;
}
}
@@ -0,0 +1,85 @@
const memoryCards = require('../../data/memoryCards')
const { chapterRoute } = require('../../utils/chapterRoute')
const MEMORY_CARD_ID_PATTERN = /^S\d{2}-C\d{2}-MC\d{2}$/
const INVALID_CARD_MESSAGE = '这回生活观察暂时没有找到。回到封面,还可以慢慢翻一回。'
function normalizeCardId(value) {
const raw = String(value || '').trim()
if (!raw || raw.length > 64) return ''
let decoded = raw
try {
decoded = decodeURIComponent(raw)
} catch (error) {
return ''
}
return MEMORY_CARD_ID_PATTERN.test(decoded) ? decoded : ''
}
function findReportCard(value) {
const cardId = normalizeCardId(value)
if (!cardId) return null
return memoryCards.find((card) => card.cardId === cardId) || null
}
function twoDigits(value) {
return String(value).padStart(2, '0')
}
Page({
data: {
reportCard: null,
chapterLabel: '',
invalidCard: false,
invalidCardMessage: INVALID_CARD_MESSAGE,
},
onLoad(options = {}) {
const reportCard = findReportCard(options.cardId)
if (!reportCard) {
this.setData({
reportCard: null,
chapterLabel: '',
invalidCard: true,
})
return
}
this.setData({
reportCard: { ...reportCard },
chapterLabel: `第${twoDigits(reportCard.chapterNumber)}回`,
invalidCard: false,
})
},
backToChapter() {
const reportCard = this.data.reportCard
if (!reportCard || !reportCard.chapterNumber) {
this.goHome()
return
}
const pages = typeof getCurrentPages === 'function'
? getCurrentPages()
: []
const previous = pages[pages.length - 2]
if (
previous
&& String(previous.route || '').endsWith('/pages/chapter/chapter')
&& typeof wx.navigateBack === 'function'
) {
wx.navigateBack({ delta: 1 })
return
}
wx.navigateTo({
url: chapterRoute(reportCard.chapterNumber),
})
},
goHome() {
const target = { url: '/pages/home/home' }
if (typeof wx.reLaunch === 'function') {
wx.reLaunch(target)
return
}
wx.redirectTo(target)
},
})
@@ -0,0 +1,4 @@
{
"navigationStyle": "custom",
"disableScroll": true
}
@@ -0,0 +1,81 @@
<view class="safe-shell report-shell">
<view wx:if="{{reportCard}}" class="report-book paper-panel">
<view class="report-identity">
<text class="report-kicker">桂香生活观察</text>
<view class="report-seal" aria-hidden="true">察</view>
<text class="report-chapter">{{chapterLabel}}</text>
<text class="report-title">{{reportCard.chapterTitle}}</text>
<view class="report-person-row">
<text class="report-person-label">这回人物</text>
<text class="report-person">{{reportCard.characterName}}</text>
</view>
<view class="report-local-note">
<text>这页只留在您的手机里。</text>
<text>不会发送答题记录,也不用于诊断。</text>
</view>
</view>
<view class="report-main">
<view class="report-capsule-safe-space" aria-hidden="true"></view>
<scroll-view
class="report-scroll"
scroll-y
enhanced
bounces="{{false}}"
show-scrollbar="{{true}}"
aria-label="{{chapterLabel}}桂香生活观察"
>
<text class="report-heading">这一回,咱们看见了什么</text>
<text class="report-disclaimer">本回生活观察,不是个人健康评估,也不替代诊断和个体化治疗</text>
<view class="report-observation-list">
<view class="report-observation-card">
<text class="report-observation-index" aria-hidden="true">1</text>
<view class="report-observation-copy">
<text class="report-observation-label">本回看见</text>
<text class="report-observation-value">{{reportCard.observation}}</text>
</view>
</view>
<view class="report-observation-card is-family">
<text class="report-observation-index" aria-hidden="true">2</text>
<view class="report-observation-copy">
<text class="report-observation-label">家里可以</text>
<text class="report-observation-value">{{reportCard.familySupport}}</text>
</view>
</view>
<view class="report-observation-card is-today">
<text class="report-observation-index" aria-hidden="true">3</text>
<view class="report-observation-copy">
<text class="report-observation-label">今天试试</text>
<text class="report-observation-value">{{reportCard.todayAction}}</text>
</view>
</view>
</view>
</scroll-view>
<view class="report-actions">
<button
class="report-action report-action-primary"
bindtap="backToChapter"
aria-label="回到{{chapterLabel}}记忆卡"
>回到这一回</button>
<button
class="report-action report-action-secondary"
bindtap="goHome"
aria-label="回到唐侦探封面"
>回到封面</button>
</view>
</view>
</view>
<view wx:else class="report-invalid paper-panel" aria-live="polite">
<view class="report-invalid-seal" aria-hidden="true">桂</view>
<text class="report-invalid-title">这回观察,暂时没有翻开</text>
<text class="report-invalid-copy">{{invalidCardMessage}}</text>
<button
class="report-action report-action-primary report-invalid-action"
bindtap="goHome"
aria-label="回到唐侦探封面"
>回到封面</button>
</view>
</view>
@@ -0,0 +1,28 @@
.report-shell{height:100vh;min-height:0;overflow:hidden}
.report-book{display:grid;width:100%;height:100%;overflow:hidden;grid-template-columns:minmax(210px,2fr) minmax(0,5fr);border:2px solid #9e845e;border-radius:9px}
.report-identity{display:flex;min-height:0;align-items:center;padding:15px 18px;overflow:hidden;color:#f5e5bd;background:#263a3e;border-right:5px solid #9b332a;flex-direction:column}
.report-kicker{align-self:flex-start;color:#dfc486;font-size:17px;font-weight:850}
.report-seal,.report-invalid-seal{display:flex;align-items:center;justify-content:center;color:#8d2d25;background:#efe0b8;border:5px double #ad4d3a;border-radius:50%;font-weight:900}
.report-seal{width:66px;height:66px;flex:none;margin:7px 0 4px;font-size:32px}
.report-title{margin-top:3px;font-size:21px;font-weight:900;line-height:1.2;text-align:center}
.report-person-label{color:#d7bd7d;font-size:14px;font-weight:850}.report-person{margin-left:8px;font-size:18px;font-weight:900}
.report-local-note{display:flex;width:100%;margin-top:auto;padding:6px 8px;color:#eadbb5;background:rgba(19,30,32,.38);border:1px solid rgba(223,196,134,.42);border-radius:6px;flex-direction:column;font-size:14px;font-weight:700;line-height:1.3}
.report-main{position:relative;display:grid;min-height:0;padding:46px 17px 12px;gap:8px;grid-template-rows:minmax(0,1fr) auto}
.report-capsule-safe-space{position:absolute;top:6px;right:8px;width:104px;height:36px;pointer-events:none}
.report-scroll{width:100%;height:100%;min-height:0}
.report-heading{display:block;padding-right:108px;color:#493325;font-size:24px;font-weight:900;line-height:1.22}
.report-disclaimer{display:block;margin-top:6px;padding:6px 9px;color:#644a37;background:#eadfbe;border-left:5px solid #8f3028;font-size:15px;font-weight:750;line-height:1.3}
.report-observation-list{display:grid;gap:6px;margin-top:7px}
.report-observation-card{display:grid;align-items:center;gap:9px;padding:6px 9px;background:rgba(255,250,232,.72);border:1px solid rgba(126,86,50,.32);border-radius:7px;grid-template-columns:34px minmax(0,1fr)}
.report-observation-card.is-family{border-left:5px solid #8c6f42}.report-observation-card.is-today{border-left:5px solid #47705c}
.report-observation-index{padding:5px;color:#fff0ce;background:#8f3028;border-radius:50%;font-size:17px;font-weight:900;text-align:center}
.report-observation-copy{display:grid;min-width:0;gap:2px}
.report-observation-label{color:#8d2f27;font-size:15px;font-weight:900}
.report-observation-value{color:#493325;font-size:19px;font-weight:750;line-height:1.32}
.report-actions{display:grid;gap:9px;grid-template-columns:repeat(2,minmax(0,1fr))}
.report-action{display:flex;min-height:52px;align-items:center;justify-content:center;padding:6px 10px;border:3px solid;border-radius:8px;font-size:19px;font-weight:900;line-height:1.2}
.report-action-primary{color:#fff0cb;background:#963128;border-color:#74231d}.report-action-secondary{color:#473326;background:#ead9b2;border-color:#997b54}
.report-invalid{display:flex;width:min(620px,100%);align-items:center;margin:auto;padding:20px;flex-direction:column}.report-invalid-seal{padding:12px;font-size:28px}.report-invalid-title{font-size:22px;font-weight:900}
@media(max-width:700px),(max-height:420px){
.report-book{grid-template-columns:minmax(180px,2fr) minmax(0,5fr)}.report-identity{padding:7px 10px}.report-kicker{font-size:14px}.report-seal{width:42px;height:42px;margin:2px 0;border-width:3px;font-size:22px}.report-title{font-size:17px}.report-local-note{padding:3px 5px;font-size:12px;line-height:1.15}
.report-main{padding:41px 9px 6px;gap:4px}.report-heading{padding-right:100px;font-size:20px}.report-disclaimer{margin-top:3px;padding:3px 6px;font-size:13px;line-height:1.18}.report-observation-list{gap:3px;margin-top:3px}.report-observation-card{gap:6px;padding:3px 6px;grid-template-columns:28px minmax(0,1fr)}.report-observation-index{font-size:14px}.report-observation-label{font-size:13px}.report-observation-value{font-size:16px;line-height:1.18}.report-actions{gap:5px}.report-action{min-height:48px;padding:4px 6px;font-size:16px}}
@@ -0,0 +1,112 @@
const memoryCards = require('../../data/memoryCards')
const { chapterRoute } = require('../../utils/chapterRoute')
const MEMORY_CARD_ID_PATTERN = /^S\d{2}-C\d{2}-MC\d{2}$/
const INVALID_CARD_MESSAGE = '这张桂香记忆卡暂时没有找到。回到封面,还可以慢慢翻一回。'
const SHARE_PREVIEW_IMAGE = '/assets/share/guixiang-story-share-preview-v1.jpg'
function memoryCardSharePath(memoryCard) {
const cardId = String(memoryCard && memoryCard.cardId || '').trim()
if (!cardId) return '/pages/home/home'
return `/pages/share/share?cardId=${encodeURIComponent(cardId)}&source=family-memory`
}
function normalizeCardId(value) {
const raw = String(value || '').trim()
if (!raw || raw.length > 64) return ''
let decoded = raw
try {
decoded = decodeURIComponent(raw)
} catch (error) {
return ''
}
return MEMORY_CARD_ID_PATTERN.test(decoded) ? decoded : ''
}
function findMemoryCard(value) {
const cardId = normalizeCardId(value)
if (!cardId) return null
return memoryCards.find((card) => card.cardId === cardId) || null
}
function twoDigits(value) {
return String(value).padStart(2, '0')
}
Page({
data: {
memoryCard: null,
chapterLabel: '',
characterInitial: '',
invalidCard: false,
invalidCardMessage: INVALID_CARD_MESSAGE,
},
onLoad(options = {}) {
const memoryCard = findMemoryCard(options.cardId)
if (!memoryCard) {
this.setData({
memoryCard: null,
chapterLabel: '',
characterInitial: '',
invalidCard: true,
})
return
}
this.setData({
memoryCard: { ...memoryCard },
chapterLabel: `第${twoDigits(memoryCard.chapterNumber)}回`,
characterInitial: String(memoryCard.characterName || '桂').slice(0, 1),
invalidCard: false,
})
},
openChapter() {
const memoryCard = this.data.memoryCard
if (!memoryCard || !memoryCard.chapterNumber) {
this.goHome()
return
}
wx.navigateTo({
url: chapterRoute(memoryCard.chapterNumber),
})
},
goHome() {
const target = { url: '/pages/home/home' }
if (typeof wx.reLaunch === 'function') {
wx.reLaunch(target)
return
}
wx.redirectTo(target)
},
onShareAppMessage() {
const memoryCard = this.data.memoryCard
if (!memoryCard) {
return {
title: '唐侦探:饭桌边,也有值得带回家说的话',
path: '/pages/home/home',
imageUrl: SHARE_PREVIEW_IMAGE,
}
}
return {
title: `带回家的一句话:${memoryCard.familyLine}`,
path: memoryCardSharePath(memoryCard),
imageUrl: SHARE_PREVIEW_IMAGE,
}
},
onShareTimeline() {
const memoryCard = this.data.memoryCard
return memoryCard
? {
title: `桂香记忆:${memoryCard.tableEcho}`,
query: `cardId=${encodeURIComponent(memoryCard.cardId)}&source=family-memory`,
}
: {
title: '唐侦探:饭桌边,也有值得带回家说的话',
query: '',
}
},
})
@@ -0,0 +1,4 @@
{
"navigationStyle": "custom",
"disableScroll": true
}
@@ -0,0 +1,71 @@
<view class="safe-shell share-shell">
<view wx:if="{{memoryCard}}" class="share-card paper-panel">
<view class="share-identity">
<text class="share-kicker">家人发来的桂香记忆</text>
<view class="share-person-seal" aria-hidden="true">{{characterInitial}}</view>
<view class="share-meta-block">
<text class="share-meta-label">人物</text>
<text class="share-person">{{memoryCard.characterName}}</text>
</view>
<view class="share-meta-block">
<text class="share-meta-label">年代</text>
<text class="share-era">{{memoryCard.eraLine}}</text>
</view>
<text class="share-chapter">{{chapterLabel}} · {{memoryCard.chapterTitle}}</text>
</view>
<view class="share-main">
<view class="share-capsule-safe-space" aria-hidden="true"></view>
<scroll-view
class="share-copy"
scroll-y
enhanced
bounces="{{false}}"
show-scrollbar="{{true}}"
aria-label="{{chapterLabel}}桂香记忆卡"
>
<view class="share-section share-echo">
<text class="share-section-label">桌边回声</text>
<text class="share-quote">“{{memoryCard.tableEcho}}”</text>
</view>
<view class="share-section">
<text class="share-section-label">今天行动</text>
<text class="share-section-copy">{{memoryCard.lifeAction}}</text>
</view>
<view class="share-section share-family">
<text class="share-section-label">带回家话语</text>
<text class="share-section-copy">“{{memoryCard.familyLine}}”</text>
</view>
</scroll-view>
<view class="share-actions">
<button
class="share-action share-action-primary"
bindtap="openChapter"
aria-label="一起翻{{chapterLabel}},{{memoryCard.chapterTitle}}"
>一起翻这一回</button>
<button
class="share-action share-action-forward"
open-type="share"
aria-label="把这一页再发给家人"
>再发给家人</button>
<button
class="share-action share-action-secondary"
bindtap="goHome"
aria-label="回到唐侦探封面"
>回到封面</button>
</view>
</view>
</view>
<view wx:else class="share-invalid paper-panel" aria-live="polite">
<view class="share-invalid-seal" aria-hidden="true">桂</view>
<text class="share-invalid-title">这页记忆,暂时没有翻开</text>
<text class="share-invalid-copy">{{invalidCardMessage}}</text>
<button
class="share-action share-action-primary share-invalid-action"
bindtap="goHome"
aria-label="回到唐侦探封面"
>回到封面</button>
</view>
</view>
@@ -0,0 +1,326 @@
.share-shell {
display: flex;
height: 100vh;
min-height: 0;
overflow: hidden;
}
.share-card {
position: relative;
display: grid;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
grid-template-columns: minmax(205px, 2fr) minmax(0, 5fr);
border: 2px solid #9e845e;
border-radius: 9px;
}
.share-identity {
display: flex;
min-width: 0;
min-height: 0;
align-items: center;
padding: 18px 22px;
overflow: hidden;
color: #f5e5bd;
background:
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.025) 0, rgba(255, 255, 255, 0.025) 1px, transparent 1px, transparent 6px),
#263a3e;
border-right: 5px solid #9b332a;
flex-direction: column;
}
.share-kicker,
.share-meta-label,
.share-section-label,
.share-actions,
.share-invalid-copy {
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
}
.share-kicker {
align-self: flex-start;
color: #dfc486;
font-size: 16px;
font-weight: 850;
letter-spacing: 2px;
}
.share-person-seal,
.share-invalid-seal {
display: flex;
align-items: center;
justify-content: center;
color: #8d2d25;
background: #efe0b8;
border: 5px double #ad4d3a;
border-radius: 50%;
font-weight: 900;
}
.share-person-seal {
width: 78px;
height: 78px;
flex: none;
margin: 9px 0 6px;
font-size: 38px;
}
.share-meta-block {
display: grid;
width: 100%;
min-width: 0;
align-items: start;
gap: 7px;
margin-top: 5px;
grid-template-columns: 42px minmax(0, 1fr);
}
.share-meta-label {
color: #d5b974;
font-size: 15px;
font-weight: 850;
}
.share-person {
font-size: 22px;
font-weight: 900;
line-height: 1.25;
}
.share-era {
color: #f0deae;
font-size: 17px;
font-weight: 750;
line-height: 1.35;
}
.share-chapter {
display: -webkit-box;
width: 100%;
margin-top: auto;
overflow: hidden;
color: #f7e9c7;
font-size: 17px;
font-weight: 850;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.share-main {
position: relative;
display: grid;
min-width: 0;
min-height: 0;
padding: 48px 20px 16px;
grid-template-rows: minmax(0, 1fr) auto;
gap: 12px;
}
.share-capsule-safe-space {
position: absolute;
top: 6px;
right: 8px;
width: 104px;
height: 36px;
pointer-events: none;
}
.share-copy {
width: 100%;
height: 100%;
min-height: 0;
}
.share-section {
display: grid;
align-items: start;
gap: 12px;
padding: 11px 14px;
grid-template-columns: 92px minmax(0, 1fr);
border-top: 1px solid rgba(126, 86, 50, 0.24);
}
.share-section:first-child {
padding-top: 0;
border-top: 0;
}
.share-section-label {
color: #8d2f27;
font-size: 17px;
font-weight: 900;
}
.share-quote,
.share-section-copy {
display: block;
font-size: 19px;
font-weight: 750;
line-height: 1.5;
}
.share-quote {
color: #493325;
font-size: 21px;
font-weight: 850;
}
.share-family {
background: rgba(167, 114, 54, 0.08);
border-bottom: 1px solid rgba(126, 86, 50, 0.22);
}
.share-actions {
display: grid;
width: 100%;
min-width: 0;
gap: 12px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.share-action {
display: flex;
width: 100%;
min-width: 0;
max-width: 100%;
min-height: 54px;
align-items: center;
justify-content: center;
padding: 8px 14px;
border: 3px solid;
border-radius: 8px;
font-size: 20px;
font-weight: 900;
line-height: 1.2;
justify-self: stretch;
}
.share-action-primary {
color: #fff0cb;
background: #963128;
border-color: #74231d;
box-shadow: 0 8px 18px rgba(111, 32, 27, 0.2);
}
.share-action-secondary {
color: #473326;
background: #ead9b2;
border-color: #997b54;
}
.share-action-forward {
color: #315545;
background: #dce7d9;
border-color: #64826e;
}
.share-invalid {
display: flex;
width: min(620px, 100%);
min-height: 250px;
align-self: center;
align-items: center;
justify-content: center;
margin: auto;
padding: 24px 34px;
border-radius: 9px;
flex-direction: column;
text-align: center;
}
.share-invalid-seal {
width: 68px;
height: 68px;
flex: none;
font-size: 31px;
}
.share-invalid-title {
margin-top: 9px;
color: #783028;
font-size: 26px;
font-weight: 900;
}
.share-invalid-copy {
margin-top: 7px;
color: #604b36;
font-size: 18px;
font-weight: 700;
line-height: 1.5;
}
.share-invalid-action {
width: 210px;
margin-top: 15px;
}
@media (max-width: 730px), (max-height: 400px) {
.share-card {
grid-template-columns: minmax(190px, 2fr) minmax(0, 5fr);
}
.share-identity {
padding: 10px 15px;
}
.share-kicker {
font-size: 14px;
}
.share-person-seal {
width: 58px;
height: 58px;
margin: 5px 0 2px;
font-size: 29px;
}
.share-person {
font-size: 19px;
}
.share-era,
.share-chapter {
font-size: 15px;
}
.share-main {
padding: 42px 12px 10px;
gap: 8px;
}
.share-section {
gap: 8px;
padding: 7px 8px;
grid-template-columns: 78px minmax(0, 1fr);
}
.share-section-label {
font-size: 15px;
}
.share-quote,
.share-section-copy {
font-size: 16px;
line-height: 1.38;
}
.share-actions {
gap: 8px;
}
.share-action {
min-height: 48px;
padding: 6px 10px;
font-size: 18px;
}
.share-invalid {
min-height: 220px;
padding: 17px 24px;
}
}