feat(wish-tree): 许愿树页面微调七项
Publish Mini Program Dev Version / publish (push) Failing after 11s
Build and Publish Server / build (push) Successful in 2m54s

1. 背景图 aspectFill 撑满全屏,挂载点坐标按裁剪比例重算,便签不再落在图外
2. 写下心愿按钮单行不换行,缩小尺寸
3. 移除页面内许愿树标题/数量/最近心愿,点击便签弹出便签式详情卡
4. 默认请求100条:最新50条挂树,其余以弹幕形式缓缓飘过
5. MAKE A WISH 下方增加顶级位置轮播(购买后24小时内展示),点击弹出金色便签详情,与普通便签区分
6. 多棵树分屏轮播(swiper),背景图从服务器获取,带指示点
7. 后端:WishTree新增image字段,Wish新增isTop运行时标记,图片用go:embed内嵌随二进制部署,/images路由提供访问
This commit is contained in:
gouki
2026-08-09 20:58:32 +00:00
parent f5c8d06fbe
commit fe5bed0668
9 changed files with 697 additions and 446 deletions
+281 -168
View File
@@ -1,46 +1,41 @@
const { request } = require('../../utils/request.js'); const { request, BASE_URL } = require('../../utils/request.js');
// 预设的树冠挂载点(百分比坐标 // 树冠挂载点(基于正方形背景图的图片坐标,百分比)
// x 需落在 22~78 之间,避免竖屏 aspectFill 时被左右裁掉
const CANOPY_SLOTS = [ const CANOPY_SLOTS = [
{ x: 16, y: 34 }, { x: 30, y: 30 }, { x: 36, y: 20 }, { x: 43, y: 14 }, { x: 50, y: 12 },
{ x: 27, y: 21 }, { x: 57, y: 15 }, { x: 64, y: 22 }, { x: 70, y: 32 }, { x: 27, y: 42 },
{ x: 39, y: 14 }, { x: 34, y: 38 }, { x: 42, y: 34 }, { x: 58, y: 34 }, { x: 66, y: 40 },
{ x: 52, y: 12 }, { x: 73, y: 48 }, { x: 27, y: 55 }, { x: 47, y: 26 }, { x: 53, y: 40 },
{ x: 63, y: 18 }, { x: 39, y: 48 }, { x: 61, y: 50 }, { x: 45, y: 56 },
{ x: 74, y: 26 },
{ x: 84, y: 38 },
{ x: 12, y: 50 },
{ x: 23, y: 46 },
{ x: 34, y: 40 },
{ x: 66, y: 40 },
{ x: 78, y: 50 },
{ x: 88, y: 30 },
{ x: 45, y: 30 },
]; ];
// 标签颜色配置 // 便签配色
const TAG_COLORS = { const TAG_COLORS = [
amber: { {
color: '#ffd700', color: '#ffd700',
bgColor: 'rgba(255, 215, 0, 0.9)', bgColor: 'rgba(255, 224, 130, 0.92)',
borderColor: 'rgba(255, 215, 0, 0.6)', borderColor: 'rgba(255, 215, 0, 0.5)',
shadowColor: 'rgba(255, 215, 0, 0.4)', shadowColor: 'rgba(255, 215, 0, 0.35)',
}, },
red: { {
color: '#ff6b6b', color: '#ff6b6b',
bgColor: 'rgba(255, 107, 107, 0.9)', bgColor: 'rgba(255, 150, 150, 0.92)',
borderColor: 'rgba(255, 107, 107, 0.6)', borderColor: 'rgba(255, 107, 107, 0.5)',
shadowColor: 'rgba(255, 107, 107, 0.4)', shadowColor: 'rgba(255, 107, 107, 0.35)',
}, },
jade: { {
color: '#51cf66', color: '#51cf66',
bgColor: 'rgba(81, 207, 102, 0.9)', bgColor: 'rgba(140, 226, 155, 0.92)',
borderColor: 'rgba(81, 207, 102, 0.6)', borderColor: 'rgba(81, 207, 102, 0.5)',
shadowColor: 'rgba(81, 207, 102, 0.4)', shadowColor: 'rgba(81, 207, 102, 0.35)',
}, },
}; ];
// 生成伪随机数(用于萤火虫和花瓣位置) // 挂树数量上限,超出的以弹幕展示
const MAX_HANG = 50;
// 伪随机(保证每次渲染一致)
function seeded(i, salt) { function seeded(i, salt) {
const v = Math.sin((i + 1) * 12.9898 + salt * 78.233) * 43758.5453; const v = Math.sin((i + 1) * 12.9898 + salt * 78.233) * 43758.5453;
return v - Math.floor(v); return v - Math.floor(v);
@@ -48,9 +43,12 @@ function seeded(i, salt) {
Page({ Page({
data: { data: {
wishes: [], trees: [],
recentWishes: [], currentTreeIndex: 0,
loading: true, currentDanmaku: [],
topWishes: [],
detailWish: null,
fireflies: [],
showCreateModal: false, showCreateModal: false,
wishContent: '', wishContent: '',
wishType: 'free', wishType: 'free',
@@ -58,25 +56,36 @@ Page({
maxPaidLength: 100, maxPaidLength: 100,
products: [], products: [],
selectedProduct: null, selectedProduct: null,
fireflies: [],
petals: [],
}, },
onLoad() { onLoad() {
// 屏幕尺寸,用于图片坐标到视口坐标的换算
const sys = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync();
this.vw = sys.windowWidth;
this.vh = sys.windowHeight;
// 每棵树的许愿缓存:{ treeId: { hangWishes, danmaku, top } }
this.wishCache = {};
this.initAmbiance(); this.initAmbiance();
this.loadWishes(); this.loadTrees();
this.loadProducts(); this.loadProducts();
}, },
onPullDownRefresh() { onPullDownRefresh() {
this.loadWishes().then(() => { const { currentTreeIndex, trees } = this.data;
const tree = trees[currentTreeIndex];
if (tree) {
this.loadTreeWishes(currentTreeIndex, true).then(() => {
wx.stopPullDownRefresh();
});
} else {
wx.stopPullDownRefresh(); wx.stopPullDownRefresh();
}); }
}, },
// 初始化氛围效果 // 初始化萤火虫氛围
initAmbiance() { initAmbiance() {
// 萤火虫
const fireflies = []; const fireflies = [];
for (let i = 0; i < 22; i++) { for (let i = 0; i < 22; i++) {
fireflies.push({ fireflies.push({
@@ -87,135 +96,246 @@ Page({
delay: seeded(i, 5) * 4, delay: seeded(i, 5) * 4,
}); });
} }
this.setData({ fireflies });
// 花瓣
const petals = [];
for (let i = 0; i < 10; i++) {
petals.push({
left: seeded(i, 6) * 100,
size: 5 + seeded(i, 7) * 6,
duration: 10 + seeded(i, 8) * 8,
delay: seeded(i, 9) * 12,
});
}
this.setData({ fireflies, petals });
}, },
// 加载许愿 // 图片坐标(正方形图)→ 视口坐标
async loadWishes() { // aspectFill 下竖屏会左右对称裁剪,横向需要按比例展开
try { transformSlot(slot) {
const res = await request({ const r = this.vh / this.vw;
url: '/api/wish/tree/1/wishes', const x = slot.x * r - (r - 1) * 50;
method: 'GET',
});
if (res.code === 0) {
const wishes = this.formatWishes(res.data.list || []);
this.setData({
wishes,
recentWishes: wishes.slice(0, 8),
loading: false,
});
}
} catch (err) {
console.error('加载许愿失败:', err);
const wishes = this.formatWishes(this.getMockWishes());
this.setData({
wishes,
recentWishes: wishes.slice(0, 8),
loading: false,
});
}
},
// 格式化许愿数据
formatWishes(list) {
const colorKeys = Object.keys(TAG_COLORS);
return list.map((wish, index) => {
const slot = this.pickSlot(index);
const colorKey = colorKeys[index % colorKeys.length];
const colors = TAG_COLORS[colorKey];
return {
id: wish.id,
content: wish.content,
author: wish.author || '匿名',
type: wish.type,
x: slot.x,
y: slot.y,
color: colors.color,
bgColor: colors.bgColor,
borderColor: colors.borderColor,
shadowColor: colors.shadowColor,
swayDuration: 3.4 + (index % 5) * 0.6,
swayDelay: (index % 7) * 0.35,
isNew: false,
};
});
},
// 选择挂载点
pickSlot(index) {
const base = CANOPY_SLOTS[index % CANOPY_SLOTS.length];
const wrap = Math.floor(index / CANOPY_SLOTS.length);
if (wrap === 0) return base;
const jitterX = ((wrap * 37) % 9) - 4;
const jitterY = ((wrap * 53) % 9) - 4;
return { return {
x: Math.min(92, Math.max(8, base.x + jitterX)), x: Math.min(96, Math.max(4, x)),
y: Math.min(58, Math.max(10, base.y + jitterY)), y: slot.y,
}; };
}, },
// 模拟许愿数据 // 加载许愿树列表(后端提供几个就显示几个分屏)
async loadTrees() {
try {
const res = await request({ url: '/api/wish/trees', method: 'GET' });
if (res.code === 0 && res.data.list && res.data.list.length > 0) {
const trees = res.data.list.map((t) => ({
...t,
imageUrl: this.resolveImageUrl(t.image),
hangWishes: [],
}));
this.setData({ trees });
this.loadTreeWishes(0);
return;
}
} catch (err) {
console.error('加载许愿树失败:', err);
}
// 兜底:单棵本地树
this.setData({
trees: [{
id: 1,
name: '祈福树',
imageUrl: '/images/wish-tree.jpg',
hangWishes: [],
}],
});
this.loadTreeWishes(0);
},
// 解析树背景图地址(相对路径拼接服务器域名)
resolveImageUrl(image) {
if (!image) return '/images/wish-tree.jpg';
if (/^https?:\/\//.test(image)) return image;
return BASE_URL + image;
},
// 分屏切换
onSwiperChange(e) {
const index = e.detail.current;
this.setData({ currentTreeIndex: index });
const tree = this.data.trees[index];
if (!tree) return;
const cache = this.wishCache[tree.id];
if (cache) {
// 已加载过,直接展示缓存
this.applyTreeWishes(index, cache);
} else {
this.loadTreeWishes(index);
}
},
// 加载指定树的许愿(默认100条)
async loadTreeWishes(index, force) {
const trees = this.data.trees;
const tree = trees[index];
if (!tree) return;
if (!force && this.wishCache[tree.id]) {
this.applyTreeWishes(index, this.wishCache[tree.id]);
return;
}
let list = [];
try {
const res = await request({
url: `/api/wish/tree/${tree.id}/wishes`,
method: 'GET',
});
if (res.code === 0) {
list = res.data.list || [];
}
} catch (err) {
console.error('加载许愿失败:', err);
list = this.getMockWishes();
}
// 按时间倒序(新数据在前)
list.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
// 顶级位置轮播
const top = list.filter((w) => w.isTop).map((w) => this.formatWish(w, 0));
// 最新50条挂树
const hangWishes = list.slice(0, MAX_HANG).map((w, i) => {
const wish = this.formatWish(w, i);
const slot = this.transformSlot(this.pickSlot(i));
return {
...wish,
x: slot.x,
y: slot.y,
swayDuration: 3.4 + (i % 5) * 0.6,
swayDelay: (i % 7) * 0.35,
isNew: Date.now() - new Date(w.createdAt || 0).getTime() < 60000,
};
});
// 其余以弹幕飘过
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),
}));
const cache = { hangWishes, danmaku, top };
this.wishCache[tree.id] = cache;
this.applyTreeWishes(index, cache);
},
// 应用某棵树的许愿数据到视图
applyTreeWishes(index, cache) {
this.setData({
[`trees[${index}].hangWishes`]: cache.hangWishes,
topWishes: cache.top,
currentDanmaku: cache.danmaku,
});
},
// 格式化许愿
formatWish(w, i) {
const colors = TAG_COLORS[i % TAG_COLORS.length];
return {
id: w.id,
content: w.content,
author: w.author || '匿名',
type: w.type,
isTop: !!w.isTop,
dateStr: this.formatDate(w.createdAt),
...colors,
};
},
// 选择挂载点(循环复用,复用时加抖动)
pickSlot(index) {
const base = CANOPY_SLOTS[index % CANOPY_SLOTS.length];
const wrap = Math.floor(index / CANOPY_SLOTS.length);
if (wrap === 0) return base;
const jitterX = ((wrap * 37) % 7) - 3;
const jitterY = ((wrap * 53) % 7) - 3;
return {
x: Math.min(75, Math.max(25, base.x + jitterX)),
y: Math.min(60, Math.max(10, base.y + jitterY)),
};
},
// 日期格式化
formatDate(t) {
if (!t) return '';
const d = new Date(t);
const pad = (n) => (n < 10 ? '0' + n : '' + n);
return `${d.getMonth() + 1}${d.getDate()}${pad(d.getHours())}:${pad(d.getMinutes())}`;
},
// 模拟数据(开发调试用,60条以验证弹幕效果)
getMockWishes() { getMockWishes() {
return [ const texts = [
{ id: 1, content: '愿家人平安喜乐,身体健康', author: '小满', type: 'paid' }, '愿家人平安喜乐,身体健康',
{ id: 2, content: '希望今年考研上岸,一战成硕', author: '阿远', type: 'free' }, '希望今年考研上岸,一战成硕',
{ id: 3, content: '愿世界温柔以待每一个努力的人', author: '林深', type: 'paid' }, '愿世界温柔以待每一个努力的人',
{ id: 4, content: '早日遇见那个对的人', author: '拾光', type: 'free' }, '早日遇见那个对的人',
{ id: 5, content: '祝爸妈身体硬朗,笑口常开', author: '念念', type: 'paid' }, '祝爸妈身体硬朗,笑口常开',
{ id: 6, content: '愿所求皆如愿,所行皆坦途', author: '白露', type: 'free' }, '愿所求皆如愿,所行皆坦途',
{ id: 7, content: '希望新工作顺顺利利', author: '子夜', type: 'paid' }, '希望新工作顺顺利利',
{ id: 8, content: '愿此生尽兴,赤诚善良', author: '青禾', type: 'free' }, '愿此生尽兴,赤诚善良',
{ id: 9, content: '愿代码零bug,一次通过', author: '小测', type: 'paid' }, '愿代码零bug,一次通过',
'希望减肥成功,越来越好看',
'愿所有付出都有回报',
'希望今年能去一次海边',
]; ];
const authors = ['小满', '阿远', '林深', '拾光', '念念', '白露', '子夜', '青禾', '小测', '桃桃', '余涵', '奕'];
const list = [];
for (let i = 0; i < 60; i++) {
list.push({
id: i + 1,
content: texts[i % texts.length],
author: authors[i % authors.length],
type: i % 5 === 0 ? 'paid' : 'free',
position: i === 0 ? 100 : 0,
isTop: i === 0,
createdAt: new Date(Date.now() - i * 3600 * 1000).toISOString(),
});
}
return list;
},
// 查看便签详情
openDetail(e) {
const wish = e.currentTarget.dataset.wish;
if (!wish) return;
this.setData({ detailWish: wish });
},
closeDetail() {
this.setData({ detailWish: null });
}, },
// 加载许愿商品 // 加载许愿商品
async loadProducts() { async loadProducts() {
try { try {
const res = await request({ const res = await request({ url: '/api/wish/products', method: 'GET' });
url: '/api/wish/products',
method: 'GET',
});
if (res.code === 0) { if (res.code === 0) {
this.setData({ products: res.data.list || [] }); this.setData({ products: res.data.list || [] });
return;
} }
} catch (err) { } catch (err) {
console.error('加载商品失败:', err); console.error('加载商品失败:', err);
this.setData({
products: [
{ id: 1, name: '普通许愿条', price: 100, duration: 7 },
{ id: 2, name: '精品许愿条', price: 500, duration: 30 },
{ id: 3, name: '至尊许愿条', price: 2000, duration: 90 },
],
});
} }
this.setData({
products: [
{ id: 1, name: '普通许愿条', price: 100, duration: 7 },
{ id: 2, name: '精品许愿条', price: 500, duration: 30 },
{ id: 3, name: '至尊许愿条', price: 2000, duration: 90 },
],
});
}, },
// 打开许愿弹窗 // 打开/关闭许愿弹窗
openCreateModal() { openCreateModal() {
this.setData({ showCreateModal: true }); this.setData({ showCreateModal: true });
}, },
// 关闭许愿弹窗
closeCreateModal() { closeCreateModal() {
this.setData({ this.setData({
showCreateModal: false, showCreateModal: false,
@@ -225,34 +345,37 @@ Page({
}); });
}, },
// 输入许愿内容
onInputContent(e) { onInputContent(e) {
this.setData({ wishContent: e.detail.value }); this.setData({ wishContent: e.detail.value });
}, },
// 选择许愿类型
selectType(e) { selectType(e) {
const type = e.currentTarget.dataset.type; this.setData({ wishType: e.currentTarget.dataset.type });
this.setData({ wishType: type });
}, },
// 选择商品
selectProduct(e) { selectProduct(e) {
const id = e.currentTarget.dataset.id; const id = e.currentTarget.dataset.id;
const product = this.data.products.find(p => p.id === id); const product = this.data.products.find((p) => p.id === id);
this.setData({ selectedProduct: product }); this.setData({ selectedProduct: product });
}, },
// 当前树ID
currentTreeId() {
const { trees, currentTreeIndex } = this.data;
const tree = trees[currentTreeIndex];
return tree ? tree.id : 1;
},
// 提交许愿 // 提交许愿
async submitWish() { async submitWish() {
const { wishContent, wishType, selectedProduct } = this.data; const { wishContent, wishType, selectedProduct, maxFreeLength, maxPaidLength } = this.data;
if (!wishContent.trim()) { if (!wishContent.trim()) {
wx.showToast({ title: '请输入许愿内容', icon: 'none' }); wx.showToast({ title: '请输入许愿内容', icon: 'none' });
return; return;
} }
const maxLength = wishType === 'free' ? this.data.maxFreeLength : this.data.maxPaidLength; const maxLength = wishType === 'free' ? maxFreeLength : maxPaidLength;
if (wishContent.length > maxLength) { if (wishContent.length > maxLength) {
wx.showToast({ title: `最多输入${maxLength}个字`, icon: 'none' }); wx.showToast({ title: `最多输入${maxLength}个字`, icon: 'none' });
return; return;
@@ -272,10 +395,9 @@ Page({
type: 'wish', type: 'wish',
productId: selectedProduct.id, productId: selectedProduct.id,
content: wishContent, content: wishContent,
treeId: 1, treeId: this.currentTreeId(),
}, },
}); });
if (orderRes.code === 0) { if (orderRes.code === 0) {
await this.wxPay(orderRes.data); await this.wxPay(orderRes.data);
} }
@@ -286,14 +408,13 @@ Page({
data: { data: {
content: wishContent, content: wishContent,
type: 'free', type: 'free',
treeId: 1, treeId: this.currentTreeId(),
}, },
}); });
if (res.code === 0) { if (res.code === 0) {
wx.showToast({ title: '许愿成功', icon: 'success' }); wx.showToast({ title: '许愿成功', icon: 'success' });
this.closeCreateModal(); this.closeCreateModal();
this.loadWishes(); this.loadTreeWishes(this.data.currentTreeIndex, true);
} }
} }
} catch (err) { } catch (err) {
@@ -314,7 +435,7 @@ Page({
success: () => { success: () => {
wx.showToast({ title: '支付成功', icon: 'success' }); wx.showToast({ title: '支付成功', icon: 'success' });
this.closeCreateModal(); this.closeCreateModal();
this.loadWishes(); this.loadTreeWishes(this.data.currentTreeIndex, true);
resolve(); resolve();
}, },
fail: (err) => { fail: (err) => {
@@ -326,14 +447,6 @@ Page({
}); });
}, },
// 查看许愿详情
viewWishDetail(e) {
const id = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/wish-detail/wish-detail?id=${id}`,
});
},
// 分享 // 分享
onShareAppMessage() { onShareAppMessage() {
return { return {
+99 -76
View File
@@ -1,94 +1,117 @@
<view class="container"> <view class="container">
<!-- 背景氛围:萤火虫 --> <!-- 背景氛围:萤火虫 -->
<view class="ambiance"> <view class="firefly"
<view wx:for="{{fireflies}}"
wx:for="{{fireflies}}" wx:key="index"
wx:key="index" style="left: {{item.left}}%; top: {{item.top}}%; width: {{item.size}}px; height: {{item.size}}px; animation-duration: {{item.duration}}s; animation-delay: {{item.delay}}s;"
class="firefly" ></view>
style="left: {{item.left}}%; top: {{item.top}}%; width: {{item.size}}px; height: {{item.size}}px; animation-duration: {{item.duration}}s; animation-delay: {{item.delay}}s;"
></view>
</view>
<!-- 背景氛围:花瓣 --> <!-- 多树分屏轮播 -->
<view class="petals"> <swiper
<view class="tree-swiper"
wx:for="{{petals}}" current="{{currentTreeIndex}}"
wx:key="index" bindchange="onSwiperChange"
class="petal" circular="{{trees.length > 1}}"
style="left: {{item.left}}%; width: {{item.size}}px; height: {{item.size}}px; animation-duration: {{item.duration}}s; animation-delay: {{item.delay}}s;" >
></view> <swiper-item wx:for="{{trees}}" wx:key="id">
</view> <view class="tree-page">
<!-- 背景图撑满全屏 -->
<image
src="{{item.imageUrl}}"
class="tree-bg"
mode="aspectFill"
/>
<!-- 边缘暗角,让树从中心发光 --> <!-- 树上的许愿便签(最新50条) -->
<view class="vignette"></view> <view
wx:for="{{item.hangWishes}}"
<!-- 主内容 --> wx:for-item="wish"
<view class="main-content"> wx:key="id"
<!-- 头部 --> class="wish-tag {{wish.isNew ? 'new' : ''}}"
<view class="header"> style="left: {{wish.x}}%; top: {{wish.y}}%;"
<text class="subtitle">MAKE A WISH</text> catchtap="openDetail"
<text class="title">许愿树</text> data-wish="{{wish}}"
<text class="description">月色微凉,灯火摇曳。写下心中所愿,挂上枝头,静待花开成真。</text> >
<text class="wish-count">已有 <text class="count">{{wishes.length}}</text> 个心愿挂满枝头</text> <view class="tag-sway" style="animation-duration: {{wish.swayDuration}}s; animation-delay: {{wish.swayDelay}}s;">
</view> <view class="tag-string" style="background: linear-gradient(to bottom, transparent, {{wish.color}});"></view>
<view class="tag-knot" style="background: {{wish.color}}; box-shadow: 0 0 8rpx {{wish.color}};"></view>
<!-- 许愿树 --> <view class="tag-content" style="background: {{wish.bgColor}}; border: 1rpx solid {{wish.borderColor}}; box-shadow: 0 6rpx 20rpx {{wish.shadowColor}};">
<view class="tree-container"> <text class="tag-text">{{wish.content}}</text>
<image <text class="tag-author">—— {{wish.author}}</text>
src="/images/wish-tree.jpg" </view>
class="tree-image"
mode="aspectFit"
/>
<!-- 许愿标签 -->
<view
wx:for="{{wishes}}"
wx:key="id"
class="wish-tag {{item.isNew ? 'new' : ''}}"
style="left: {{item.x}}%; top: {{item.y}}%;"
bindtap="viewWishDetail"
data-id="{{item.id}}"
>
<view class="tag-sway" style="animation-duration: {{item.swayDuration}}s; animation-delay: {{item.swayDelay}}s;">
<!-- 挂绳 -->
<view class="tag-string" style="background: linear-gradient(to bottom, transparent, {{item.color}});"></view>
<!-- 绳结 -->
<view class="tag-knot" style="background: {{item.color}}; box-shadow: 0 0 8px {{item.color}};"></view>
<!-- 标签 -->
<view class="tag-content" style="background: {{item.bgColor}}; border: 1px solid {{item.borderColor}}; box-shadow: 0 4px 16px {{item.shadowColor}};">
<text class="tag-text">{{item.content}}</text>
<text class="tag-author">—— {{item.author}}</text>
</view> </view>
</view> </view>
</view> </view>
</view> </swiper-item>
</swiper>
<!-- 最近的心愿 --> <!-- 弹幕层(超出50条的心愿缓缓飘过) -->
<view class="recent-section"> <view class="danmaku-layer">
<text class="section-title">最 近 的 心 愿</text> <view
<scroll-view class="recent-list" scroll-x enhanced show-scrollbar="{{false}}"> wx:for="{{currentDanmaku}}"
<view wx:key="id"
wx:for="{{recentWishes}}" class="danmaku-item {{item.type === 'paid' ? 'paid' : ''}}"
wx:key="id" style="top: {{item.top}}%; animation-duration: {{item.duration}}s; animation-delay: {{item.delay}}s;"
class="recent-item" >{{item.content}} · {{item.author}}</view>
bindtap="viewWishDetail"
data-id="{{item.id}}"
>
<text class="recent-text">{{item.content}}</text>
<text class="recent-author">—— {{item.author}}</text>
</view>
</scroll-view>
</view>
</view> </view>
<!-- 许愿按钮 --> <!-- 边缘暗角 -->
<view class="vignette"></view>
<!-- 顶部:MAKE A WISH + 顶级位置轮播 -->
<view class="header">
<text class="subtitle">MAKE A WISH</text>
<swiper
wx:if="{{topWishes.length > 0}}"
class="top-swiper"
vertical
autoplay
circular
interval="3000"
duration="500"
>
<swiper-item wx:for="{{topWishes}}" wx:key="id">
<view class="top-item" catchtap="openDetail" data-wish="{{item}}">
<text class="top-crown">👑</text>
<text class="top-text">{{item.content}}</text>
<text class="top-author">—— {{item.author}}</text>
</view>
</swiper-item>
</swiper>
</view>
<!-- 分屏指示点 -->
<view class="page-dots" wx:if="{{trees.length > 1}}">
<view
wx:for="{{trees}}"
wx:key="id"
class="dot {{currentTreeIndex === index ? 'active' : ''}}"
></view>
</view>
<!-- 许愿按钮(树根附近) -->
<view class="action-bar"> <view class="action-bar">
<button class="btn-wish" bindtap="openCreateModal"> <button class="btn-wish" bindtap="openCreateModal">
<text class="icon">🙏</text> <text class="icon">🙏</text>
<text>写下心愿</text> <text class="btn-text">写下心愿</text>
</button> </button>
</view> </view>
<!-- 便签详情弹窗 -->
<view class="detail-modal" wx:if="{{detailWish}}">
<view class="detail-mask" bindtap="closeDetail"></view>
<view class="note-card {{detailWish.isTop ? 'top-note' : ''}}">
<view class="note-string"></view>
<view class="note-knot" style="background: {{detailWish.color}};"></view>
<view class="note-badge" wx:if="{{detailWish.isTop}}">👑 顶级心愿</view>
<text class="note-text">{{detailWish.content}}</text>
<view class="note-footer">
<text class="note-author">—— {{detailWish.author}}</text>
<text class="note-date">{{detailWish.dateStr}}</text>
</view>
</view>
</view>
<!-- 许愿弹窗 --> <!-- 许愿弹窗 -->
<view class="modal" wx:if="{{showCreateModal}}"> <view class="modal" wx:if="{{showCreateModal}}">
<view class="modal-mask" bindtap="closeCreateModal"></view> <view class="modal-mask" bindtap="closeCreateModal"></view>
@@ -133,7 +156,7 @@
<!-- 付费商品选择 --> <!-- 付费商品选择 -->
<view class="product-section" wx:if="{{wishType === 'paid'}}"> <view class="product-section" wx:if="{{wishType === 'paid'}}">
<view class="section-title">选择许愿商品</view> <view class="product-title">选择许愿商品</view>
<view class="product-list"> <view class="product-list">
<view <view
wx:for="{{products}}" wx:for="{{products}}"
+274 -188
View File
@@ -6,41 +6,40 @@
background: #0d0d1a; background: #0d0d1a;
} }
/* 背景氛围 */ /* 多树分屏 */
.ambiance { .tree-swiper {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none;
z-index: 1; z-index: 1;
} }
.tree-page {
position: relative;
width: 100%;
height: 100%;
}
/* 背景图撑满全屏 */
.tree-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* 萤火虫 */
.firefly { .firefly {
position: absolute; position: absolute;
z-index: 3;
border-radius: 50%; border-radius: 50%;
background: #ffd700; background: #ffd700;
box-shadow: 0 0 8px #ffd700, 0 0 14px #ffd700; box-shadow: 0 0 8px #ffd700, 0 0 14px #ffd700;
animation: twinkle 3s ease-in-out infinite; animation: twinkle 3s ease-in-out infinite;
}
.petals {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; pointer-events: none;
z-index: 1;
}
.petal {
position: absolute;
top: -20px;
border-radius: 50% 50% 50% 0;
background: rgba(255, 200, 200, 0.6);
animation: drift 12s linear infinite;
} }
/* 边缘暗角 */ /* 边缘暗角 */
@@ -50,84 +49,15 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: radial-gradient(120% 90% at 50% 30%, transparent 40%, rgba(13, 13, 26, 0.9) 100%); background: radial-gradient(120% 90% at 50% 30%, transparent 45%, rgba(13, 13, 26, 0.75) 100%);
pointer-events: none; pointer-events: none;
z-index: 2; z-index: 2;
} }
/* 主内容 */ /* 许愿便签 */
.main-content {
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 60rpx 40rpx 200rpx;
}
/* 头部 */
.header {
text-align: center;
margin-bottom: 20rpx;
}
.subtitle {
display: block;
font-size: 20rpx;
letter-spacing: 8rpx;
color: rgba(255, 215, 0, 0.8);
margin-bottom: 16rpx;
}
.title {
display: block;
font-size: 72rpx;
font-weight: bold;
color: #ffd700;
text-shadow: 0 4rpx 24rpx rgba(255, 215, 0, 0.4);
margin-bottom: 24rpx;
}
.description {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.7);
line-height: 1.6;
max-width: 500rpx;
margin: 0 auto 20rpx;
}
.wish-count {
display: block;
font-size: 24rpx;
color: rgba(255, 215, 0, 0.7);
}
.wish-count .count {
font-weight: bold;
color: #ffd700;
}
/* 许愿树容器 */
.tree-container {
position: relative;
width: 100%;
max-width: 700rpx;
aspect-ratio: 1;
margin: 0 auto;
}
.tree-image {
width: 100%;
height: 100%;
object-fit: contain;
}
/* 许愿标签 */
.wish-tag { .wish-tag {
position: absolute; position: absolute;
z-index: 20; z-index: 5;
transform: translateX(-50%); transform: translateX(-50%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -148,7 +78,7 @@
.tag-string { .tag-string {
width: 2rpx; width: 2rpx;
height: 40rpx; height: 36rpx;
} }
.tag-knot { .tag-knot {
@@ -160,127 +90,297 @@
.tag-content { .tag-content {
margin-top: 8rpx; margin-top: 8rpx;
padding: 16rpx 20rpx; padding: 12rpx 16rpx;
border-radius: 12rpx; border-radius: 12rpx;
min-width: 160rpx; width: 160rpx;
max-width: 200rpx; box-sizing: border-box;
backdrop-filter: blur(4px);
transition: transform 0.3s;
}
.wish-tag:active .tag-content {
transform: translateY(-4rpx) scale(1.05);
} }
.tag-text { .tag-text {
display: block; display: -webkit-box;
font-size: 20rpx; -webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
font-size: 19rpx;
font-weight: 500; font-weight: 500;
color: #2a2a3a; color: #2a2a3a;
line-height: 1.4; line-height: 1.4;
overflow: hidden; word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
} }
.tag-author { .tag-author {
display: block; display: block;
font-size: 16rpx; font-size: 15rpx;
color: #4a4a5a; color: rgba(42, 42, 58, 0.7);
margin-top: 8rpx; margin-top: 6rpx;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* 最近的心愿 */ /* 弹幕层 */
.recent-section { .danmaku-layer {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
margin-top: 40rpx; height: 100%;
z-index: 4;
pointer-events: none;
overflow: hidden;
} }
.section-title { .danmaku-item {
display: block; position: absolute;
text-align: center; left: 0;
font-size: 22rpx;
letter-spacing: 6rpx;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 24rpx;
}
.recent-list {
display: flex;
gap: 20rpx;
padding-bottom: 20rpx;
white-space: nowrap; white-space: nowrap;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.65);
padding: 6rpx 24rpx;
border-radius: 30rpx;
background: rgba(13, 13, 26, 0.35);
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.6);
animation-name: danmaku;
animation-timing-function: linear;
animation-iteration-count: infinite;
will-change: transform;
} }
.recent-item { .danmaku-item.paid {
display: inline-flex; color: #ffd700;
background: rgba(60, 40, 0, 0.35);
}
/* 顶部 */
.header {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
padding: 40rpx 40rpx 0;
display: flex;
flex-direction: column; flex-direction: column;
min-width: 280rpx; align-items: center;
padding: 24rpx; pointer-events: none;
background: rgba(255, 255, 255, 0.08);
border: 1rpx solid rgba(255, 255, 255, 0.1);
border-radius: 20rpx;
backdrop-filter: blur(8px);
} }
.recent-text { .subtitle {
font-size: 22rpx;
letter-spacing: 8rpx;
color: rgba(255, 215, 0, 0.85);
text-shadow: 0 2rpx 10rpx rgba(255, 215, 0, 0.4);
}
/* 顶级位置轮播 */
.top-swiper {
width: 100%;
height: 64rpx;
margin-top: 16rpx;
pointer-events: auto;
}
.top-item {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
height: 64rpx;
padding: 0 32rpx;
border-radius: 32rpx;
background: linear-gradient(90deg, rgba(255, 215, 0, 0.12), rgba(255, 215, 0, 0.25), rgba(255, 215, 0, 0.12));
border: 1rpx solid rgba(255, 215, 0, 0.35);
}
.top-crown {
font-size: 26rpx; font-size: 26rpx;
color: rgba(255, 255, 255, 0.9); }
line-height: 1.5;
.top-text {
font-size: 24rpx;
color: #ffd700;
font-weight: 500;
max-width: 420rpx;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
} }
.recent-author { .top-author {
font-size: 22rpx; font-size: 20rpx;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 215, 0, 0.7);
margin-top: 12rpx;
} }
/* 许愿按钮 */ /* 分屏指示点 */
.page-dots {
position: absolute;
bottom: 190rpx;
left: 0;
right: 0;
display: flex;
justify-content: center;
gap: 12rpx;
z-index: 10;
pointer-events: none;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transition: all 0.3s;
}
.dot.active {
width: 32rpx;
border-radius: 8rpx;
background: rgba(255, 215, 0, 0.9);
}
/* 许愿按钮(树根附近,单行) */
.action-bar { .action-bar {
position: fixed; position: absolute;
bottom: 60rpx; bottom: calc(48rpx + env(safe-area-inset-bottom));
left: 50%; left: 0;
transform: translateX(-50%); right: 0;
z-index: 100; display: flex;
justify-content: center;
z-index: 20;
} }
.btn-wish { .btn-wish {
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 16rpx; white-space: nowrap;
gap: 12rpx;
background: linear-gradient(135deg, #c41e3a 0%, #e74c3c 50%, #ff6b6b 100%); background: linear-gradient(135deg, #c41e3a 0%, #e74c3c 50%, #ff6b6b 100%);
color: #fff; color: #fff;
font-size: 36rpx; font-size: 32rpx;
font-weight: bold; font-weight: bold;
padding: 28rpx 80rpx; line-height: 1;
padding: 24rpx 72rpx;
border-radius: 60rpx; border-radius: 60rpx;
border: none; border: none;
box-shadow: 0 10rpx 40rpx rgba(196, 30, 58, 0.5), box-shadow: 0 10rpx 40rpx rgba(196, 30, 58, 0.5),
0 0 60rpx rgba(255, 107, 107, 0.3); 0 0 50rpx rgba(255, 107, 107, 0.25);
transition: all 0.3s; }
.btn-wish::after {
border: none;
} }
.btn-wish:active { .btn-wish:active {
transform: scale(0.95); transform: scale(0.95);
box-shadow: 0 5rpx 20rpx rgba(196, 30, 58, 0.4);
} }
.btn-wish .icon { .btn-wish .icon {
font-size: 44rpx; font-size: 36rpx;
} }
/* 弹窗 */ .btn-wish .btn-text {
white-space: nowrap;
}
/* 便签详情弹窗 */
.detail-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.detail-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
}
.note-card {
position: relative;
width: 560rpx;
padding: 64rpx 48rpx 40rpx;
background: linear-gradient(160deg, #fff8e7 0%, #ffedc8 100%);
border-radius: 16rpx;
box-shadow: 0 20rpx 80rpx rgba(0, 0, 0, 0.5);
transform: rotate(-1.5deg);
animation: tag-drop 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
/* 顶级便签:金色卡片,与普通便签区分 */
.note-card.top-note {
background: linear-gradient(160deg, #fff3c4 0%, #ffd97a 100%);
border: 2rpx solid rgba(255, 200, 60, 0.8);
box-shadow: 0 20rpx 80rpx rgba(0, 0, 0, 0.5),
0 0 60rpx rgba(255, 215, 0, 0.35);
}
.note-string {
position: absolute;
top: -48rpx;
left: 50%;
width: 2rpx;
height: 48rpx;
background: linear-gradient(to bottom, transparent, #b8860b);
}
.note-knot {
position: absolute;
top: -10rpx;
left: 50%;
width: 16rpx;
height: 16rpx;
margin-left: -8rpx;
border-radius: 50%;
}
.note-badge {
display: inline-block;
font-size: 22rpx;
color: #8b5a00;
background: rgba(255, 255, 255, 0.6);
border: 1rpx solid rgba(180, 120, 0, 0.4);
border-radius: 24rpx;
padding: 6rpx 20rpx;
margin-bottom: 24rpx;
}
.note-text {
display: block;
font-size: 34rpx;
color: #3a2a1a;
line-height: 1.8;
word-break: break-all;
}
.note-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 36rpx;
}
.note-author {
font-size: 26rpx;
color: rgba(58, 42, 26, 0.75);
}
.note-date {
font-size: 22rpx;
color: rgba(58, 42, 26, 0.5);
}
/* 许愿弹窗 */
.modal { .modal {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -351,14 +451,12 @@
border: 2rpx solid rgba(255, 255, 255, 0.2); border: 2rpx solid rgba(255, 255, 255, 0.2);
border-radius: 20rpx; border-radius: 20rpx;
text-align: center; text-align: center;
transition: all 0.3s;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
} }
.type-item.active { .type-item.active {
border-color: #ffd700; border-color: #ffd700;
background: rgba(255, 215, 0, 0.1); background: rgba(255, 215, 0, 0.1);
box-shadow: 0 0 30rpx rgba(255, 215, 0, 0.2);
} }
.type-name { .type-name {
@@ -387,10 +485,6 @@
color: #fff; color: #fff;
} }
.wish-input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.input-count { .input-count {
text-align: right; text-align: right;
font-size: 24rpx; font-size: 24rpx;
@@ -403,7 +497,7 @@
margin-top: 40rpx; margin-top: 40rpx;
} }
.section-title { .product-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: bold; font-weight: bold;
color: #ffd700; color: #ffd700;
@@ -423,7 +517,6 @@
padding: 32rpx; padding: 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.15); border: 2rpx solid rgba(255, 255, 255, 0.15);
border-radius: 20rpx; border-radius: 20rpx;
transition: all 0.3s;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
} }
@@ -464,6 +557,7 @@
border-radius: 60rpx; border-radius: 60rpx;
font-size: 32rpx; font-size: 32rpx;
font-weight: bold; font-weight: bold;
white-space: nowrap;
} }
.btn-outline { .btn-outline {
@@ -476,7 +570,6 @@
background: linear-gradient(135deg, #c41e3a 0%, #e74c3c 100%); background: linear-gradient(135deg, #c41e3a 0%, #e74c3c 100%);
color: #fff; color: #fff;
border: none; border: none;
box-shadow: 0 8rpx 30rpx rgba(196, 30, 58, 0.4);
} }
/* 动画 */ /* 动画 */
@@ -500,30 +593,13 @@
} }
} }
@keyframes drift {
0% {
transform: translate3d(0, 0, 0) rotate(0deg);
opacity: 0;
}
10% {
opacity: 0.9;
}
90% {
opacity: 0.9;
}
100% {
transform: translate3d(-40px, 120vh, 0) rotate(360deg);
opacity: 0;
}
}
@keyframes tag-drop { @keyframes tag-drop {
0% { 0% {
transform: translateY(-24px) rotate(-8deg); transform: translateY(-40rpx) rotate(-8deg);
opacity: 0; opacity: 0;
} }
60% { 60% {
transform: translateY(4px) rotate(3deg); transform: translateY(8rpx) rotate(3deg);
opacity: 1; opacity: 1;
} }
100% { 100% {
@@ -531,3 +607,13 @@
opacity: 1; opacity: 1;
} }
} }
/* 弹幕:从屏幕右侧飘到左侧 */
@keyframes danmaku {
0% {
transform: translateX(100vw);
}
100% {
transform: translateX(-120%);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

+13
View File
@@ -1,7 +1,10 @@
package main package main
import ( import (
"embed"
"io/fs"
"log" "log"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gouki/lunar-server/internal/config" "github.com/gouki/lunar-server/internal/config"
@@ -19,6 +22,11 @@ var (
CommitSha = "unknown" CommitSha = "unknown"
) )
// 内嵌图片资源(许愿树背景图等),随二进制一起部署
//
//go:embed assets/images
var assetsFS embed.FS
func main() { func main() {
// 加载本地环境变量文件(生产环境由容器注入,忽略缺失;../.env.local 兼容从 server/ 目录启动) // 加载本地环境变量文件(生产环境由容器注入,忽略缺失;../.env.local 兼容从 server/ 目录启动)
_ = godotenv.Load(".env.local", "../.env.local", ".env") _ = godotenv.Load(".env.local", "../.env.local", ".env")
@@ -57,6 +65,11 @@ func main() {
r.StaticFile("/", "./web/index.html") r.StaticFile("/", "./web/index.html")
r.LoadHTMLFiles("./web/index.html") r.LoadHTMLFiles("./web/index.html")
// 内嵌图片资源(许愿树背景图等)
if imagesFS, err := fs.Sub(assetsFS, "assets/images"); err == nil {
r.GET("/images/*path", gin.WrapH(http.StripPrefix("/images", http.FileServer(http.FS(imagesFS)))))
}
// API 路由 // API 路由
api := r.Group("/api") api := r.Group("/api")
{ {
+10 -3
View File
@@ -56,6 +56,13 @@ func autoMigrate() error {
// seedDefaultData 初始化默认数据 // seedDefaultData 初始化默认数据
func seedDefaultData() error { func seedDefaultData() error {
// 补充已有许愿树的背景图(新增字段后老数据为空)
if err := DB.Model(&model.WishTree{}).
Where("image = '' OR image IS NULL").
Update("image", "/images/trees/tree-1.jpg").Error; err != nil {
return err
}
// 检查是否已有许愿树数据 // 检查是否已有许愿树数据
var count int64 var count int64
DB.Model(&model.WishTree{}).Count(&count) DB.Model(&model.WishTree{}).Count(&count)
@@ -65,9 +72,9 @@ func seedDefaultData() error {
// 插入默认许愿树 // 插入默认许愿树
trees := []model.WishTree{ trees := []model.WishTree{
{Name: "祈福树", Description: "许下美好愿望,祈福平安顺遂", Type: "pine", MaxWishes: 100, Sort: 0, Status: 1}, {Name: "祈福树", Description: "许下美好愿望,祈福平安顺遂", Type: "pine", Image: "/images/trees/tree-1.jpg", MaxWishes: 100, Sort: 0, Status: 1},
{Name: "姻缘树", Description: "祈求姻缘美满,爱情甜蜜", Type: "sakura", MaxWishes: 50, Sort: 1, Status: 1}, {Name: "姻缘树", Description: "祈求姻缘美满,爱情甜蜜", Type: "sakura", Image: "/images/trees/tree-1.jpg", MaxWishes: 50, Sort: 1, Status: 1},
{Name: "事业树", Description: "祈愿事业顺利,步步高升", Type: "bamboo", MaxWishes: 80, Sort: 2, Status: 1}, {Name: "事业树", Description: "祈愿事业顺利,步步高升", Type: "bamboo", Image: "/images/trees/tree-1.jpg", MaxWishes: 80, Sort: 2, Status: 1},
} }
if err := DB.Create(&trees).Error; err != nil { if err := DB.Create(&trees).Error; err != nil {
return err return err
+2
View File
@@ -14,6 +14,7 @@ type Wish struct {
Position int `gorm:"default:0" json:"position"` // 位置(用于排序/覆盖) Position int `gorm:"default:0" json:"position"` // 位置(用于排序/覆盖)
Status int `gorm:"default:1" json:"status"` // 1:正常 0:隐藏/删除 Status int `gorm:"default:1" json:"status"` // 1:正常 0:隐藏/删除
IsRobot bool `gorm:"default:false" json:"isRobot"` // 是否机器人发布 IsRobot bool `gorm:"default:false" json:"isRobot"` // 是否机器人发布
IsTop bool `gorm:"-" json:"isTop"` // 是否顶级位置(运行时计算)
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
@@ -24,6 +25,7 @@ type WishTree struct {
Name string `gorm:"size:50" json:"name"` Name string `gorm:"size:50" json:"name"`
Description string `gorm:"size:255" json:"description"` Description string `gorm:"size:255" json:"description"`
Type string `gorm:"size:20;default:'pine'" json:"type"` // pine:松树 sakura:樱花 bamboo:竹子 Type string `gorm:"size:20;default:'pine'" json:"type"` // pine:松树 sakura:樱花 bamboo:竹子
Image string `gorm:"size:255" json:"image"` // 背景图地址
MaxWishes int `gorm:"default:100" json:"maxWishes"` // 最大许愿条数 MaxWishes int `gorm:"default:100" json:"maxWishes"` // 最大许愿条数
Sort int `gorm:"default:0" json:"sort"` // 排序 Sort int `gorm:"default:0" json:"sort"` // 排序
Status int `gorm:"default:1" json:"status"` Status int `gorm:"default:1" json:"status"`
+8 -2
View File
@@ -97,15 +97,21 @@ func (s *WishService) CreateWish(wish *model.Wish) error {
return s.db.Create(wish).Error return s.db.Create(wish).Error
} }
// GetTreeWishes 获取指定树的许愿列表 // GetTreeWishes 获取指定树的许愿列表(最新100条,标记顶级位置)
func (s *WishService) GetTreeWishes(treeID uint) ([]*model.Wish, error) { func (s *WishService) GetTreeWishes(treeID uint) ([]*model.Wish, error) {
var wishes []*model.Wish var wishes []*model.Wish
if err := s.db.Where("tree_id = ? AND status = 1", treeID). if err := s.db.Where("tree_id = ? AND status = 1", treeID).
Order("position DESC, created_at DESC"). Order("created_at DESC").
Limit(100). Limit(100).
Find(&wishes).Error; err != nil { Find(&wishes).Error; err != nil {
return nil, err return nil, err
} }
// 计算顶级位置:高位置付费许愿且购买后24小时内
for _, w := range wishes {
w.IsTop = w.Position >= 100 && time.Since(w.CreatedAt) <= 24*time.Hour
}
return wishes, nil return wishes, nil
} }
+5 -4
View File
@@ -57,6 +57,7 @@ CREATE TABLE IF NOT EXISTS wish_trees (
name VARCHAR(50) NOT NULL COMMENT '名称', name VARCHAR(50) NOT NULL COMMENT '名称',
description VARCHAR(255) DEFAULT '' COMMENT '描述', description VARCHAR(255) DEFAULT '' COMMENT '描述',
type VARCHAR(20) DEFAULT 'pine' COMMENT '树类型 pine:松树 sakura:樱花 bamboo:竹子', type VARCHAR(20) DEFAULT 'pine' COMMENT '树类型 pine:松树 sakura:樱花 bamboo:竹子',
image VARCHAR(255) DEFAULT '' COMMENT '背景图地址',
max_wishes INT DEFAULT 100 COMMENT '最大许愿条数', max_wishes INT DEFAULT 100 COMMENT '最大许愿条数',
sort INT DEFAULT 0 COMMENT '排序', sort INT DEFAULT 0 COMMENT '排序',
status TINYINT DEFAULT 1 COMMENT '状态', status TINYINT DEFAULT 1 COMMENT '状态',
@@ -96,10 +97,10 @@ CREATE TABLE IF NOT EXISTS wish_products (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿商品表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许愿商品表';
-- 插入默认许愿树 -- 插入默认许愿树
INSERT IGNORE INTO wish_trees (id, name, description, type, max_wishes, sort) VALUES INSERT IGNORE INTO wish_trees (id, name, description, type, image, max_wishes, sort) VALUES
(1, '祈福树', '许下美好愿望,祈福平安顺遂', 'pine', 100, 0), (1, '祈福树', '许下美好愿望,祈福平安顺遂', 'pine', '/images/trees/tree-1.jpg', 100, 0),
(2, '姻缘树', '祈求姻缘美满,爱情甜蜜', 'sakura', 50, 1), (2, '姻缘树', '祈求姻缘美满,爱情甜蜜', 'sakura', '/images/trees/tree-1.jpg', 50, 1),
(3, '事业树', '祈愿事业顺利,步步高升', 'bamboo', 80, 2); (3, '事业树', '祈愿事业顺利,步步高升', 'bamboo', '/images/trees/tree-1.jpg', 80, 2);
-- 插入默认许愿商品 -- 插入默认许愿商品
INSERT IGNORE INTO wish_products (id, name, description, price, duration, position) VALUES INSERT IGNORE INTO wish_products (id, name, description, price, duration, position) VALUES