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%);
}
}