Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cc3edeef9 | ||
|
|
8b5bfe8385 | ||
|
|
a29328f35b | ||
|
|
87f5fa2478 | ||
|
|
f7d88622e0 | ||
|
|
ac2c99c3bc | ||
|
|
4efdd20ff7 | ||
|
|
297747041a |
@@ -35,26 +35,58 @@ jobs:
|
||||
echo "复用缓存 Node 工具链"
|
||||
else
|
||||
# 清理旧缓存:Alpine busybox mv 遇已存在目录会合并而非替换,
|
||||
# 残留的 glibc 版 node 会污染 musl 环境导致 symbol not found
|
||||
# 残留的旧版 node 会污染环境导致 symbol not found
|
||||
rm -rf "$TOOLDIR"
|
||||
mkdir -p "$TOOLDIR"
|
||||
# 探测 libc:Alpine(musl) 用 unofficial musl 构建,其余用官方 glibc 构建(tar.gz,busybox tar 不支持 xz)
|
||||
# 关键教训:unofficial-builds 在亚洲带宽极低,wget --timeout 只限单次读取,
|
||||
# 慢速断流仍返回成功,产生截断的 gzip(解压出损坏二进制报 symbol not found)。
|
||||
# 必须用 sha256 硬校验,不匹配则换源重试。
|
||||
if ldd --version 2>&1 | grep -qi musl || [ ! -e /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ]; then
|
||||
NODE_DIST="node-v22.12.0-linux-x64-musl"
|
||||
wget -q --tries=2 --timeout=120 -O "$TOOLDIR/node.tgz" \
|
||||
"https://unofficial-builds.nodejs.org/download/release/v22.12.0/$NODE_DIST.tar.gz"
|
||||
NODE_SHA="8d96de0293fc3978d09e2a3fee68c66bca4efcca90f52105e6f7463636e97308"
|
||||
# 优先 npmmirror(国内/亚洲快),unofficial-builds 作为后备
|
||||
URLS="https://registry.npmmirror.com/-/binary/node-unofficial-builds/v22.12.0/$NODE_DIST.tar.gz
|
||||
https://unofficial-builds.nodejs.org/download/release/v22.12.0/$NODE_DIST.tar.gz"
|
||||
else
|
||||
NODE_DIST="node-v22.12.0-linux-x64"
|
||||
wget -q --tries=2 --timeout=120 -O "$TOOLDIR/node.tgz" \
|
||||
"https://nodejs.org/dist/v22.12.0/$NODE_DIST.tar.gz" \
|
||||
|| wget -q --tries=2 --timeout=120 -O "$TOOLDIR/node.tgz" \
|
||||
"https://mirrors.aliyun.com/nodejs-release/v22.12.0/$NODE_DIST.tar.gz"
|
||||
NODE_SHA="e05a4d65232ae2b27b3d77da2e368522fb46b923335b8e0d5f77624c32484044"
|
||||
URLS="https://nodejs.org/dist/v22.12.0/$NODE_DIST.tar.gz
|
||||
https://mirrors.aliyun.com/nodejs-release/v22.12.0/$NODE_DIST.tar.gz"
|
||||
fi
|
||||
DL_OK=""
|
||||
for url in $URLS; do
|
||||
echo "尝试下载: $url"
|
||||
if wget -t 2 -T 120 -q -O "$TOOLDIR/node.tgz" "$url"; then
|
||||
# sha256 硬校验(busybox 有 sha256sum):拦截截断/错误页等假文件
|
||||
if [ "$(sha256sum "$TOOLDIR/node.tgz" | cut -d' ' -f1)" = "$NODE_SHA" ]; then
|
||||
echo "下载成功且校验通过"
|
||||
DL_OK=1
|
||||
break
|
||||
fi
|
||||
echo "sha256 校验失败,文件可能截断,尝试下一源"
|
||||
else
|
||||
echo "wget 下载失败,尝试下一源"
|
||||
fi
|
||||
rm -f "$TOOLDIR/node.tgz"
|
||||
done
|
||||
test -n "$DL_OK" || { echo "::error::所有下载源均失败或校验不通过"; exit 1; }
|
||||
tar -C "$TOOLDIR" -xzf "$TOOLDIR/node.tgz"
|
||||
mv "$TOOLDIR/$NODE_DIST" "$TOOLDIR/node"
|
||||
rm -f "$TOOLDIR/node.tgz"
|
||||
# musl 版 node 动态链接 libstdc++/libgcc_s,Alpine 精简容器缺失时补装
|
||||
if [ "$NODE_DIST" = "node-v22.12.0-linux-x64-musl" ] && [ ! -e /usr/lib/libstdc++.so.6 ] && [ ! -e /lib/libstdc++.so.6 ]; then
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache libstdc++ libgcc || echo "::warning::libstdc++/libgcc 安装失败,node 可能无法启动"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# 验证二进制可执行,失败则清缓存并报错(避免坏缓存被后续 run 复用)
|
||||
if ! "$TOOLDIR/node/bin/node" --version; then
|
||||
echo "::error::Node 二进制无法执行,请检查下载源与 libc 匹配"
|
||||
rm -rf "$TOOLDIR"
|
||||
exit 1
|
||||
fi
|
||||
"$TOOLDIR/node/bin/node" --version
|
||||
echo "PATH=$TOOLDIR/node/bin:$PATH" >> "$GITEA_ENV"
|
||||
|
||||
- name: Verify Node runtime
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"pages/divination/divination",
|
||||
"pages/solar-terms/solar-terms",
|
||||
"pages/settings/settings",
|
||||
"pages/preferences/preferences",
|
||||
"pages/bookmarks/bookmarks",
|
||||
"pages/birth-profiles/birth-profiles",
|
||||
"pages/wish-tree/wish-tree",
|
||||
"pages/wish-detail/wish-detail",
|
||||
"pages/wiki/wiki"
|
||||
|
||||
@@ -23,7 +23,7 @@ const GLOSSARY = [
|
||||
{ term: '空亡', desc: '六十甲子分为六旬,每旬十个干支配十二地支,余下两个地支无天干相配,称为该旬"空亡"。这是干支纪法本身的结构性概念,此处仅作历法知识展示。' },
|
||||
{ term: '大运', desc: '传统上以月柱为基础、按十年一换的节奏推演出的干支序列,用来描述人生不同阶段的节律。属于传统民俗推演方法,供文化了解。' },
|
||||
{ term: '流年', desc: '即每一年的干支(如 2026 年为丙午年)。传统做法将流年与本命干支对照,观察逐年的变化关系。' },
|
||||
{ term: '子时流派', desc: '子时(23:00-01:00)横跨两天,晚子时(23:00-24:00)出生者的日柱按当天还是次日起算,历代流派说法不一。可在"我的-偏好设置"中选择。' }
|
||||
{ term: '子时流派', desc: '子时(23:00-01:00)横跨两天,晚子时(23:00-24:00)出生者的日柱按当天还是次日起算,历代流派说法不一。可在"我的-设置"中选择。' }
|
||||
];
|
||||
|
||||
/** 根据出生信息构建排盘展示数据 */
|
||||
@@ -144,10 +144,10 @@ Page({
|
||||
},
|
||||
|
||||
goSettings() {
|
||||
wx.switchTab({ url: '/pages/settings/settings' });
|
||||
wx.navigateTo({ url: '/pages/birth-profiles/birth-profiles' });
|
||||
},
|
||||
|
||||
goFortune() {
|
||||
wx.switchTab({ url: '/pages/fortune/fortune' });
|
||||
wx.navigateTo({ url: '/pages/fortune/fortune' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
const birthProfile = require('../../utils/birth-profile.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loggedIn: false,
|
||||
profiles: [],
|
||||
activeProfileId: '',
|
||||
maxFree: birthProfile.MAX_PROFILES,
|
||||
canAdd: true,
|
||||
showAddForm: false,
|
||||
formName: '',
|
||||
formGender: 'male',
|
||||
formDate: '1990-01-01',
|
||||
formTime: '12:00'
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ loggedIn: birthProfile.isLoggedIn() });
|
||||
this.loadProfiles();
|
||||
},
|
||||
|
||||
loadProfiles() {
|
||||
const render = () => {
|
||||
const profiles = birthProfile.getProfiles();
|
||||
const active = birthProfile.getActiveProfile();
|
||||
this.setData({
|
||||
profiles,
|
||||
activeProfileId: active ? active.id : '',
|
||||
canAdd: profiles.length < this.data.maxFree
|
||||
});
|
||||
};
|
||||
// 已登录时先从服务端同步,失败静默回退本地
|
||||
if (birthProfile.isLoggedIn()) {
|
||||
birthProfile.syncFromServer().then(render).catch(render);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
},
|
||||
|
||||
toggleAddForm() {
|
||||
if (!this.data.showAddForm && !this.data.canAdd) {
|
||||
wx.showToast({ title: `最多可免费绑定 ${this.data.maxFree} 个生辰`, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ showAddForm: !this.data.showAddForm });
|
||||
},
|
||||
|
||||
onFormName(e) {
|
||||
this.setData({ formName: e.detail.value });
|
||||
},
|
||||
|
||||
setFormGender(e) {
|
||||
this.setData({ formGender: e.currentTarget.dataset.g });
|
||||
},
|
||||
|
||||
onFormDate(e) {
|
||||
this.setData({ formDate: e.detail.value });
|
||||
},
|
||||
|
||||
onFormTime(e) {
|
||||
this.setData({ formTime: e.detail.value });
|
||||
},
|
||||
|
||||
submitProfile() {
|
||||
const { formName, formGender, formDate, formTime } = this.data;
|
||||
if (!formDate || !formTime) {
|
||||
wx.showToast({ title: '请选择出生日期和时间', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const result = birthProfile.addProfile({
|
||||
name: formName.trim() || '未命名',
|
||||
gender: formGender,
|
||||
birthday: formDate,
|
||||
birthTime: formTime
|
||||
});
|
||||
if (!result.ok) {
|
||||
wx.showToast({ title: result.msg, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
// 已登录时推送到服务端(静默失败,不影响本地)
|
||||
birthProfile.pushProfileToServer(result.profile);
|
||||
this.setData({ showAddForm: false, formName: '' });
|
||||
wx.showToast({ title: '绑定成功', icon: 'success' });
|
||||
this.loadProfiles();
|
||||
},
|
||||
|
||||
removeProfile(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
const profile = this.data.profiles.find(p => p.id === id);
|
||||
if (!profile) return;
|
||||
wx.showModal({
|
||||
title: '删除生辰',
|
||||
content: `确定删除「${profile.name}」的生辰信息吗?`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
birthProfile.removeProfile(id);
|
||||
birthProfile.removeProfileFromServer(profile);
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
this.loadProfiles();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 未登录时引导回个人中心登录
|
||||
goLogin() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "生辰管理",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<view class="container">
|
||||
<!-- 未登录提示 -->
|
||||
<view class="card login-tip" wx:if="{{!loggedIn}}">
|
||||
<text class="text-sm">当前未登录,生辰仅保存在本机</text>
|
||||
<text class="text-xs text-muted block mt-1">登录后可云端同步,换设备不丢失</text>
|
||||
<view class="btn-outline text-center mt-2" bindtap="goLogin">去登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 生辰管理 -->
|
||||
<view class="card">
|
||||
<view class="section-title flex justify-between items-center">
|
||||
<text>生辰管理</text>
|
||||
<text class="text-xs text-muted">{{profiles.length}}/{{maxFree}}</text>
|
||||
</view>
|
||||
<view class="text-xs text-muted mb-2">八字排盘基于生辰信息,每位用户可免费绑定 {{maxFree}} 个出生年月(如本人、家人)。</view>
|
||||
|
||||
<view class="profile-list" wx:if="{{profiles.length > 0}}">
|
||||
<view class="profile-item" wx:for="{{profiles}}" wx:key="id">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<view class="flex items-center gap-1">
|
||||
<text class="text-base font-medium">{{item.name}}</text>
|
||||
<text class="badge badge-lucky" wx:if="{{item.id === activeProfileId}}">使用中</text>
|
||||
</view>
|
||||
<text class="text-xs text-muted block">{{item.gender === 'female' ? '女' : '男'}} · {{item.birthday}} {{item.birthTime}}</text>
|
||||
</view>
|
||||
<view class="btn-ghost text-unlucky" bindtap="removeProfile" data-id="{{item.id}}">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-center text-muted p-2" wx:else>
|
||||
<text>暂未绑定生辰,请到下方添加</text>
|
||||
</view>
|
||||
|
||||
<view class="btn-outline text-center mt-2" bindtap="toggleAddForm" wx:if="{{canAdd}}">
|
||||
{{showAddForm ? '收起' : '+ 添加生辰'}}
|
||||
</view>
|
||||
<view class="text-center text-xs text-muted mt-2" wx:else>
|
||||
免费名额已用完,可删除旧档案后添加
|
||||
</view>
|
||||
|
||||
<!-- 添加表单 -->
|
||||
<block wx:if="{{showAddForm}}">
|
||||
<view class="form-item mt-2">
|
||||
<text class="form-label">备注名(如:本人、爸爸)</text>
|
||||
<input class="form-input" placeholder="未命名" value="{{formName}}" bindinput="onFormName"/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">性别</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{formGender === 'male' ? 'active' : ''}}" bindtap="setFormGender" data-g="male">男</view>
|
||||
<view class="btn-outline {{formGender === 'female' ? 'active' : ''}}" bindtap="setFormGender" data-g="female">女</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">出生日期(公历)</text>
|
||||
<picker mode="date" value="{{formDate}}" start="1900-01-01" end="2100-12-31" bindchange="onFormDate">
|
||||
<view class="picker-input">{{formDate}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">出生时间</text>
|
||||
<picker mode="time" value="{{formTime}}" bindchange="onFormTime">
|
||||
<view class="picker-input">{{formTime}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="btn-primary text-center" bindtap="submitProfile">保存生辰</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,56 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
color: #C41E3A;
|
||||
}
|
||||
|
||||
.login-tip {
|
||||
border: 1rpx dashed #C41E3A;
|
||||
}
|
||||
|
||||
/* 生辰档案 */
|
||||
.profile-item {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.profile-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 添加生辰表单 */
|
||||
.form-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 24rpx;
|
||||
color: #6B5E53;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 20rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.picker-input {
|
||||
padding: 20rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.btn-outline.active {
|
||||
background: #C41E3A;
|
||||
color: #FFFFFF;
|
||||
border-color: #C41E3A;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
Page({
|
||||
data: {
|
||||
bookmarks: []
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadBookmarks();
|
||||
},
|
||||
|
||||
loadBookmarks() {
|
||||
const bookmarks = wx.getStorageSync('lunar-bookmarks') || [];
|
||||
this.setData({ bookmarks });
|
||||
},
|
||||
|
||||
removeBookmark(e) {
|
||||
const date = e.currentTarget.dataset.date;
|
||||
const bookmarks = this.data.bookmarks.filter(b => b.date !== date);
|
||||
wx.setStorageSync('lunar-bookmarks', bookmarks);
|
||||
this.setData({ bookmarks });
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
},
|
||||
|
||||
// 点击收藏项跳转到当日详情
|
||||
openDetail(e) {
|
||||
const date = e.currentTarget.dataset.date;
|
||||
wx.navigateTo({ url: `/pages/day-detail/day-detail?date=${date}` });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的收藏",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<view class="section-title">我的收藏</view>
|
||||
<view class="bookmark-list" wx:if="{{bookmarks.length > 0}}">
|
||||
<view class="bookmark-item" wx:for="{{bookmarks}}" wx:key="date" bindtap="openDetail" data-date="{{item.date}}">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<text class="text-base font-medium">{{item.date}}</text>
|
||||
<text class="text-xs text-muted block">{{item.lunarDate}}</text>
|
||||
</view>
|
||||
<view class="btn-ghost text-unlucky" catchtap="removeBookmark" data-date="{{item.date}}">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-tip" wx:else>
|
||||
<text class="text-sm text-muted">暂无收藏</text>
|
||||
<text class="text-xs text-muted block mt-1">在日期详情页点击「收藏」即可添加</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,24 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
color: #C41E3A;
|
||||
}
|
||||
|
||||
.bookmark-item {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.bookmark-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
Page({
|
||||
data: {
|
||||
weekStartDay: 0,
|
||||
ziHourSect: 'lateZiNextDay',
|
||||
solarTime: true,
|
||||
showBuddhist: false,
|
||||
highlightLunar: true
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadSettings();
|
||||
},
|
||||
|
||||
loadSettings() {
|
||||
const settings = wx.getStorageSync('lunar-settings') || {};
|
||||
this.setData({
|
||||
weekStartDay: settings.weekStartDay || 0,
|
||||
ziHourSect: settings.ziHourSect || 'lateZiNextDay',
|
||||
solarTime: settings.solarTime !== false,
|
||||
showBuddhist: settings.showBuddhist === true,
|
||||
highlightLunar: settings.highlightLunar !== false
|
||||
});
|
||||
},
|
||||
|
||||
saveSettings() {
|
||||
const { weekStartDay, ziHourSect, solarTime, showBuddhist, highlightLunar } = this.data;
|
||||
wx.setStorageSync('lunar-settings', { weekStartDay, ziHourSect, solarTime, showBuddhist, highlightLunar });
|
||||
},
|
||||
|
||||
setWeekStart(e) {
|
||||
this.setData({ weekStartDay: Number(e.currentTarget.dataset.v) });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
setZiSect(e) {
|
||||
this.setData({ ziHourSect: e.currentTarget.dataset.v });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
onSolarTimeChange(e) {
|
||||
this.setData({ solarTime: e.detail.value });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
onBuddhistChange(e) {
|
||||
this.setData({ showBuddhist: e.detail.value });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
onHighlightLunarChange(e) {
|
||||
this.setData({ highlightLunar: e.detail.value });
|
||||
this.saveSettings();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "偏好设置",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<view class="section-title">偏好设置</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">周起始日</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{weekStartDay === 0 ? 'active' : ''}}" bindtap="setWeekStart" data-v="0">周日</view>
|
||||
<view class="btn-outline {{weekStartDay === 1 ? 'active' : ''}}" bindtap="setWeekStart" data-v="1">周一</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">子时流派</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{ziHourSect === 'lateZiNextDay' ? 'active' : ''}}" bindtap="setZiSect" data-v="lateZiNextDay">晚子时算次日</view>
|
||||
<view class="btn-outline {{ziHourSect === 'lateZiSameDay' ? 'active' : ''}}" bindtap="setZiSect" data-v="lateZiSameDay">晚子时算当日</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">真太阳时校正</text>
|
||||
<switch checked="{{solarTime}}" bindchange="onSolarTimeChange" color="#C41E3A"/>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">显示佛历提醒</text>
|
||||
<switch checked="{{showBuddhist}}" bindchange="onBuddhistChange" color="#C41E3A"/>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">初一十五高亮</text>
|
||||
<switch checked="{{highlightLunar}}" bindchange="onHighlightLunarChange" color="#C41E3A"/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="text-xs text-muted text-center mt-2">设置仅保存在本机,修改后立即生效</view>
|
||||
</view>
|
||||
@@ -0,0 +1,32 @@
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
color: #C41E3A;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.setting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-outline.active {
|
||||
background: #C41E3A;
|
||||
color: #FFFFFF;
|
||||
border-color: #C41E3A;
|
||||
}
|
||||
+17
-159
@@ -7,140 +7,33 @@ Page({
|
||||
userInfo: null,
|
||||
loggedIn: false,
|
||||
logging: false,
|
||||
weekStartDay: 0,
|
||||
ziHourSect: 'lateZiNextDay',
|
||||
solarTime: true,
|
||||
showBuddhist: false,
|
||||
highlightLunar: true,
|
||||
bookmarks: [],
|
||||
// 生辰档案
|
||||
profiles: [],
|
||||
activeProfileId: '',
|
||||
// 菜单角标
|
||||
profileCount: 0,
|
||||
maxFree: birthProfile.MAX_PROFILES,
|
||||
canAdd: true,
|
||||
showAddForm: false,
|
||||
formName: '',
|
||||
formGender: 'male',
|
||||
formDate: '1990-01-01',
|
||||
formTime: '12:00',
|
||||
bookmarkCount: 0,
|
||||
version: versionInfo.version,
|
||||
buildNumber: versionInfo.buildNumber,
|
||||
buildTime: versionInfo.buildTime,
|
||||
commitSha: versionInfo.commitSha
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadSettings();
|
||||
this.loadBookmarks();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadBookmarks();
|
||||
this.loadProfiles();
|
||||
this.refreshUser();
|
||||
this.refreshCounts();
|
||||
},
|
||||
|
||||
loadSettings() {
|
||||
const settings = wx.getStorageSync('lunar-settings') || {};
|
||||
this.setData({
|
||||
weekStartDay: settings.weekStartDay || 0,
|
||||
ziHourSect: settings.ziHourSect || 'lateZiNextDay',
|
||||
solarTime: settings.solarTime !== false,
|
||||
showBuddhist: settings.showBuddhist === true,
|
||||
highlightLunar: settings.highlightLunar !== false
|
||||
});
|
||||
refreshUser() {
|
||||
const token = wx.getStorageSync('token');
|
||||
const userInfo = wx.getStorageSync('lunar-user-info');
|
||||
this.setData({ loggedIn: !!token, userInfo: userInfo || null });
|
||||
},
|
||||
|
||||
saveSettings() {
|
||||
const { weekStartDay, ziHourSect, solarTime, showBuddhist, highlightLunar } = this.data;
|
||||
wx.setStorageSync('lunar-settings', { weekStartDay, ziHourSect, solarTime, showBuddhist, highlightLunar });
|
||||
},
|
||||
|
||||
loadBookmarks() {
|
||||
refreshCounts() {
|
||||
const bookmarks = wx.getStorageSync('lunar-bookmarks') || [];
|
||||
this.setData({ bookmarks });
|
||||
},
|
||||
|
||||
// ==================== 生辰档案 ====================
|
||||
|
||||
loadProfiles() {
|
||||
const render = () => {
|
||||
const profiles = birthProfile.getProfiles();
|
||||
const active = birthProfile.getActiveProfile();
|
||||
this.setData({
|
||||
profiles,
|
||||
activeProfileId: active ? active.id : '',
|
||||
canAdd: profiles.length < this.data.maxFree
|
||||
});
|
||||
};
|
||||
// 已登录时先从服务端同步,失败静默回退本地
|
||||
if (birthProfile.isLoggedIn()) {
|
||||
birthProfile.syncFromServer().then(render).catch(render);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
},
|
||||
|
||||
toggleAddForm() {
|
||||
if (!this.data.showAddForm && !this.data.canAdd) {
|
||||
wx.showToast({ title: `最多可免费绑定 ${this.data.maxFree} 个生辰`, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ showAddForm: !this.data.showAddForm });
|
||||
},
|
||||
|
||||
onFormName(e) {
|
||||
this.setData({ formName: e.detail.value });
|
||||
},
|
||||
|
||||
setFormGender(e) {
|
||||
this.setData({ formGender: e.currentTarget.dataset.g });
|
||||
},
|
||||
|
||||
onFormDate(e) {
|
||||
this.setData({ formDate: e.detail.value });
|
||||
},
|
||||
|
||||
onFormTime(e) {
|
||||
this.setData({ formTime: e.detail.value });
|
||||
},
|
||||
|
||||
submitProfile() {
|
||||
const { formName, formGender, formDate, formTime } = this.data;
|
||||
if (!formDate || !formTime) {
|
||||
wx.showToast({ title: '请选择出生日期和时间', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const result = birthProfile.addProfile({
|
||||
name: formName.trim() || '未命名',
|
||||
gender: formGender,
|
||||
birthday: formDate,
|
||||
birthTime: formTime
|
||||
});
|
||||
if (!result.ok) {
|
||||
wx.showToast({ title: result.msg, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
// 已登录时推送到服务端(静默失败,不影响本地)
|
||||
birthProfile.pushProfileToServer(result.profile);
|
||||
this.setData({ showAddForm: false, formName: '' });
|
||||
wx.showToast({ title: '绑定成功', icon: 'success' });
|
||||
this.loadProfiles();
|
||||
},
|
||||
|
||||
removeProfile(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
const profile = this.data.profiles.find(p => p.id === id);
|
||||
if (!profile) return;
|
||||
wx.showModal({
|
||||
title: '删除生辰',
|
||||
content: `确定删除「${profile.name}」的生辰信息吗?`,
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
birthProfile.removeProfile(id);
|
||||
birthProfile.removeProfileFromServer(profile);
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
this.loadProfiles();
|
||||
}
|
||||
const profiles = birthProfile.getProfiles();
|
||||
this.setData({
|
||||
bookmarkCount: bookmarks.length,
|
||||
profileCount: profiles.length
|
||||
});
|
||||
},
|
||||
|
||||
@@ -166,8 +59,8 @@ Page({
|
||||
wx.setStorageSync('lunar-user-info', res.data.user);
|
||||
this.setData({ userInfo: res.data.user, loggedIn: true, logging: false });
|
||||
wx.showToast({ title: '登录成功', icon: 'success' });
|
||||
// 登录后同步服务端生辰档案
|
||||
this.loadProfiles();
|
||||
// 登录后拉取云端生辰档案,保证菜单计数准确
|
||||
birthProfile.syncFromServer().then(() => this.refreshCounts()).catch(() => {});
|
||||
} else {
|
||||
this.setData({ logging: false });
|
||||
wx.showToast({ title: (res && res.msg) || '登录失败', icon: 'none' });
|
||||
@@ -198,45 +91,10 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 偏好设置 ====================
|
||||
|
||||
setWeekStart(e) {
|
||||
this.setData({ weekStartDay: Number(e.currentTarget.dataset.v) });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
setZiSect(e) {
|
||||
this.setData({ ziHourSect: e.currentTarget.dataset.v });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
onSolarTimeChange(e) {
|
||||
this.setData({ solarTime: e.detail.value });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
onBuddhistChange(e) {
|
||||
this.setData({ showBuddhist: e.detail.value });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
onHighlightLunarChange(e) {
|
||||
this.setData({ highlightLunar: e.detail.value });
|
||||
this.saveSettings();
|
||||
},
|
||||
|
||||
removeBookmark(e) {
|
||||
const date = e.currentTarget.dataset.date;
|
||||
let bookmarks = this.data.bookmarks.filter(b => b.date !== date);
|
||||
wx.setStorageSync('lunar-bookmarks', bookmarks);
|
||||
this.setData({ bookmarks });
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
},
|
||||
|
||||
navTo(e) {
|
||||
const url = e.currentTarget.dataset.url;
|
||||
// tabBar 页面必须用 switchTab 打开
|
||||
const tabPages = ['/pages/home/home', '/pages/wish-tree/wish-tree', '/pages/bazi/bazi', '/pages/wiki/wiki', '/pages/settings/settings', '/pages/fortune/fortune'];
|
||||
const tabPages = ['/pages/home/home', '/pages/wish-tree/wish-tree', '/pages/bazi/bazi', '/pages/wiki/wiki', '/pages/settings/settings'];
|
||||
if (tabPages.includes(url)) {
|
||||
wx.switchTab({ url });
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<view class="container">
|
||||
<!-- 用户信息 -->
|
||||
<!-- 登录与账号 -->
|
||||
<view class="card">
|
||||
<view class="section-title">用户信息</view>
|
||||
<view class="flex items-center gap-2 justify-between" wx:if="{{loggedIn}}">
|
||||
<view class="flex items-center justify-between" wx:if="{{loggedIn}}">
|
||||
<view class="flex items-center gap-2">
|
||||
<image src="{{userInfo.avatar}}" class="avatar" mode="aspectFill" wx:if="{{userInfo.avatar}}"/>
|
||||
<view class="avatar avatar-placeholder" wx:else>
|
||||
@@ -10,135 +9,50 @@
|
||||
</view>
|
||||
<view>
|
||||
<text class="text-base font-medium">{{userInfo.nickname || '微信用户'}}</text>
|
||||
<text class="text-xs text-muted block">已登录,档案云端同步</text>
|
||||
<text class="text-xs text-muted block">已登录,生辰档案云端同步</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-ghost text-muted" bindtap="logout">退出</view>
|
||||
</view>
|
||||
<block wx:else>
|
||||
<view class="text-sm text-muted mb-2">登录后可将生辰档案同步到云端,换设备不丢失</view>
|
||||
<view class="flex items-center gap-2 mb-2">
|
||||
<view class="avatar avatar-placeholder">
|
||||
<text>客</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="text-base font-medium">未登录</text>
|
||||
<text class="text-xs text-muted block">绑定的生辰仅保存在本机,登录后才生效</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-primary text-center" bindtap="login">{{logging ? '登录中…' : '微信登录'}}</view>
|
||||
<view class="text-xs text-muted text-center mt-2">登录后可将生辰档案同步到云端,换设备不丢失</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 生辰管理 -->
|
||||
<!-- 我的数据 -->
|
||||
<view class="card">
|
||||
<view class="section-title flex justify-between items-center">
|
||||
<text>生辰管理</text>
|
||||
<text class="text-xs text-muted">{{profiles.length}}/{{maxFree}}</text>
|
||||
</view>
|
||||
<view class="text-xs text-muted mb-2">八字排盘基于生辰信息,每位用户可免费绑定 {{maxFree}} 个出生年月(如本人、家人)。</view>
|
||||
|
||||
<view class="profile-list" wx:if="{{profiles.length > 0}}">
|
||||
<view class="profile-item" wx:for="{{profiles}}" wx:key="id">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<view class="flex items-center gap-1">
|
||||
<text class="text-base font-medium">{{item.name}}</text>
|
||||
<text class="badge badge-lucky" wx:if="{{item.id === activeProfileId}}">使用中</text>
|
||||
</view>
|
||||
<text class="text-xs text-muted block">{{item.gender === 'female' ? '女' : '男'}} · {{item.birthday}} {{item.birthTime}}</text>
|
||||
</view>
|
||||
<view class="btn-ghost text-unlucky" bindtap="removeProfile" data-id="{{item.id}}">删除</view>
|
||||
</view>
|
||||
<view class="section-title">我的数据</view>
|
||||
<view class="menu-item" bindtap="navTo" data-url="/pages/birth-profiles/birth-profiles">
|
||||
<text class="menu-label">生辰管理</text>
|
||||
<view class="menu-right">
|
||||
<text class="menu-count">{{profileCount}}/{{maxFree}}</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-center text-muted p-2" wx:else>
|
||||
<text>暂未绑定生辰,请到下方添加</text>
|
||||
</view>
|
||||
|
||||
<view class="btn-outline text-center mt-2" bindtap="toggleAddForm" wx:if="{{canAdd}}">
|
||||
{{showAddForm ? '收起' : '+ 添加生辰'}}
|
||||
</view>
|
||||
<view class="text-center text-xs text-muted mt-2" wx:else>
|
||||
免费名额已用完,可删除旧档案后添加
|
||||
</view>
|
||||
|
||||
<!-- 添加表单 -->
|
||||
<block wx:if="{{showAddForm}}">
|
||||
<view class="form-item mt-2">
|
||||
<text class="form-label">备注名(如:本人、爸爸)</text>
|
||||
<input class="form-input" placeholder="未命名" value="{{formName}}" bindinput="onFormName"/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">性别</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{formGender === 'male' ? 'active' : ''}}" bindtap="setFormGender" data-g="male">男</view>
|
||||
<view class="btn-outline {{formGender === 'female' ? 'active' : ''}}" bindtap="setFormGender" data-g="female">女</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">出生日期(公历)</text>
|
||||
<picker mode="date" value="{{formDate}}" start="1900-01-01" end="2100-12-31" bindchange="onFormDate">
|
||||
<view class="picker-input">{{formDate}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">出生时间</text>
|
||||
<picker mode="time" value="{{formTime}}" bindchange="onFormTime">
|
||||
<view class="picker-input">{{formTime}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="btn-primary text-center" bindtap="submitProfile">保存生辰</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 偏好设置 -->
|
||||
<view class="card">
|
||||
<view class="section-title">偏好设置</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">周起始日</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{weekStartDay === 0 ? 'active' : ''}}" bindtap="setWeekStart" data-v="0">周日</view>
|
||||
<view class="btn-outline {{weekStartDay === 1 ? 'active' : ''}}" bindtap="setWeekStart" data-v="1">周一</view>
|
||||
<view class="menu-item" bindtap="navTo" data-url="/pages/bookmarks/bookmarks">
|
||||
<text class="menu-label">我的收藏</text>
|
||||
<view class="menu-right">
|
||||
<text class="menu-count" wx:if="{{bookmarkCount > 0}}">{{bookmarkCount}}</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">子时流派</text>
|
||||
<view class="flex gap-2">
|
||||
<view class="btn-outline {{ziHourSect === 'lateZiNextDay' ? 'active' : ''}}" bindtap="setZiSect" data-v="lateZiNextDay">晚子时算次日</view>
|
||||
<view class="btn-outline {{ziHourSect === 'lateZiSameDay' ? 'active' : ''}}" bindtap="setZiSect" data-v="lateZiSameDay">晚子时算当日</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">真太阳时校正</text>
|
||||
<switch checked="{{solarTime}}" bindchange="onSolarTimeChange" color="#C41E3A"/>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">显示佛历提醒</text>
|
||||
<switch checked="{{showBuddhist}}" bindchange="onBuddhistChange" color="#C41E3A"/>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">初一十五高亮</text>
|
||||
<switch checked="{{highlightLunar}}" bindchange="onHighlightLunarChange" color="#C41E3A"/>
|
||||
<view class="menu-item" bindtap="navTo" data-url="/pages/preferences/preferences">
|
||||
<text class="menu-label">设置</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收藏管理 -->
|
||||
<view class="card">
|
||||
<view class="section-title">我的收藏</view>
|
||||
<view class="bookmark-list" wx:if="{{bookmarks.length > 0}}">
|
||||
<view class="bookmark-item" wx:for="{{bookmarks}}" wx:key="date">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<text class="text-base font-medium">{{item.date}}</text>
|
||||
<text class="text-xs text-muted block">{{item.lunarDate}}</text>
|
||||
</view>
|
||||
<view class="btn-ghost text-unlucky" bindtap="removeBookmark" data-date="{{item.date}}">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-center text-muted" wx:else>
|
||||
<text>暂无收藏</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能入口 -->
|
||||
<!-- 功能 -->
|
||||
<view class="card">
|
||||
<view class="section-title">功能</view>
|
||||
<view class="menu-item" bindtap="navTo" data-url="/pages/fortune/fortune">
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@@ -21,74 +21,7 @@
|
||||
justify-content: center;
|
||||
background: #C41E3A;
|
||||
color: #FFFFFF;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
/* 生辰档案 */
|
||||
.profile-item {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.profile-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 添加生辰表单 */
|
||||
.form-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 24rpx;
|
||||
color: #6B5E53;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 20rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.picker-input {
|
||||
padding: 20rpx;
|
||||
background: #FFFBF5;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.setting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-outline.active {
|
||||
background: #C41E3A;
|
||||
color: #FFFFFF;
|
||||
border-color: #C41E3A;
|
||||
}
|
||||
|
||||
.bookmark-item {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #E8D5C4;
|
||||
}
|
||||
|
||||
.bookmark-item:last-child {
|
||||
border-bottom: none;
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
@@ -107,6 +40,17 @@
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.menu-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.menu-count {
|
||||
font-size: 24rpx;
|
||||
color: #9E8E7E;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
color: #C9B8A6;
|
||||
font-size: 36rpx;
|
||||
|
||||
@@ -75,6 +75,21 @@ Page({
|
||||
this.loadProducts();
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.stopDanmakuScheduler();
|
||||
},
|
||||
|
||||
onHide() {
|
||||
this.stopDanmakuScheduler();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 从后台/下一页返回时,若已有弹幕数据则恢复调度
|
||||
if (this.danmakuRows && this.danmakuRows.length && !(this.danmakuTimers && this.danmakuTimers.length)) {
|
||||
this.startDanmakuScheduler(this.danmakuRows);
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
const { currentTreeIndex, trees } = this.data;
|
||||
const tree = trees[currentTreeIndex];
|
||||
@@ -211,16 +226,8 @@ Page({
|
||||
};
|
||||
});
|
||||
|
||||
// 其余以弹幕飘过
|
||||
const danmaku = list.slice(MAX_HANG).map((w, i) => ({
|
||||
id: w.id,
|
||||
content: w.content,
|
||||
author: w.author || '匿名',
|
||||
type: w.type,
|
||||
top: 12 + (i % 6) * 7,
|
||||
duration: 16 + w.content.length * 0.4,
|
||||
delay: -(i * 2.1),
|
||||
}));
|
||||
// 其余以弹幕飘过:树桩处3行 + 置顶轮播下方1行,慢速均匀间隔不重叠
|
||||
const danmaku = this.buildDanmaku(list.slice(MAX_HANG));
|
||||
|
||||
const cache = { hangWishes, danmaku, top };
|
||||
this.wishCache[tree.id] = cache;
|
||||
@@ -232,8 +239,93 @@ Page({
|
||||
this.setData({
|
||||
[`trees[${index}].hangWishes`]: cache.hangWishes,
|
||||
topWishes: cache.top,
|
||||
currentDanmaku: cache.danmaku,
|
||||
});
|
||||
// 启动弹幕调度:每行一条完毕后再出下一条
|
||||
this.startDanmakuScheduler(cache.danmaku);
|
||||
},
|
||||
|
||||
// 启动弹幕调度:每行同一时刻只播一条,前一条飘完再出下一条(循环)
|
||||
startDanmakuScheduler(rows) {
|
||||
this.stopDanmakuScheduler();
|
||||
this.danmakuRows = rows || [];
|
||||
this.danmakuTimers = [];
|
||||
|
||||
if (!this.danmakuRows.length) {
|
||||
this.setData({ currentDanmaku: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
const GAP = 1.5; // 同一行前一条飘完后,间隔再出下一条(秒)
|
||||
|
||||
// 每行独立调度:row 行的第 idx 条播完后,隔 GAP 秒播第 idx+1 条
|
||||
const playRow = (row, idx) => {
|
||||
const rowData = this.danmakuRows[row];
|
||||
if (!rowData) return;
|
||||
const item = rowData.list[idx % rowData.list.length];
|
||||
// 更新该行当前弹幕(用行号作为 key 保证 setData 局部刷新)
|
||||
this.setData({
|
||||
[`currentDanmaku[${row}]`]: {
|
||||
key: `${row}-${idx % rowData.list.length}-${Date.now()}`,
|
||||
id: item.id,
|
||||
content: item.content,
|
||||
author: item.author,
|
||||
type: item.type,
|
||||
top: rowData.top,
|
||||
duration: rowData.duration,
|
||||
},
|
||||
});
|
||||
// 前一条飘完 + 间隔后,播下一条
|
||||
const timer = setTimeout(() => {
|
||||
playRow(row, idx + 1);
|
||||
}, (rowData.duration + GAP) * 1000);
|
||||
this.danmakuTimers[row] = timer;
|
||||
};
|
||||
|
||||
// 初始化 currentDanmaku 数组长度,并启动每行
|
||||
this.setData({ currentDanmaku: new Array(this.danmakuRows.length).fill(null) });
|
||||
this.danmakuRows.forEach((_, row) => {
|
||||
// 各行错开启动,避免 4 行同时从右侧涌出
|
||||
const startTimer = setTimeout(() => playRow(row, 0), row * 1200);
|
||||
this.danmakuTimers.push(startTimer);
|
||||
});
|
||||
},
|
||||
|
||||
// 停止弹幕调度
|
||||
stopDanmakuScheduler() {
|
||||
if (this.danmakuTimers) {
|
||||
this.danmakuTimers.forEach((t) => clearTimeout(t));
|
||||
this.danmakuTimers = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 构建弹幕:共 4 行(行 0-2 树桩处三行,行 3 顶部置顶轮播下方)
|
||||
// 按行分组返回,由 JS 定时器控制:同一行一条完毕后再出下一条,不重叠
|
||||
buildDanmaku(list) {
|
||||
const DURATION_BOTTOM = 30; // 树桩处一条飘完全屏的秒数(慢速)
|
||||
const DURATION_TOP = 24; // 顶部行稍快一点
|
||||
// 行位置(屏幕百分比):
|
||||
// - 行3 顶部行紧贴置顶轮播下方(稍有空隙)
|
||||
// - 行0-2 树桩处三行,第一条68%,三行紧凑(间隔5%)
|
||||
const ROW_TOPS = [68, 73, 78, 12];
|
||||
const ROW_DURATIONS = [DURATION_BOTTOM, DURATION_BOTTOM, DURATION_BOTTOM, DURATION_TOP];
|
||||
|
||||
// 轮流分配到 4 行
|
||||
const rows = [[], [], [], []];
|
||||
list.forEach((w, i) => {
|
||||
rows[i % 4].push(w);
|
||||
});
|
||||
|
||||
// 每行存该行的所有弹幕(带位置/时长),供定时器轮流播放
|
||||
return rows.map((rowList, row) => ({
|
||||
top: ROW_TOPS[row],
|
||||
duration: ROW_DURATIONS[row],
|
||||
list: rowList.map((w) => ({
|
||||
id: w.id,
|
||||
content: w.content,
|
||||
author: w.author || '匿名',
|
||||
type: w.type,
|
||||
})),
|
||||
})).filter((r) => r.list.length > 0);
|
||||
},
|
||||
|
||||
// 格式化许愿
|
||||
@@ -457,4 +549,10 @@ Page({
|
||||
path: '/pages/wish-tree/wish-tree',
|
||||
};
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '快来许愿树许下你的愿望吧!'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -45,14 +45,14 @@
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 弹幕层(超出50条的心愿缓缓飘过) -->
|
||||
<!-- 弹幕层(超出30条的心愿缓缓飘过,每行一条完毕再出下一条) -->
|
||||
<view class="danmaku-layer">
|
||||
<view
|
||||
wx:for="{{currentDanmaku}}"
|
||||
wx:key="id"
|
||||
class="danmaku-item {{item.type === 'paid' ? 'paid' : ''}}"
|
||||
style="top: {{item.top}}%; animation-duration: {{item.duration}}s; animation-delay: {{item.delay}}s;"
|
||||
>{{item.content}} · {{item.author}}</view>
|
||||
<block wx:for="{{currentDanmaku}}" wx:key="key" wx:if="{{item}}">
|
||||
<view
|
||||
class="danmaku-item {{item.type === 'paid' ? 'paid' : ''}}"
|
||||
style="top: {{item.top}}%; animation-duration: {{item.duration}}s;"
|
||||
>{{item.content}} · {{item.author}}</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 边缘暗角 -->
|
||||
|
||||
@@ -142,7 +142,8 @@
|
||||
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.6);
|
||||
animation-name: danmaku;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: both;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@@ -608,7 +609,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹幕:从屏幕右侧飘到左侧 */
|
||||
/* 弹幕:从屏幕右侧飘到左侧。每条只播一次,由 JS 控制一行一条排队出现。 */
|
||||
@keyframes danmaku {
|
||||
0% {
|
||||
transform: translateX(100vw);
|
||||
|
||||
+5
-3
@@ -4,7 +4,11 @@ go 1.22
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -36,10 +40,8 @@ require (
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
gorm.io/driver/mysql v1.6.0 // indirect
|
||||
gorm.io/gorm v1.31.2 // indirect
|
||||
)
|
||||
|
||||
+4
-2
@@ -48,6 +48,8 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -83,8 +85,6 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
@@ -99,6 +99,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -20,6 +20,7 @@ type ServerConfig struct {
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type string
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
@@ -59,6 +60,7 @@ func Load() *Config {
|
||||
CORSOrigins: getEnv("CORS_ORIGINS", ""),
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Type: getEnv("DB_TYPE", "mysql"),
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
Port: getEnv("DB_PORT", "3306"),
|
||||
User: getEnv("DB_USER", "root"),
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"github.com/gouki/lunar-server/internal/model"
|
||||
)
|
||||
@@ -13,16 +14,27 @@ var DB *gorm.DB
|
||||
|
||||
// InitDB 初始化数据库连接
|
||||
func InitDB(cfg *Config) error {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
cfg.Database.User,
|
||||
cfg.Database.Password,
|
||||
cfg.Database.Host,
|
||||
cfg.Database.Port,
|
||||
cfg.Database.Name,
|
||||
)
|
||||
|
||||
var err error
|
||||
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
|
||||
switch cfg.Database.Type {
|
||||
case "sqlite", "sqlite3":
|
||||
// DB_NAME 作为 SQLite 文件路径,例如 /app/data/lunar.db
|
||||
dbPath := cfg.Database.Name
|
||||
if dbPath == "" {
|
||||
dbPath = "lunar.db"
|
||||
}
|
||||
DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
default:
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
cfg.Database.User,
|
||||
cfg.Database.Password,
|
||||
cfg.Database.Host,
|
||||
cfg.Database.Port,
|
||||
cfg.Database.Name,
|
||||
)
|
||||
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect database: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user