feat(wish-tree): 许愿树页面微调七项
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:
+286
-173
@@ -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,137 +96,248 @@ 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,
|
||||||
wishContent: '',
|
wishContent: '',
|
||||||
wishType: 'free',
|
wishType: 'free',
|
||||||
@@ -225,44 +345,47 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wishType === 'paid' && !selectedProduct) {
|
if (wishType === 'paid' && !selectedProduct) {
|
||||||
wx.showToast({ title: '请选择许愿商品', icon: 'none' });
|
wx.showToast({ title: '请选择许愿商品', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (wishType === 'paid') {
|
if (wishType === 'paid') {
|
||||||
const orderRes = await request({
|
const orderRes = await request({
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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 |
@@ -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")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user