Compare commits
10 Commits
d6f781a666
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 058a575e10 | |||
| 0715a16d91 | |||
| 9fe2fd42e0 | |||
| 40f3a8cfa8 | |||
| 2031e788b0 | |||
| a34d7df090 | |||
| bcba77f912 | |||
| 5789e8bf17 | |||
| 5800466e69 | |||
| 2d8ffd842a |
@@ -21,7 +21,7 @@ App({
|
||||
|
||||
if (token && typeof token === 'string') {
|
||||
wx.setStorageSync('token', token);
|
||||
console.log('Login successful');
|
||||
|
||||
if (this._resolveLogin) this._resolveLogin(token);
|
||||
|
||||
// Background Profile Update
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"pages/plant-detail/growth-record/index",
|
||||
"pages/wiki/detail/index",
|
||||
"pages/wiki/identify/index",
|
||||
"pages/wiki/chat/index",
|
||||
"pages/wiki/chat/history/index",
|
||||
"pages/profile/identify-history/index",
|
||||
"pages/profile/badges/index",
|
||||
"pages/profile/badges/level-detail/index",
|
||||
@@ -19,7 +21,8 @@
|
||||
"pages/profile/favorites/index",
|
||||
"pages/profile/posts/index",
|
||||
"pages/profile/about/index",
|
||||
"pages/profile/exchange/index"
|
||||
"pages/profile/exchange/index",
|
||||
"pages/profile/exchange/orders/index"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
@@ -65,5 +68,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"sitemapLocation": "sitemap.json"
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"requiredPrivateInfos": [
|
||||
"chooseLocation"
|
||||
],
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于发布内容时展示地理位置"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
@@ -138,23 +138,49 @@ Page({
|
||||
},
|
||||
|
||||
formatLocation(address, name) {
|
||||
if (!address) return name || '';
|
||||
if (!address && !name) return '';
|
||||
|
||||
// Municipalities
|
||||
const source = address || '';
|
||||
|
||||
// 直辖市
|
||||
const munis = ['北京', '上海', '天津', '重庆'];
|
||||
for (let m of munis) {
|
||||
if (address.startsWith(m)) {
|
||||
if (source.startsWith(m + '市') || source.startsWith(m)) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
// Standard: Prov + City (Simplify names)
|
||||
const match = address.match(/^(.+?)(?:省|自治区)(.+?)(?:市|自治州|地区|盟)/);
|
||||
if (match) {
|
||||
return `${match[1]}.${match[2]}`;
|
||||
// 特别行政区
|
||||
if (source.startsWith('香港') || source.startsWith('澳门')) {
|
||||
return source.substring(0, 2);
|
||||
}
|
||||
|
||||
return name || address;
|
||||
// 自治区:提取简称
|
||||
const autoRegions = [
|
||||
{ prefix: '内蒙古', full: '内蒙古自治区' },
|
||||
{ prefix: '广西', full: '广西壮族自治区' },
|
||||
{ prefix: '西藏', full: '西藏自治区' },
|
||||
{ prefix: '宁夏', full: '宁夏回族自治区' },
|
||||
{ prefix: '新疆', full: '新疆维吾尔自治区' }
|
||||
];
|
||||
for (let region of autoRegions) {
|
||||
if (source.startsWith(region.prefix)) {
|
||||
return region.prefix;
|
||||
}
|
||||
}
|
||||
|
||||
// 标准省份:只提取省名
|
||||
const provinceMatch = source.match(/^(.+?)省/);
|
||||
if (provinceMatch) {
|
||||
return provinceMatch[1];
|
||||
}
|
||||
|
||||
// 如果都不匹配,截取短地址
|
||||
if (source.length > 6) {
|
||||
return source.substring(0, 6);
|
||||
}
|
||||
|
||||
return source || name || '';
|
||||
},
|
||||
|
||||
toggleTopic(e) {
|
||||
@@ -236,11 +262,11 @@ Page({
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '发布成功', icon: 'success' });
|
||||
|
||||
// Refresh previous page
|
||||
// 直接通知上一页(社区列表页)刷新数据
|
||||
const pages = getCurrentPages();
|
||||
const prevPage = pages[pages.length - 2];
|
||||
if (prevPage && prevPage.onRefresh) {
|
||||
prevPage.onRefresh();
|
||||
const communityPage = pages[pages.length - 2];
|
||||
if (communityPage && communityPage.onRefresh) {
|
||||
communityPage.onRefresh();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -81,6 +81,15 @@ Page({
|
||||
const publisher = item.publisher || {};
|
||||
const avatarObj = publisher.avatar || {};
|
||||
|
||||
// 从 likeList 提取所有点赞者名字,当前用户显示"我"
|
||||
const app = getApp();
|
||||
const myId = (app.globalData.userInfo || {}).id;
|
||||
const likeNames = (item.likeList || []).map(like => {
|
||||
const liker = like.liker || {};
|
||||
if (myId && liker.id === myId) return '我';
|
||||
return liker.nickName || liker.name || '花友';
|
||||
});
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
user: publisher.nickName || publisher.name || '花友',
|
||||
@@ -89,7 +98,7 @@ Page({
|
||||
images: (item.imgList || []).map(img => img.url),
|
||||
location: item.location || '',
|
||||
time: item.createdAtStr || '刚刚',
|
||||
likes: item.hasLiked === 1 ? ['我'] : [],
|
||||
likes: likeNames,
|
||||
comments: (item.commentList || []).map(c => ({
|
||||
id: c.id,
|
||||
user: c.commentator ? (c.commentator.nickName || c.commentator.name) : '花友',
|
||||
@@ -180,10 +189,13 @@ Page({
|
||||
const updatedPosts = this.data.posts.map(p => {
|
||||
if (p.id === postId) {
|
||||
const liked = !p.likedByMe;
|
||||
const updatedLikes = liked
|
||||
? (p.likes.includes('我') ? p.likes : [...p.likes, '我'])
|
||||
: p.likes.filter(n => n !== '我');
|
||||
return {
|
||||
...p,
|
||||
likedByMe: liked,
|
||||
likes: liked ? ['我'] : []
|
||||
likes: updatedLikes
|
||||
};
|
||||
}
|
||||
return p;
|
||||
|
||||
@@ -135,11 +135,16 @@
|
||||
|
||||
<!-- Empty State -->
|
||||
<view wx:else class="empty-feed">
|
||||
<view class="empty-icon">
|
||||
<t-icon name="chat" size="80rpx" color="#ccc" />
|
||||
<view class="empty-scene">
|
||||
<view class="empty-glow"></view>
|
||||
<view class="empty-emoji anim-breathe">💬</view>
|
||||
</view>
|
||||
<text class="empty-text">暂无相关动态</text>
|
||||
<text class="empty-title">暂无相关动态</text>
|
||||
<text class="empty-hint">快来发布第一条动态吧</text>
|
||||
<view class="empty-cta" bindtap="goToCreatePost">
|
||||
<t-icon name="add" size="28rpx" color="#fff" />
|
||||
<text>发布动态</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Bottom Spacer -->
|
||||
|
||||
+63
-19
@@ -276,31 +276,75 @@ page {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx;
|
||||
color: #999;
|
||||
padding: 100rpx 40rpx 60rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 50%;
|
||||
.empty-scene {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
.empty-glow {
|
||||
position: absolute;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(85,139,47,0.12) 0%, transparent 70%);
|
||||
animation: emptyPulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes emptyPulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.7; }
|
||||
50% { transform: scale(1.1); opacity: 1; }
|
||||
}
|
||||
|
||||
.empty-emoji {
|
||||
font-size: 96rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.anim-breathe {
|
||||
animation: breathe 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0%, 100% { transform: scale(1) translateY(0); }
|
||||
50% { transform: scale(1.05) translateY(-8rpx); }
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
font-weight: 700;
|
||||
color: #558B2F;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
color: #90A4AE;
|
||||
margin-bottom: 36rpx;
|
||||
}
|
||||
|
||||
.empty-cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 16rpx 40rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, #558B2F, #7CB342);
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 6rpx 20rpx rgba(85,139,47,0.3);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.empty-cta:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Floating Action Button */
|
||||
@@ -308,23 +352,23 @@ page {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 60rpx;
|
||||
background: #558B2F;
|
||||
background: rgba(85, 139, 47, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
color: white;
|
||||
padding: 24rpx 40rpx;
|
||||
border-radius: 60rpx;
|
||||
padding: 20rpx 36rpx;
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4);
|
||||
gap: 10rpx;
|
||||
box-shadow: 0 8rpx 28rpx rgba(85, 139, 47, 0.35);
|
||||
z-index: 100;
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.floating-add-btn:active {
|
||||
transform: scale(0.92);
|
||||
box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2);
|
||||
}
|
||||
|
||||
/* WeChat Style Action Container */
|
||||
|
||||
+31
-3
@@ -7,6 +7,8 @@ Page({
|
||||
plants: [],
|
||||
dateString: '',
|
||||
greeting: '',
|
||||
bannerList: [],
|
||||
currentBanner: 0,
|
||||
|
||||
// Pagination
|
||||
currentPage: 1,
|
||||
@@ -14,12 +16,14 @@ Page({
|
||||
total: 0,
|
||||
isLastPage: false,
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
scrollTop: 0
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.initTime();
|
||||
this.loadPlants(true);
|
||||
this.loadBanners();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
@@ -34,13 +38,23 @@ Page({
|
||||
this.loadPlants(true);
|
||||
},
|
||||
|
||||
// Pull to refresh
|
||||
// Pull to refresh (page-level)
|
||||
onPullDownRefresh() {
|
||||
this.loadPlants(true).then(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
},
|
||||
|
||||
// Pull to refresh (scroll-view)
|
||||
onRefresh() {
|
||||
this.setData({ isRefreshing: true });
|
||||
this.loadPlants(true).then(() => {
|
||||
this.setData({ isRefreshing: false });
|
||||
}).catch(() => {
|
||||
this.setData({ isRefreshing: false });
|
||||
});
|
||||
},
|
||||
|
||||
// Infinite scroll
|
||||
onReachBottom() {
|
||||
if (!this.data.isLastPage && !this.data.isLoading) {
|
||||
@@ -91,6 +105,16 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
async loadBanners() {
|
||||
try {
|
||||
const res = await request.get('/plantBanner/activeList');
|
||||
const list = (res.list || []).map(item => item.image ? item.image.url : '');
|
||||
this.setData({ bannerList: list.filter(Boolean) });
|
||||
} catch (err) {
|
||||
console.error('Load banners failed', err);
|
||||
}
|
||||
},
|
||||
|
||||
initTime() {
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
@@ -115,6 +139,10 @@ Page({
|
||||
updateTime();
|
||||
},
|
||||
|
||||
onBannerChange(e) {
|
||||
this.setData({ currentBanner: e.detail.current });
|
||||
},
|
||||
|
||||
navigateToDetail(e) {
|
||||
const { id } = e.currentTarget.dataset;
|
||||
wx.navigateTo({
|
||||
@@ -136,14 +164,14 @@ Page({
|
||||
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '我的植物花园 - Sundynix Plant',
|
||||
title: '我的私人花园 - 植趣',
|
||||
path: '/pages/garden/index'
|
||||
};
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '我的植物花园 - Sundynix Plant'
|
||||
title: '我的私人花园 - 植趣'
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
+27
-3
@@ -17,7 +17,27 @@
|
||||
</view>
|
||||
|
||||
<view class="banner-container">
|
||||
<image src="https://images.unsplash.com/photo-1585320806297-9794b3e4eeae?w=800" class="garden-banner" mode="aspectFill" />
|
||||
<swiper
|
||||
class="banner-swiper"
|
||||
indicator-dots="{{false}}"
|
||||
autoplay="{{true}}"
|
||||
interval="{{4000}}"
|
||||
duration="{{600}}"
|
||||
circular="{{true}}"
|
||||
bindchange="onBannerChange"
|
||||
easing-function="easeInOutCubic"
|
||||
>
|
||||
<swiper-item wx:for="{{bannerList}}" wx:key="*this">
|
||||
<image src="{{item}}" mode="aspectFill" class="banner-image" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view class="banner-dots">
|
||||
<view
|
||||
wx:for="{{bannerList}}"
|
||||
wx:key="*this"
|
||||
class="dot {{currentBanner === index ? 'active' : ''}}"
|
||||
/>
|
||||
</view>
|
||||
<view class="banner-overlay">
|
||||
<text class="count-tag">共养护 {{total}} 盆植物</text>
|
||||
</view>
|
||||
@@ -45,7 +65,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view wx:else scroll-y class="garden-list-container" enhanced show-scrollbar="{{false}}" bindscrolltolower="onScrollLower" scroll-top="{{scrollTop}}">
|
||||
<scroll-view wx:else scroll-y class="garden-list-container" enhanced show-scrollbar="{{false}}" bindscrolltolower="onScrollLower" scroll-top="{{scrollTop}}" refresher-enabled="{{true}}" bindrefresherrefresh="onRefresh" refresher-triggered="{{isRefreshing}}">
|
||||
<view class="plant-grid">
|
||||
<view wx:for="{{plants}}" wx:key="id" class="plant-card" bindtap="navigateToDetail" data-id="{{item.id}}">
|
||||
<view class="plant-image-container">
|
||||
@@ -55,13 +75,17 @@
|
||||
width="100%"
|
||||
height="100%"
|
||||
t-class="uploaded-img"
|
||||
lazy
|
||||
/>
|
||||
<view class="days-badge">{{item.daysPlanted}}天</view>
|
||||
</view>
|
||||
<view class="plant-info">
|
||||
<text class="plant-name">{{item.name}}</text>
|
||||
<view class="status-wrap">
|
||||
<text class="status">生长中</text>
|
||||
<text class="status">{{item.desc || '生长中'}}</text>
|
||||
<view wx:if="{{item.carePlans && item.carePlans.length > 0}}" class="care-dot-badge">
|
||||
<text>{{item.carePlans.length}}项养护</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
+55
-8
@@ -83,11 +83,41 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.garden-banner {
|
||||
.banner-swiper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Custom indicator dots */
|
||||
.banner-dots {
|
||||
position: absolute;
|
||||
bottom: 48rpx;
|
||||
right: 32rpx;
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.banner-dots .dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.banner-dots .dot.active {
|
||||
width: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 0 8rpx rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -191,6 +221,9 @@
|
||||
|
||||
.status-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status {
|
||||
@@ -200,6 +233,20 @@
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 180rpx;
|
||||
}
|
||||
|
||||
.care-dot-badge {
|
||||
font-size: 20rpx;
|
||||
color: #1565C0;
|
||||
background: #E3F2FD;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 10rpx;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Custom Floating Button */
|
||||
@@ -207,23 +254,23 @@
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 60rpx;
|
||||
background: #558B2F;
|
||||
background: rgba(85, 139, 47, 0.92);
|
||||
backdrop-filter: blur(12px);
|
||||
color: white;
|
||||
padding: 24rpx 40rpx;
|
||||
border-radius: 60rpx;
|
||||
padding: 20rpx 36rpx;
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4);
|
||||
gap: 10rpx;
|
||||
box-shadow: 0 8rpx 28rpx rgba(85, 139, 47, 0.35);
|
||||
z-index: 1000;
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.floating-add-btn:active {
|
||||
transform: scale(0.92);
|
||||
box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2);
|
||||
}
|
||||
/* List Footer */
|
||||
.list-footer {
|
||||
|
||||
@@ -72,7 +72,12 @@ Page({
|
||||
}
|
||||
|
||||
// 2. Prepare payload
|
||||
const mapTitle = { growth: '生长记录', repot: '换盆记录', pest: '病虫害记录', other: '日常记录' };
|
||||
const mapTitle = {
|
||||
growth: '生长记录', flower: '开花记录', repot: '换盆记录',
|
||||
prune: '修剪记录', fertilize: '施肥记录', soil: '换土记录',
|
||||
pest: '病虫害记录', medicine: '用药记录', move: '移位记录',
|
||||
other: '日常记录'
|
||||
};
|
||||
const title = mapTitle[this.data.recordType] || '日常记录';
|
||||
|
||||
const payload = {
|
||||
|
||||
@@ -8,14 +8,38 @@
|
||||
<t-icon name="thumb-up" size="32rpx" />
|
||||
<text>生长</text>
|
||||
</view>
|
||||
<view class="chip {{recordType === 'flower' ? 'active' : ''}}" bindtap="setRecordType" data-type="flower">
|
||||
<t-icon name="heart" size="32rpx" />
|
||||
<text>开花</text>
|
||||
</view>
|
||||
<view class="chip {{recordType === 'repot' ? 'active' : ''}}" bindtap="setRecordType" data-type="repot">
|
||||
<t-icon name="swap" size="32rpx" />
|
||||
<text>换盆</text>
|
||||
</view>
|
||||
<view class="chip {{recordType === 'prune' ? 'active' : ''}}" bindtap="setRecordType" data-type="prune">
|
||||
<t-icon name="cut" size="32rpx" />
|
||||
<text>修剪</text>
|
||||
</view>
|
||||
<view class="chip {{recordType === 'fertilize' ? 'active' : ''}}" bindtap="setRecordType" data-type="fertilize">
|
||||
<t-icon name="edit-1" size="32rpx" />
|
||||
<text>施肥</text>
|
||||
</view>
|
||||
<view class="chip {{recordType === 'soil' ? 'active' : ''}}" bindtap="setRecordType" data-type="soil">
|
||||
<t-icon name="layers" size="32rpx" />
|
||||
<text>换土</text>
|
||||
</view>
|
||||
<view class="chip {{recordType === 'pest' ? 'active' : ''}}" bindtap="setRecordType" data-type="pest">
|
||||
<t-icon name="error-circle" size="32rpx" />
|
||||
<text>病虫害</text>
|
||||
</view>
|
||||
<view class="chip {{recordType === 'medicine' ? 'active' : ''}}" bindtap="setRecordType" data-type="medicine">
|
||||
<t-icon name="heart-filled" size="32rpx" />
|
||||
<text>用药</text>
|
||||
</view>
|
||||
<view class="chip {{recordType === 'move' ? 'active' : ''}}" bindtap="setRecordType" data-type="move">
|
||||
<t-icon name="map-navigation" size="32rpx" />
|
||||
<text>移位</text>
|
||||
</view>
|
||||
<view class="chip {{recordType === 'other' ? 'active' : ''}}" bindtap="setRecordType" data-type="other">
|
||||
<t-icon name="file" size="32rpx" />
|
||||
<text>其他</text>
|
||||
|
||||
@@ -79,13 +79,32 @@ Page({
|
||||
imageUrl = item.imgList[0].url;
|
||||
}
|
||||
|
||||
// Type → icon/color mapping
|
||||
const typeConfig = {
|
||||
growth: { icon: 'thumb-up', color: '#4CAF50', accent: '#E8F5E9' },
|
||||
flower: { icon: 'heart', color: '#E91E63', accent: '#FCE4EC' },
|
||||
repot: { icon: 'swap', color: '#FF9800', accent: '#FFF3E0' },
|
||||
prune: { icon: 'cut', color: '#9C27B0', accent: '#F3E5F5' },
|
||||
fertilize: { icon: 'edit-1', color: '#FF9800', accent: '#FFF8E1' },
|
||||
soil: { icon: 'layers', color: '#795548', accent: '#EFEBE9' },
|
||||
pest: { icon: 'error-circle', color: '#F44336', accent: '#FFEBEE' },
|
||||
medicine: { icon: 'heart-filled', color: '#E91E63', accent: '#FCE4EC' },
|
||||
move: { icon: 'map-navigation', color: '#00BCD4', accent: '#E0F7FA' },
|
||||
other: { icon: 'file', color: '#2196F3', accent: '#E3F2FD' },
|
||||
};
|
||||
const tag = item.tag || 'other';
|
||||
const cfg = typeConfig[tag] || typeConfig.other;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
date: item.createdAtStr ? item.createdAtStr.split(' ')[0] : '',
|
||||
type: item.tag || 'growth',
|
||||
type: tag,
|
||||
title: item.name || '成长记录',
|
||||
content: item.content || item.desc || '',
|
||||
image: imageUrl
|
||||
image: imageUrl,
|
||||
iconName: cfg.icon,
|
||||
iconColor: cfg.color,
|
||||
accentColor: cfg.accent,
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
@@ -7,14 +7,20 @@
|
||||
</view>
|
||||
|
||||
<view class="header-gallery">
|
||||
<t-swiper
|
||||
current="{{activeImageIndex}}"
|
||||
autoplay="{{false}}"
|
||||
navigation="{{ { type: '' } }}"
|
||||
list="{{swiperImages}}"
|
||||
bind:change="onSwiperChange"
|
||||
height="500rpx"
|
||||
/>
|
||||
<block wx:if="{{swiperImages.length > 0}}">
|
||||
<t-swiper
|
||||
current="{{activeImageIndex}}"
|
||||
autoplay="{{false}}"
|
||||
navigation="{{ { type: '' } }}"
|
||||
list="{{swiperImages}}"
|
||||
bind:change="onSwiperChange"
|
||||
height="500rpx"
|
||||
/>
|
||||
</block>
|
||||
<view wx:else class="header-empty-placeholder">
|
||||
<text class="empty-plant-emoji">🌱</text>
|
||||
<text class="empty-photo-hint">去编辑页添加照片吧</text>
|
||||
</view>
|
||||
<view class="header-gradient"></view>
|
||||
|
||||
<!-- Image Counter -->
|
||||
@@ -177,12 +183,11 @@
|
||||
<view wx:for="{{displayRecords}}" wx:key="id" class="timeline-item">
|
||||
<view class="timeline-dot"></view>
|
||||
<text class="timeline-date">{{item.date}}</text>
|
||||
<view class="timeline-content-box">
|
||||
<view class="timeline-content-box" style="border-left-color: {{item.iconColor || '#81C784'}};">
|
||||
<view class="timeline-title">
|
||||
<t-icon wx:if="{{item.type === 'growth'}}" name="thumb-up" size="32rpx" color="#4CAF50" />
|
||||
<t-icon wx:elif="{{item.type === 'repot'}}" name="swap" size="32rpx" color="#FF9800" />
|
||||
<t-icon wx:elif="{{item.type === 'pest'}}" name="error-circle" size="32rpx" color="#F44336" />
|
||||
<t-icon wx:else name="file" size="32rpx" color="#2196F3" />
|
||||
<view class="timeline-type-badge" style="background: {{item.accentColor || '#E8F5E9'}};">
|
||||
<t-icon name="{{item.iconName || 'file'}}" size="28rpx" color="{{item.iconColor || '#4CAF50'}}" />
|
||||
</view>
|
||||
<text>{{item.title}}</text>
|
||||
</view>
|
||||
<text class="timeline-desc">{{item.content}}</text>
|
||||
@@ -190,7 +195,7 @@
|
||||
wx:if="{{item.image}}"
|
||||
src="{{item.image}}"
|
||||
mode="aspectFill"
|
||||
style="width: 220rpx; height: 220rpx; border-radius: 16rpx; margin-top: 16rpx;"
|
||||
style="width: 100%; height: 280rpx; border-radius: 20rpx; margin-top: 20rpx;"
|
||||
class="timeline-img"
|
||||
bindtap="handlePreviewRecordImage"
|
||||
data-src="{{item.image}}"
|
||||
|
||||
+128
-19
@@ -110,34 +110,39 @@ page {
|
||||
border-bottom: 2rpx solid rgba(0, 0, 0, 0.05);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
flex-shrink: 0;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.pd-tab-btn {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 32rpx 48rpx;
|
||||
font-size: 30rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #6B7280;
|
||||
background: none;
|
||||
border: none;
|
||||
position: relative;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pd-tab-btn.active {
|
||||
color: #558B2F;
|
||||
font-weight: 600;
|
||||
color: #2E7D32;
|
||||
font-weight: 700;
|
||||
background: rgba(85, 139, 47, 0.06);
|
||||
}
|
||||
|
||||
.pd-tab-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2rpx;
|
||||
left: 25%;
|
||||
right: 25%;
|
||||
left: 20%;
|
||||
right: 20%;
|
||||
height: 6rpx;
|
||||
background: #558B2F;
|
||||
background: linear-gradient(90deg, #689F38, #33691E);
|
||||
border-radius: 4rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
@@ -145,7 +150,7 @@ page {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 48rpx;
|
||||
padding-bottom: 160rpx;
|
||||
padding-bottom: 48rpx;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@@ -182,12 +187,25 @@ page {
|
||||
color: #263238;
|
||||
}
|
||||
|
||||
/* Care Log List - Matching Prototype */
|
||||
/* Care Log List - Timeline Style */
|
||||
.care-log-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
gap: 0;
|
||||
margin-bottom: 40rpx;
|
||||
position: relative;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.care-log-list::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 6rpx;
|
||||
top: 32rpx;
|
||||
bottom: 32rpx;
|
||||
width: 3rpx;
|
||||
background: linear-gradient(to bottom, #C8E6C9, #E8F5E9);
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.care-log-item {
|
||||
@@ -195,6 +213,27 @@ page {
|
||||
border-radius: 32rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
margin-bottom: 20rpx;
|
||||
margin-left: 24rpx;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.care-log-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -30rpx;
|
||||
top: 40rpx;
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
background: #81C784;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #F4F6F0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.care-log-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.log-left {
|
||||
@@ -309,8 +348,19 @@ page {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 12rpx;
|
||||
background: linear-gradient(90deg, #AED581, #2E7D32);
|
||||
height: 10rpx;
|
||||
background: linear-gradient(90deg, #81C784, #2E7D32, #81C784);
|
||||
}
|
||||
|
||||
.archive-identity-card::after {
|
||||
content: '🌿';
|
||||
position: absolute;
|
||||
top: -20rpx;
|
||||
right: -10rpx;
|
||||
font-size: 120rpx;
|
||||
opacity: 0.08;
|
||||
transform: rotate(-15deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aic-header {
|
||||
@@ -420,7 +470,8 @@ page {
|
||||
.archive-timeline {
|
||||
position: relative;
|
||||
padding-left: 48rpx;
|
||||
border-left: 4rpx solid #E0E0E0;
|
||||
border-left: 4rpx solid transparent;
|
||||
border-image: linear-gradient(to bottom, #81C784, #C8E6C9, #E8F5E9) 1;
|
||||
margin-left: 24rpx;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
@@ -441,25 +492,43 @@ page {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: white;
|
||||
border: 6rpx solid #558B2F;
|
||||
border: 6rpx solid #81C784;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 0 8rpx #F4F6F0;
|
||||
}
|
||||
|
||||
.timeline-item:first-child .timeline-dot {
|
||||
background: #558B2F;
|
||||
border-color: #558B2F;
|
||||
animation: dotPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes dotPulse {
|
||||
0%, 100% { box-shadow: 0 0 0 8rpx #F4F6F0; }
|
||||
50% { box-shadow: 0 0 0 16rpx rgba(85, 139, 47, 0.15); }
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-size: 24rpx;
|
||||
color: #9E9E9E;
|
||||
font-size: 22rpx;
|
||||
color: #81C784;
|
||||
margin-bottom: 16rpx;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
background: rgba(129, 199, 132, 0.1);
|
||||
display: inline-block;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.timeline-content-box {
|
||||
background: white;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 28rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
|
||||
border: 2rpx solid rgba(0, 0, 0, 0.02);
|
||||
border-left: 6rpx solid #81C784;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline-content-box:active {
|
||||
@@ -467,7 +536,7 @@ page {
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-size: 32rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
@@ -476,6 +545,16 @@ page {
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.timeline-type-badge {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeline-desc {
|
||||
font-size: 28rpx;
|
||||
color: #546E7A;
|
||||
@@ -840,3 +919,33 @@ page {
|
||||
color: #EF5350;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
/* No-image Placeholder */
|
||||
.header-empty-placeholder {
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 50%, #A5D6A7 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.empty-plant-emoji {
|
||||
font-size: 120rpx;
|
||||
opacity: 0.6;
|
||||
animation: gentleBounce 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes gentleBounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-16rpx); }
|
||||
}
|
||||
|
||||
.empty-photo-hint {
|
||||
font-size: 26rpx;
|
||||
color: rgba(46, 125, 50, 0.5);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -25,45 +25,39 @@ Page({
|
||||
this.setData({ isLoading: true });
|
||||
wx.showLoading({ title: '加载中...' });
|
||||
try {
|
||||
// Fetch Config Tree
|
||||
const treeRes = await request.get('/config/badge/tree');
|
||||
// Parallel Fetch: Config Tree & User Badges
|
||||
const [treeRes, userBadgesRes] = await Promise.all([
|
||||
request.get('/config/badge/tree'),
|
||||
request.get('/profile/badge')
|
||||
]);
|
||||
|
||||
const list = Array.isArray(treeRes) ? treeRes : (treeRes.data || []);
|
||||
|
||||
// DEBUG: Force Unlock All
|
||||
// Extract user badge list from response structure { list: [...] }
|
||||
let userBadgeList = [];
|
||||
if (Array.isArray(userBadgesRes)) {
|
||||
userBadgeList = userBadgesRes;
|
||||
} else if (userBadgesRes && Array.isArray(userBadgesRes.list)) {
|
||||
userBadgeList = userBadgesRes.list;
|
||||
} else if (userBadgesRes && userBadgesRes.data) {
|
||||
userBadgeList = userBadgesRes.data || [];
|
||||
}
|
||||
|
||||
// Populate Achieved Map using Badge ID (not Record ID)
|
||||
let achievedMap = {};
|
||||
list.forEach(dim => {
|
||||
if (dim.groups) {
|
||||
dim.groups.forEach(grp => {
|
||||
if (grp.badges) {
|
||||
grp.badges.forEach(b => {
|
||||
achievedMap[b.id] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
userBadgeList.forEach(b => {
|
||||
const badgeId = b.badgeId || (b.badge ? b.badge.id : null);
|
||||
if (badgeId) {
|
||||
achievedMap[badgeId] = b;
|
||||
}
|
||||
});
|
||||
|
||||
// Original logic commented out for debug
|
||||
/*
|
||||
try {
|
||||
const profile = await request.get('/profile/detail');
|
||||
if (profile && profile.achievedBadges) {
|
||||
profile.achievedBadges.forEach(b => {
|
||||
const id = typeof b === 'string' ? b : b.id;
|
||||
achievedMap[id] = true;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail
|
||||
}
|
||||
*/
|
||||
|
||||
this.setData({
|
||||
dimensions: list,
|
||||
achievedMap
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Fetch badge tree failed', e);
|
||||
console.error('Fetch badge data failed', e);
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ isLoading: false });
|
||||
|
||||
@@ -26,7 +26,7 @@ Page({
|
||||
try {
|
||||
// Fetch levels
|
||||
const levelRes = await request.get('/config/level/list');
|
||||
console.log('Level Detail - API Response:', levelRes);
|
||||
|
||||
|
||||
let list = [];
|
||||
if (levelRes) {
|
||||
@@ -41,14 +41,14 @@ Page({
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Level Detail - Parsed List:', list);
|
||||
|
||||
list.sort((a, b) => a.minSunlight - b.minSunlight);
|
||||
|
||||
// Fetch profile if sunlight not passed
|
||||
let currentSunlight = passedSunlight;
|
||||
if (currentSunlight === undefined) {
|
||||
const profileRes = await request.get('/profile/detail');
|
||||
console.log('Level Detail - Profile:', profileRes);
|
||||
|
||||
currentSunlight = profileRes.totalSunlight || 0;
|
||||
this.setData({ currentSunlight });
|
||||
}
|
||||
@@ -63,7 +63,7 @@ Page({
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Level Detail - Calculated Level:', currentLevel);
|
||||
|
||||
|
||||
this.setData({
|
||||
levels: list,
|
||||
|
||||
@@ -1 +1,196 @@
|
||||
Page({});
|
||||
import request from '../../../utils/request';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
items: [],
|
||||
currentSunlight: 0,
|
||||
isLoading: true,
|
||||
hasMore: true,
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
activeType: 'PHYSICAL',
|
||||
// Redeem popup
|
||||
showRedeemPopup: false,
|
||||
selectedItem: null,
|
||||
redeemForm: {
|
||||
recipientName: '',
|
||||
phone: '',
|
||||
address: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.fetchProfile();
|
||||
this.fetchItems();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.fetchProfile();
|
||||
},
|
||||
|
||||
async fetchProfile() {
|
||||
try {
|
||||
const res = await request.get('/profile/detail');
|
||||
if (res) {
|
||||
this.setData({ currentSunlight: res.currentSunlight || 0 });
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent
|
||||
}
|
||||
},
|
||||
|
||||
async fetchItems(append = false) {
|
||||
if (!append) {
|
||||
this.setData({ isLoading: true, current: 1, items: [] });
|
||||
}
|
||||
try {
|
||||
const res = await request.get('/exchange/list', {
|
||||
current: this.data.current,
|
||||
pageSize: this.data.pageSize,
|
||||
type: this.data.activeType
|
||||
});
|
||||
const rawList = (res && res.list) ? res.list : [];
|
||||
const total = (res && res.total) ? res.total : 0;
|
||||
|
||||
const now = Date.now();
|
||||
const list = rawList.map(item => {
|
||||
const hasStart = !!item.startTime;
|
||||
const hasEnd = !!item.endTime;
|
||||
const startTs = hasStart ? new Date(item.startTime).getTime() : 0;
|
||||
const endTs = hasEnd ? new Date(item.endTime).getTime() : 0;
|
||||
|
||||
const notStarted = hasStart && now < startTs;
|
||||
const hasEnded = hasEnd && now > endTs;
|
||||
const isActive = !notStarted && !hasEnded;
|
||||
|
||||
let timeLabel = '';
|
||||
if (hasStart && hasEnd) {
|
||||
timeLabel = this.formatDate(item.startTime) + ' ~ ' + this.formatDate(item.endTime);
|
||||
} else if (hasStart) {
|
||||
timeLabel = this.formatDate(item.startTime) + ' 起';
|
||||
} else if (hasEnd) {
|
||||
timeLabel = '截止 ' + this.formatDate(item.endTime);
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
hasTimeLimit: hasStart || hasEnd,
|
||||
timeLabel,
|
||||
notStarted,
|
||||
hasEnded,
|
||||
isActive
|
||||
};
|
||||
});
|
||||
|
||||
this.setData({
|
||||
items: append ? [...this.data.items, ...list] : list,
|
||||
hasMore: this.data.items.length + list.length < total
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Fetch exchange items failed', e);
|
||||
} finally {
|
||||
this.setData({ isLoading: false });
|
||||
wx.stopPullDownRefresh();
|
||||
}
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (!this.data.hasMore || this.data.isLoading) return;
|
||||
this.setData({ current: this.data.current + 1 });
|
||||
this.fetchItems(true);
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchProfile();
|
||||
this.fetchItems();
|
||||
},
|
||||
|
||||
switchType(e) {
|
||||
const key = e.currentTarget.dataset.key;
|
||||
if (key === this.data.activeType) return;
|
||||
this.setData({ activeType: key });
|
||||
this.fetchItems();
|
||||
},
|
||||
|
||||
// Redeem Flow
|
||||
onItemTap(e) {
|
||||
const item = e.currentTarget.dataset.item;
|
||||
if (item.notStarted) {
|
||||
wx.showToast({ title: '活动尚未开始', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (item.hasEnded) {
|
||||
wx.showToast({ title: '活动已结束', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (item.stock === 0) {
|
||||
wx.showToast({ title: '已兑完', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({
|
||||
selectedItem: item,
|
||||
showRedeemPopup: true,
|
||||
redeemForm: { recipientName: '', phone: '', address: '' }
|
||||
});
|
||||
},
|
||||
|
||||
onPopupClose() {
|
||||
this.setData({ showRedeemPopup: false });
|
||||
},
|
||||
|
||||
onFormInput(e) {
|
||||
const field = e.currentTarget.dataset.field;
|
||||
this.setData({ [`redeemForm.${field}`]: e.detail.value });
|
||||
},
|
||||
|
||||
async confirmRedeem() {
|
||||
const item = this.data.selectedItem;
|
||||
if (!item) return;
|
||||
|
||||
// Check sunlight
|
||||
if (this.data.currentSunlight < item.costSunlight) {
|
||||
wx.showToast({ title: '阳光值不足', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Physical items require address
|
||||
if (item.type === 'PHYSICAL') {
|
||||
const { recipientName, phone, address } = this.data.redeemForm;
|
||||
if (!recipientName || !phone || !address) {
|
||||
wx.showToast({ title: '请填写完整收货信息', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '兑换中...', mask: true });
|
||||
try {
|
||||
await request.post('/exchange/redeem', {
|
||||
itemId: item.id,
|
||||
quantity: 1,
|
||||
...this.data.redeemForm
|
||||
});
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '兑换成功!', icon: 'success' });
|
||||
this.setData({ showRedeemPopup: false });
|
||||
// Refresh data
|
||||
this.fetchProfile();
|
||||
this.fetchItems();
|
||||
} catch (e) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: e.message || '兑换失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
goToOrders() {
|
||||
wx.navigateTo({ url: '/pages/profile/exchange/orders/index' });
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = d.getDate().toString().padStart(2, '0');
|
||||
return m + '.' + day;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"navigationBarTitleText": "兑换中心",
|
||||
"disableScroll": true,
|
||||
"usingComponents": {
|
||||
"t-empty": "tdesign-miniprogram/empty/empty"
|
||||
"t-empty": "tdesign-miniprogram/empty/empty",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-popup": "tdesign-miniprogram/popup/popup"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,141 @@
|
||||
<view class="exchange-page">
|
||||
<t-empty icon="info-circle-filled" description="兑换中心功能正在开发中,敬请期待" />
|
||||
|
||||
<!-- Balance Header (sticky) -->
|
||||
<view class="balance-header">
|
||||
<view class="balance-card">
|
||||
<view class="balance-left">
|
||||
<text class="balance-label">我的阳光值</text>
|
||||
<view class="balance-value-row">
|
||||
<text class="balance-emoji">☀️</text>
|
||||
<text class="balance-value">{{currentSunlight}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="balance-right" bindtap="goToOrders">
|
||||
<t-icon name="assignment" size="40rpx" color="#558B2F" />
|
||||
<text class="orders-text">兑换记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<scroll-view scroll-y class="page-scroll" show-scrollbar="{{false}}" enhanced="{{true}}" lower-threshold="200" bindscrolltolower="onReachBottom">
|
||||
|
||||
<!-- Items Grid -->
|
||||
<view class="items-grid" wx:if="{{items.length > 0}}">
|
||||
<view class="item-card {{!item.isActive ? 'inactive' : ''}}" wx:for="{{items}}" wx:key="id" bindtap="onItemTap" data-item="{{item}}">
|
||||
<view class="item-image-wrap">
|
||||
<image wx:if="{{item.image}}" src="{{item.image.url}}" mode="aspectFill" class="item-img" lazy-load />
|
||||
<view wx:else class="item-img-placeholder">
|
||||
<t-icon name="gift" size="64rpx" color="#C5E1A5" />
|
||||
</view>
|
||||
<!-- Stock Badge -->
|
||||
<view class="stock-badge" wx:if="{{item.isActive && item.stock >= 0 && item.stock <= 10 && item.stock > 0}}">
|
||||
<text>仅剩 {{item.stock}}</text>
|
||||
</view>
|
||||
<!-- Status overlays -->
|
||||
<view class="sold-out-mask" wx:if="{{item.stock === 0}}">
|
||||
<text>已兑完</text>
|
||||
</view>
|
||||
<view class="sold-out-mask not-started" wx:elif="{{item.notStarted}}">
|
||||
<text>未开始</text>
|
||||
</view>
|
||||
<view class="sold-out-mask ended" wx:elif="{{item.hasEnded}}">
|
||||
<text>已结束</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<text class="item-name">{{item.name}}</text>
|
||||
<view class="item-price-row">
|
||||
<text class="price-sun">☀️</text>
|
||||
<text class="price-val">{{item.costSunlight}}</text>
|
||||
</view>
|
||||
<view class="item-time" wx:if="{{item.hasTimeLimit}}">
|
||||
<t-icon name="time" size="24rpx" color="#9CA3AF" />
|
||||
<text>{{item.timeLabel}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Loading / Empty States -->
|
||||
<view class="state-footer" wx:if="{{isLoading}}">
|
||||
<t-loading theme="circular" size="40rpx" text="加载中..." inherit-color />
|
||||
</view>
|
||||
<view class="empty-state" wx:elif="{{!isLoading && items.length === 0}}">
|
||||
<text class="empty-icon">🎁</text>
|
||||
<text class="empty-title">暂无可兑换商品</text>
|
||||
<text class="empty-desc">新的好礼正在路上,敬请期待</text>
|
||||
</view>
|
||||
<view class="state-footer" wx:elif="{{!hasMore && items.length > 0}}">
|
||||
<text class="no-more">— 已经到底啦 —</text>
|
||||
</view>
|
||||
|
||||
<!-- Spacer -->
|
||||
<view style="height: 60rpx;"></view>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<!-- Redeem Popup -->
|
||||
<t-popup visible="{{showRedeemPopup}}" bind:visible-change="onPopupClose" placement="bottom">
|
||||
<view class="redeem-popup" wx:if="{{selectedItem}}">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">确认兑换</text>
|
||||
<view class="popup-close" bindtap="onPopupClose">
|
||||
<t-icon name="close" size="40rpx" color="#999" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Item Preview -->
|
||||
<view class="redeem-item-preview">
|
||||
<view class="preview-img-wrap">
|
||||
<image wx:if="{{selectedItem.image}}" src="{{selectedItem.image.url}}" mode="aspectFill" class="preview-img" />
|
||||
<view wx:else class="preview-img-placeholder">
|
||||
<t-icon name="gift" size="48rpx" color="#C5E1A5" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="preview-info">
|
||||
<text class="preview-name">{{selectedItem.name}}</text>
|
||||
<text class="preview-desc">{{selectedItem.description}}</text>
|
||||
<view class="preview-cost">
|
||||
<text class="cost-sun">☀️</text>
|
||||
<text class="cost-val">{{selectedItem.costSunlight}}</text>
|
||||
<text class="cost-unit">阳光值</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Address Form (Physical only) -->
|
||||
<view class="address-form" wx:if="{{selectedItem.type === 'PHYSICAL'}}">
|
||||
<view class="form-title">收货信息</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">姓名</text>
|
||||
<input placeholder="请输入收货人姓名" value="{{redeemForm.recipientName}}"
|
||||
bindinput="onFormInput" data-field="recipientName" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">电话</text>
|
||||
<input type="number" placeholder="请输入联系电话" value="{{redeemForm.phone}}"
|
||||
bindinput="onFormInput" data-field="phone" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">地址</text>
|
||||
<input placeholder="请输入详细收货地址" value="{{redeemForm.address}}"
|
||||
bindinput="onFormInput" data-field="address" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Balance Check -->
|
||||
<view class="balance-check {{currentSunlight >= selectedItem.costSunlight ? '' : 'insufficient'}}">
|
||||
<text>当前余额: ☀️ {{currentSunlight}}</text>
|
||||
<text wx:if="{{currentSunlight < selectedItem.costSunlight}}" class="insufficient-tip">余额不足</text>
|
||||
</view>
|
||||
|
||||
<!-- Confirm Button -->
|
||||
<view class="redeem-btn {{currentSunlight >= selectedItem.costSunlight && selectedItem.stock !== 0 ? '' : 'disabled'}}"
|
||||
bindtap="confirmRedeem">
|
||||
<text>立即兑换</text>
|
||||
</view>
|
||||
</view>
|
||||
</t-popup>
|
||||
</view>
|
||||
|
||||
@@ -1,9 +1,447 @@
|
||||
page {
|
||||
background: #F4F6F0;
|
||||
background: #F4F6F0;
|
||||
}
|
||||
|
||||
.exchange-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* ========== Balance Header ========== */
|
||||
.balance-header {
|
||||
background: linear-gradient(180deg, #E8F5E9 0%, #F4F6F0 100%);
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.balance-value-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.balance-emoji {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: #558B2F;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.balance-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #F1F8E9;
|
||||
}
|
||||
|
||||
.orders-text {
|
||||
font-size: 20rpx;
|
||||
color: #558B2F;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ========== Type Tabs ========== */
|
||||
.type-tabs {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
padding: 20rpx 32rpx;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.type-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10rpx 28rpx;
|
||||
background: #fff;
|
||||
border: 2rpx solid #E5E7EB;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
margin-right: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.type-tab.active {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
/* ========== Items Grid ========== */
|
||||
.items-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 32rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.item-card:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.item-image-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background: #F8FAF5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.item-img-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #F1F8E9, #E8F5E9);
|
||||
}
|
||||
|
||||
.stock-badge {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
background: rgba(239, 83, 80, 0.85);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 14rpx;
|
||||
}
|
||||
|
||||
.sold-out-mask {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.sold-out-mask.not-started {
|
||||
background: rgba(33, 150, 243, 0.5);
|
||||
}
|
||||
|
||||
.sold-out-mask.ended {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.item-card.inactive {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
padding: 16rpx 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1F2937;
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-price-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.price-sun {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.price-val {
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: #E65100;
|
||||
}
|
||||
|
||||
.item-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ========== Empty & Footer States ========== */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx 80rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 100rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 24rpx;
|
||||
color: #9CA3AF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.state-footer {
|
||||
padding: 40rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
font-size: 24rpx;
|
||||
color: #D1D5DB;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ========== Redeem Popup ========== */
|
||||
.redeem-popup {
|
||||
background: #fff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 36rpx;
|
||||
padding-bottom: calc(36rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
/* Item Preview in Popup */
|
||||
.redeem-item-preview {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
background: #F8FAF5;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.preview-img-wrap {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: #E8F5E9;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-img-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.preview-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.preview-desc {
|
||||
font-size: 24rpx;
|
||||
color: #9CA3AF;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.preview-cost {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.cost-sun { font-size: 24rpx; }
|
||||
.cost-val { font-size: 32rpx; font-weight: 800; color: #E65100; }
|
||||
.cost-unit { font-size: 22rpx; color: #9CA3AF; margin-left: 2rpx; }
|
||||
|
||||
/* Address Form */
|
||||
.address-form {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #F9FAFB;
|
||||
border: 2rpx solid #F3F4F6;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 26rpx;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
width: 60rpx;
|
||||
}
|
||||
|
||||
.form-item input {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
/* Balance Check */
|
||||
.balance-check {
|
||||
padding: 20rpx 24rpx;
|
||||
background: #F1F8E9;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #558B2F;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.balance-check.insufficient {
|
||||
background: #FFF3E0;
|
||||
color: #E65100;
|
||||
}
|
||||
|
||||
.insufficient-tip {
|
||||
color: #EF5350;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Redeem Button */
|
||||
.redeem-btn {
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
background: linear-gradient(135deg, #558B2F, #689F38);
|
||||
border-radius: 46rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.25);
|
||||
}
|
||||
|
||||
.redeem-btn:active {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.redeem-btn.disabled {
|
||||
background: #E5E7EB;
|
||||
color: #9CA3AF;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import request from '../../../../utils/request';
|
||||
|
||||
const STATUS_MAP = {
|
||||
1: { text: '待处理', theme: 'warning' },
|
||||
2: { text: '处理中', theme: 'primary' },
|
||||
3: { text: '已发货', theme: 'primary' },
|
||||
4: { text: '已完成', theme: 'success' },
|
||||
5: { text: '已取消', theme: 'default' }
|
||||
};
|
||||
|
||||
Page({
|
||||
data: {
|
||||
orders: [],
|
||||
isLoading: true,
|
||||
hasMore: true,
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
activeStatus: 0,
|
||||
statusTabs: [
|
||||
{ key: 0, label: '全部' },
|
||||
{ key: 1, label: '待处理' },
|
||||
{ key: 4, label: '已完成' },
|
||||
{ key: 5, label: '已取消' }
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.fetchOrders();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.fetchOrders();
|
||||
},
|
||||
|
||||
async fetchOrders(append = false) {
|
||||
if (!append) {
|
||||
this.setData({ isLoading: true, current: 1, orders: [] });
|
||||
}
|
||||
try {
|
||||
const params = {
|
||||
current: this.data.current,
|
||||
pageSize: this.data.pageSize
|
||||
};
|
||||
if (this.data.activeStatus) {
|
||||
params.status = this.data.activeStatus;
|
||||
}
|
||||
const res = await request.get('/exchange/orders', params);
|
||||
let list = (res && res.list) ? res.list : [];
|
||||
const total = (res && res.total) ? res.total : 0;
|
||||
|
||||
// Enrich with status display info
|
||||
list = list.map(order => ({
|
||||
...order,
|
||||
statusInfo: STATUS_MAP[order.status] || { text: '未知', theme: 'default' }
|
||||
}));
|
||||
|
||||
this.setData({
|
||||
orders: append ? [...this.data.orders, ...list] : list,
|
||||
hasMore: this.data.orders.length + list.length < total
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Fetch orders failed', e);
|
||||
} finally {
|
||||
this.setData({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
switchStatus(e) {
|
||||
const key = e.currentTarget.dataset.key;
|
||||
if (key === this.data.activeStatus) return;
|
||||
this.setData({ activeStatus: key });
|
||||
this.fetchOrders();
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (!this.data.hasMore || this.data.isLoading) return;
|
||||
this.setData({ current: this.data.current + 1 });
|
||||
this.fetchOrders(true);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"navigationBarTitleText": "兑换记录",
|
||||
"disableScroll": true,
|
||||
"usingComponents": {
|
||||
"t-empty": "tdesign-miniprogram/empty/empty",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<view class="orders-page">
|
||||
|
||||
<!-- Status Tabs -->
|
||||
<view class="status-tabs-wrap">
|
||||
<scroll-view class="status-tabs" scroll-x enable-flex show-scrollbar="{{false}}">
|
||||
<view wx:for="{{statusTabs}}" wx:key="key"
|
||||
class="status-tab {{activeStatus === item.key ? 'active' : ''}}"
|
||||
bindtap="switchStatus" data-key="{{item.key}}">
|
||||
<text>{{item.label}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="page-scroll" show-scrollbar="{{false}}" enhanced="{{true}}" bindscrolltolower="onReachBottom">
|
||||
|
||||
<view class="orders-list" wx:if="{{orders.length > 0}}">
|
||||
<view class="order-card" wx:for="{{orders}}" wx:key="id">
|
||||
<view class="order-header">
|
||||
<text class="order-time">{{item.createdAtStr}}</text>
|
||||
<t-tag size="small" variant="light" theme="{{item.statusInfo.theme}}">
|
||||
{{item.statusInfo.text}}
|
||||
</t-tag>
|
||||
</view>
|
||||
|
||||
<view class="order-body">
|
||||
<view class="order-img-wrap">
|
||||
<image wx:if="{{item.item && item.item.image}}" src="{{item.item.image.url}}" mode="aspectFill" class="order-img" />
|
||||
<view wx:else class="order-img-placeholder">
|
||||
<t-icon name="gift" size="48rpx" color="#C5E1A5" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="order-info">
|
||||
<text class="order-item-name">{{item.itemName}}</text>
|
||||
<text class="order-type-tag">{{item.itemType === 'PHYSICAL' ? '实物' : (item.itemType === 'VIRTUAL' ? '虚拟' : '优惠券')}}</text>
|
||||
<view class="order-cost-row">
|
||||
<text class="order-cost-sun">☀️</text>
|
||||
<text class="order-cost-val">-{{item.costSunlight}}</text>
|
||||
<text class="order-qty" wx:if="{{item.quantity > 1}}">x{{item.quantity}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Shipping Info (if physical and shipped) -->
|
||||
<view class="order-shipping" wx:if="{{item.trackingNo}}">
|
||||
<t-icon name="deliver" size="28rpx" color="#558B2F" />
|
||||
<text class="tracking-text">快递单号: {{item.trackingNo}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Loading / Empty -->
|
||||
<view class="state-footer" wx:if="{{isLoading}}">
|
||||
<t-loading theme="circular" size="40rpx" text="加载中..." inherit-color />
|
||||
</view>
|
||||
<view class="empty-state" wx:elif="{{!isLoading && orders.length === 0}}">
|
||||
<text class="empty-icon">📦</text>
|
||||
<text class="empty-title">暂无兑换记录</text>
|
||||
<text class="empty-desc">去兑换中心挑选心仪好礼吧</text>
|
||||
</view>
|
||||
<view class="state-footer" wx:elif="{{!hasMore && orders.length > 0}}">
|
||||
<text class="no-more">— 已经到底啦 —</text>
|
||||
</view>
|
||||
|
||||
<view style="height: 60rpx;"></view>
|
||||
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
page {
|
||||
background: #F4F6F0;
|
||||
}
|
||||
|
||||
.orders-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* ========== Status Tabs ========== */
|
||||
.status-tabs-wrap {
|
||||
flex-shrink: 0;
|
||||
background: #F4F6F0;
|
||||
padding: 20rpx 0 12rpx;
|
||||
}
|
||||
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
padding-left: 32rpx;
|
||||
height: 88rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.status-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 28rpx;
|
||||
height: 72rpx;
|
||||
background: #fff;
|
||||
border-radius: 36rpx;
|
||||
margin-right: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #546E7A;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04);
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
flex-shrink: 0;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.status-tab:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.status-tab.active {
|
||||
background: #558B2F;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8rpx 20rpx rgba(85, 139, 47, 0.3);
|
||||
border-color: #558B2F;
|
||||
}
|
||||
|
||||
/* ========== Orders List ========== */
|
||||
.orders-list {
|
||||
padding: 12rpx 32rpx;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.order-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid #F3F4F6;
|
||||
}
|
||||
|
||||
.order-time {
|
||||
font-size: 24rpx;
|
||||
color: #9CA3AF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-body {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.order-img-wrap {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: #F8FAF5;
|
||||
}
|
||||
|
||||
.order-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.order-img-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #F1F8E9, #E8F5E9);
|
||||
}
|
||||
|
||||
.order-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.order-item-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1F2937;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-type-tag {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
font-size: 20rpx;
|
||||
color: #6B7280;
|
||||
background: #F3F4F6;
|
||||
padding: 2rpx 14rpx;
|
||||
border-radius: 10rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.order-cost-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.order-cost-sun {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.order-cost-val {
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #E65100;
|
||||
}
|
||||
|
||||
.order-qty {
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
/* Shipping */
|
||||
.order-shipping {
|
||||
margin-top: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #F3F4F6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.tracking-text {
|
||||
font-size: 24rpx;
|
||||
color: #558B2F;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ========== Empty & Footer States ========== */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx 80rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 100rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 24rpx;
|
||||
color: #9CA3AF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.state-footer {
|
||||
padding: 40rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
font-size: 24rpx;
|
||||
color: #D1D5DB;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的收藏",
|
||||
"disableScroll": true,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-image": "tdesign-miniprogram/image/image",
|
||||
|
||||
@@ -130,7 +130,7 @@ Page({
|
||||
// Open WeChat notification settings
|
||||
wx.openSetting({
|
||||
success: (res) => {
|
||||
console.log('Settings opened', res);
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -29,10 +29,9 @@
|
||||
<t-icon name="setting" size="40rpx" color="#666" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- Stats Card (Fixed) -->
|
||||
|
||||
|
||||
<scroll-view scroll-y class="profile-content" enhanced show-scrollbar="{{false}}" scroll-top="{{scrollTop}}">
|
||||
<!-- Menu -->
|
||||
<view class="profile-menu">
|
||||
@@ -46,7 +45,6 @@
|
||||
<text class="menu-text">兑换中心</text>
|
||||
</view>
|
||||
<view class="menu-right-info">
|
||||
<text class="menu-badge-text">开发中</text>
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -70,7 +68,9 @@
|
||||
</view>
|
||||
<text class="menu-text">我的收藏</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
<view class="menu-right-info">
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" bindtap="goToPosts">
|
||||
@@ -80,7 +80,9 @@
|
||||
</view>
|
||||
<text class="menu-text">我的发布</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
<view class="menu-right-info">
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-item" bindtap="goToIdentifyHistory">
|
||||
@@ -90,7 +92,9 @@
|
||||
</view>
|
||||
<text class="menu-text">识别记录</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
<view class="menu-right-info">
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-group-title" style="margin-top: 32rpx;">更多服务</view>
|
||||
@@ -102,7 +106,9 @@
|
||||
</view>
|
||||
<text class="menu-text">帮助与关于</text>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
<view class="menu-right-info">
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
+71
-57
@@ -1,5 +1,6 @@
|
||||
// pages/tasks/index.js
|
||||
import request from '../../utils/request';
|
||||
import { requestSubscription } from '../../utils/subscribe';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -239,71 +240,84 @@ Page({
|
||||
const taskId = this.data.completingTask.id;
|
||||
const remark = this.data.remark || '';
|
||||
|
||||
wx.showLoading({ title: '提交中...', mask: true });
|
||||
|
||||
request.post('/plant/completeTask', {
|
||||
taskId: taskId,
|
||||
remark: remark
|
||||
}).then(res => {
|
||||
wx.hideLoading();
|
||||
|
||||
// Handle Rewards
|
||||
const queue = [];
|
||||
// Check if res has level up or badge data
|
||||
// Note: res is already data.data from request.js
|
||||
if (res && res.isLevelUp && res.currentLevel) {
|
||||
queue.push({ type: 'level', data: res.currentLevel });
|
||||
// Attempt to subscribe (silent mode avoids error popups if disabled)
|
||||
// This encourages "Always Allow" behavior for seamless experience
|
||||
requestSubscription(undefined, true).then((subResult) => {
|
||||
// Debug failure feedback
|
||||
if (!subResult.success) {
|
||||
if (subResult.isMainSwitchOff) {
|
||||
wx.showToast({ title: '提醒总开关已关闭', icon: 'none' });
|
||||
} else {
|
||||
console.log('[Task] Subscription quota not increased:', subResult.res || subResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Badge using IsGetBadge flag (allowing for casing variance)
|
||||
if (res && (res.IsGetBadge === true || res.isGetBadge === true) && res.newBadge) {
|
||||
queue.push({ type: 'badge', data: res.newBadge });
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true });
|
||||
|
||||
this._popupQueue = queue;
|
||||
request.post('/plant/completeTask', {
|
||||
taskId: taskId,
|
||||
remark: remark
|
||||
}).then(res => {
|
||||
wx.hideLoading();
|
||||
|
||||
// Optimistic UI Update Logic
|
||||
const groups = this.data.groupedTasks;
|
||||
let updated = false;
|
||||
// Need to deep clone possibly, but here we modify and set back
|
||||
for (let g of groups) {
|
||||
// g is an object. groupedTasks is array of objects.
|
||||
if (g.tasks) {
|
||||
const t = g.tasks.find(x => x && x.id === taskId);
|
||||
if (t) {
|
||||
t.isCompleted = true;
|
||||
t.isOverdue = false;
|
||||
updated = true;
|
||||
break;
|
||||
// Handle Rewards
|
||||
const queue = [];
|
||||
// Check if res has level up or badge data
|
||||
// Note: res is already data.data from request.js
|
||||
if (res && res.isLevelUp && res.currentLevel) {
|
||||
queue.push({ type: 'level', data: res.currentLevel });
|
||||
}
|
||||
|
||||
// Check for Badge using IsGetBadge flag (allowing for casing variance)
|
||||
if (res && (res.IsGetBadge === true || res.isGetBadge === true) && res.newBadge) {
|
||||
queue.push({ type: 'badge', data: res.newBadge });
|
||||
}
|
||||
|
||||
this._popupQueue = queue;
|
||||
|
||||
// Optimistic UI Update Logic
|
||||
const groups = this.data.groupedTasks;
|
||||
let updated = false;
|
||||
// Need to deep clone possibly, but here we modify and set back
|
||||
for (let g of groups) {
|
||||
// g is an object. groupedTasks is array of objects.
|
||||
if (g.tasks) {
|
||||
const t = g.tasks.find(x => x && x.id === taskId);
|
||||
if (t) {
|
||||
t.isCompleted = true;
|
||||
t.isOverdue = false;
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger Animation and Close Modal
|
||||
this.setData({
|
||||
completingTask: null,
|
||||
remark: '',
|
||||
groupedTasks: groups, // Update UI
|
||||
tasks: groups,
|
||||
showSunshine: true
|
||||
// Trigger Animation and Close Modal
|
||||
this.setData({
|
||||
completingTask: null,
|
||||
remark: '',
|
||||
groupedTasks: groups, // Update UI
|
||||
tasks: groups,
|
||||
showSunshine: true
|
||||
});
|
||||
|
||||
// Hide Animation after duration and Start showing modals
|
||||
setTimeout(() => {
|
||||
this.setData({ showSunshine: false });
|
||||
// Show rewards after sunshine animation
|
||||
if (this._popupQueue.length > 0) {
|
||||
this.processPopupQueue();
|
||||
}
|
||||
}, 1000); // 1.0s delay usually covers animation
|
||||
|
||||
// Sync with backend silently
|
||||
this.fetchTodayTasks();
|
||||
|
||||
}).catch(err => {
|
||||
wx.hideLoading();
|
||||
console.error('Complete task failed', err);
|
||||
wx.showToast({ title: '操作失败', icon: 'none' });
|
||||
});
|
||||
|
||||
// Hide Animation after duration and Start showing modals
|
||||
setTimeout(() => {
|
||||
this.setData({ showSunshine: false });
|
||||
// Show rewards after sunshine animation
|
||||
if (this._popupQueue.length > 0) {
|
||||
this.processPopupQueue();
|
||||
}
|
||||
}, 1000); // 1.0s delay usually covers animation
|
||||
|
||||
// Sync with backend silently
|
||||
this.fetchTodayTasks();
|
||||
|
||||
}).catch(err => {
|
||||
wx.hideLoading();
|
||||
console.error('Complete task failed', err);
|
||||
wx.showToast({ title: '操作失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -459,6 +459,13 @@
|
||||
.mini-check-btn.btn-checked {
|
||||
background: #4CAF50 !important;
|
||||
border-color: #4CAF50 !important;
|
||||
animation: checkPop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
@keyframes checkPop {
|
||||
0% { transform: scale(0.6); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Urgent/Overdue State */
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// pages/wiki/chat/history/index.js
|
||||
import request from '../../../../utils/request';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
list: [],
|
||||
total: 0,
|
||||
current: 1,
|
||||
pageSize: 15,
|
||||
loading: false,
|
||||
hasMore: true,
|
||||
showClearDialog: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.fetchHistory(true);
|
||||
},
|
||||
|
||||
fetchHistory(reset = false) {
|
||||
if (this.data.loading) return;
|
||||
if (!reset && !this.data.hasMore) return;
|
||||
|
||||
const current = reset ? 1 : this.data.current;
|
||||
this.setData({ loading: true });
|
||||
|
||||
request.get('/plant/chat/history', { current, pageSize: this.data.pageSize })
|
||||
.then(res => {
|
||||
const items = (res.list || []).map(item => ({
|
||||
...item,
|
||||
answerPreview: (item.answer || '').substring(0, 80) + ((item.answer || '').length > 80 ? '...' : ''),
|
||||
}));
|
||||
const total = res.total || 0;
|
||||
|
||||
if (reset) {
|
||||
this.setData({
|
||||
list: items,
|
||||
total,
|
||||
current: 2,
|
||||
hasMore: items.length < total,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
const old = this.data.list;
|
||||
const update = {};
|
||||
items.forEach((item, i) => {
|
||||
update[`list[${old.length + i}]`] = item;
|
||||
});
|
||||
update.current = current + 1;
|
||||
update.hasMore = (old.length + items.length) < total;
|
||||
update.loading = false;
|
||||
update.total = total;
|
||||
this.setData(update);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.setData({ loading: false });
|
||||
});
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
this.fetchHistory(false);
|
||||
},
|
||||
|
||||
onTapItem(e) {
|
||||
const item = e.currentTarget.dataset.item;
|
||||
// Navigate to chat page with prefilled Q&A
|
||||
wx.navigateTo({
|
||||
url: '/pages/wiki/chat/index?fromHistory=1',
|
||||
success(res) {
|
||||
res.eventChannel.emit('historyData', {
|
||||
question: item.question,
|
||||
answer: item.answer,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onDeleteItem(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
wx.showModal({
|
||||
title: '删除记录',
|
||||
content: '确定删除这条问答记录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
request.post('/plant/chat/history/delete', { id }).then(() => {
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
this.fetchHistory(true);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onClearAll() {
|
||||
this.setData({ showClearDialog: true });
|
||||
},
|
||||
|
||||
closeClearDialog() {
|
||||
this.setData({ showClearDialog: false });
|
||||
},
|
||||
|
||||
doClearAll() {
|
||||
this.setData({ showClearDialog: false });
|
||||
request.post('/plant/chat/history/clear').then(() => {
|
||||
wx.showToast({ title: '已清空', icon: 'success' });
|
||||
this.setData({ list: [], total: 0, hasMore: false });
|
||||
});
|
||||
},
|
||||
|
||||
goToChat() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"navigationBarTitleText": "问答历史",
|
||||
"navigationBarBackgroundColor": "#558B2F",
|
||||
"navigationBarTextStyle": "white",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty",
|
||||
"t-dialog": "tdesign-miniprogram/dialog/dialog",
|
||||
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<!--pages/wiki/chat/history/index.wxml-->
|
||||
<view class="history-page">
|
||||
<scroll-view
|
||||
class="history-scroll"
|
||||
scroll-y
|
||||
bindscrolltolower="loadMore"
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
>
|
||||
<!-- Header Actions -->
|
||||
<view class="header-bar" wx:if="{{list.length > 0}}">
|
||||
<text class="header-count">共 {{total}} 条记录</text>
|
||||
<view class="clear-btn" bindtap="onClearAll">
|
||||
<t-icon name="delete" size="32rpx" color="#EF4444" />
|
||||
<text>清空全部</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- History List -->
|
||||
<view wx:for="{{list}}" wx:key="id" class="history-card" bindtap="onTapItem" data-item="{{item}}">
|
||||
<view class="card-header">
|
||||
<text class="card-time">{{item.createdAtStr}}</text>
|
||||
<view class="card-del" catchtap="onDeleteItem" data-id="{{item.id}}">
|
||||
<t-icon name="close" size="28rpx" color="#9CA3AF" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-question">
|
||||
<text class="q-label">Q</text>
|
||||
<text class="q-text">{{item.question}}</text>
|
||||
</view>
|
||||
<view class="card-answer">
|
||||
<text class="a-label">A</text>
|
||||
<text class="a-text">{{item.answerPreview}}</text>
|
||||
<view class="card-arrow">
|
||||
<t-icon name="chevron-right" size="28rpx" color="#CCC" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- States -->
|
||||
<view class="footer">
|
||||
<t-loading wx:if="{{loading}}" theme="circular" size="40rpx" text="加载中..." />
|
||||
<text wx:elif="{{!hasMore && list.length > 0}}" class="no-more">没有更多了</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!loading && list.length === 0}}" class="empty-wrap">
|
||||
<view class="empty-icon">📝</view>
|
||||
<text class="empty-text">暂无问答记录</text>
|
||||
<text class="empty-sub">去和AI助手聊聊吧</text>
|
||||
<view class="empty-cta" bindtap="goToChat">
|
||||
<text>开始提问</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 60rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
<t-dialog
|
||||
visible="{{showClearDialog}}"
|
||||
title="清空全部历史"
|
||||
content="确定要清空所有问答记录吗?此操作不可恢复。"
|
||||
confirm-btn="确定清空"
|
||||
cancel-btn="取消"
|
||||
bind:confirm="doClearAll"
|
||||
bind:cancel="closeClearDialog"
|
||||
/>
|
||||
</view>
|
||||
@@ -0,0 +1,195 @@
|
||||
/** pages/wiki/chat/history/index.wxss **/
|
||||
.history-page {
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #EEF3E5 0%, #F4F6F0 100%);
|
||||
}
|
||||
|
||||
.history-scroll {
|
||||
height: 100%;
|
||||
padding: 24rpx 28rpx;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.header-count {
|
||||
font-size: 26rpx;
|
||||
color: #78909C;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
font-size: 26rpx;
|
||||
color: #EF4444;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.clear-btn:active {
|
||||
background: rgba(239,68,68,0.08);
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.history-card {
|
||||
background: rgba(255,255,255,0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 24rpx 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(85,139,47,0.05);
|
||||
border: 1rpx solid rgba(85,139,47,0.04);
|
||||
transition: all 0.15s;
|
||||
animation: cardIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes cardIn {
|
||||
from { opacity: 0; transform: translateY(12rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.history-card:active {
|
||||
transform: scale(0.98);
|
||||
background: #FAFDF7;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.card-time {
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.card-del {
|
||||
padding: 8rpx;
|
||||
margin: -8rpx;
|
||||
}
|
||||
|
||||
.card-question {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.q-label {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(135deg, #558B2F, #7CB342);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
line-height: 40rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.q-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1F2937;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-answer {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.a-label {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 10rpx;
|
||||
background: #E8F5E9;
|
||||
color: #2E7D32;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
line-height: 40rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.a-text {
|
||||
font-size: 26rpx;
|
||||
color: #6B7280;
|
||||
line-height: 1.6;
|
||||
flex: 1;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
font-size: 24rpx;
|
||||
color: #CCC;
|
||||
}
|
||||
|
||||
/* Empty */
|
||||
.empty-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #9CA3AF;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.empty-sub {
|
||||
font-size: 26rpx;
|
||||
color: #CCC;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.empty-cta {
|
||||
padding: 16rpx 48rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, #558B2F, #7CB342);
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 6rpx 20rpx rgba(85,139,47,0.3);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.empty-cta:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// pages/wiki/chat/index.js
|
||||
import request from '../../../utils/request';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
messages: [],
|
||||
inputValue: '',
|
||||
isTyping: false,
|
||||
scrollAnchor: '',
|
||||
_counter: 0,
|
||||
quotaRemaining: -1,
|
||||
quotaLimit: 0,
|
||||
_pendingPrefill: '',
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
if (options && options.fromHistory === '1') {
|
||||
const channel = this.getOpenerEventChannel();
|
||||
channel.on('historyData', (data) => {
|
||||
const msgs = [
|
||||
{ id: 'h1', role: 'user', content: data.question },
|
||||
{ id: 'h2', role: 'ai', content: this._cleanMd(data.answer) },
|
||||
];
|
||||
this.setData({ messages: msgs, _counter: 2 }, () => this.scrollToBottom());
|
||||
});
|
||||
} else if (options && options.prefillQuestion) {
|
||||
// Only save the question; actual send waits for quota check in onShow
|
||||
this._pendingPrefill = decodeURIComponent(options.prefillQuestion);
|
||||
this.setData({ inputValue: this._pendingPrefill });
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._fetchQuota();
|
||||
},
|
||||
|
||||
_fetchQuota() {
|
||||
request.get('/plant/chat/quota').then(res => {
|
||||
this.setData({
|
||||
quotaRemaining: res.remaining,
|
||||
quotaLimit: res.limit,
|
||||
});
|
||||
// Auto-send prefill question only after quota is confirmed
|
||||
if (this._pendingPrefill) {
|
||||
const q = this._pendingPrefill;
|
||||
this._pendingPrefill = '';
|
||||
if (res.remaining > 0) {
|
||||
this.onSend();
|
||||
} else {
|
||||
wx.showToast({ title: '今日问答次数已用完,明天再来吧', icon: 'none', duration: 2500 });
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
goToHistory() {
|
||||
wx.navigateTo({ url: '/pages/wiki/chat/history/index' });
|
||||
},
|
||||
|
||||
onQuickAsk(e) {
|
||||
const query = e.currentTarget.dataset.q;
|
||||
this.setData({ inputValue: query }, () => this.onSend());
|
||||
},
|
||||
|
||||
onInput(e) {
|
||||
this.setData({ inputValue: e.detail.value });
|
||||
},
|
||||
|
||||
onSend() {
|
||||
const query = this.data.inputValue.trim();
|
||||
if (!query || this.data.isTyping) return;
|
||||
|
||||
// 额度检查
|
||||
if (this.data.quotaRemaining === 0) {
|
||||
wx.showToast({ title: '今日问答次数已用完,明天再来吧', icon: 'none', duration: 2500 });
|
||||
return;
|
||||
}
|
||||
|
||||
const uid = 'u' + (++this.data._counter);
|
||||
const aid = 'a' + (++this.data._counter);
|
||||
const len = this.data.messages.length;
|
||||
|
||||
this.setData({
|
||||
[`messages[${len}]`]: { id: uid, role: 'user', content: query },
|
||||
[`messages[${len + 1}]`]: { id: aid, role: 'ai', content: '' },
|
||||
inputValue: '',
|
||||
isTyping: true,
|
||||
}, () => {
|
||||
this.scrollToBottom();
|
||||
this._streamRequest(query, aid);
|
||||
});
|
||||
},
|
||||
|
||||
_streamRequest(query, aiMsgId) {
|
||||
let fullText = '';
|
||||
|
||||
request.stream('/plant/chat/stream', { query }, {
|
||||
onChunk: (res) => {
|
||||
const text = this._decode(res.data);
|
||||
|
||||
// Detect non-SSE JSON error (e.g. quota exceeded returns {code:7, msg:"..."})
|
||||
if (!text.startsWith('data: ')) {
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (json.code && json.code !== 200 && json.msg) {
|
||||
this._updateAiMsg(aiMsgId, '⚠️ ' + json.msg);
|
||||
this.setData({ isTyping: false });
|
||||
this._fetchQuota();
|
||||
return;
|
||||
}
|
||||
} catch (_) { /* not JSON, continue SSE parsing */ }
|
||||
}
|
||||
|
||||
const lines = text.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
const chunk = line.substring(6);
|
||||
|
||||
if (chunk === '[DONE]') {
|
||||
this.setData({ isTyping: false });
|
||||
return;
|
||||
}
|
||||
if (chunk.startsWith('[ERROR]')) {
|
||||
fullText += '\n⚠️ ' + (chunk.substring(7) || '服务异常');
|
||||
this._updateAiMsg(aiMsgId, fullText);
|
||||
this.setData({ isTyping: false });
|
||||
return;
|
||||
}
|
||||
|
||||
fullText += chunk;
|
||||
this._updateAiMsg(aiMsgId, fullText);
|
||||
}
|
||||
this.scrollToBottom();
|
||||
},
|
||||
onDone: () => {
|
||||
this.setData({ isTyping: false });
|
||||
this.scrollToBottom();
|
||||
// Delay: backend saves history async (go func) after stream ends
|
||||
setTimeout(() => this._fetchQuota(), 800);
|
||||
},
|
||||
onError: () => {
|
||||
this._updateAiMsg(aiMsgId, '网络连接失败,请稍后重试');
|
||||
this.setData({ isTyping: false });
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_updateAiMsg(id, content) {
|
||||
const idx = this.data.messages.findIndex(m => m.id === id);
|
||||
if (idx !== -1) {
|
||||
this.setData({ [`messages[${idx}].content`]: this._cleanMd(content) });
|
||||
}
|
||||
},
|
||||
|
||||
// Strip residual markdown symbols for clean display
|
||||
_cleanMd(text) {
|
||||
return text
|
||||
.replace(/^#{1,6}\s*/gm, '') // ### headers
|
||||
.replace(/\*\*(.+?)\*\*/g, '【$1】') // **bold** → 【bold】
|
||||
.replace(/\*(.+?)\*/g, '$1') // *italic*
|
||||
.replace(/^[\-\*]\s+/gm, '· ') // - list → · list
|
||||
.replace(/^\d+\.\s+/gm, (m) => m) // keep numbered lists
|
||||
.replace(/`([^`]+)`/g, '$1') // `code`
|
||||
.replace(/^---+$/gm, '————') // --- → ————
|
||||
.replace(/\n{3,}/g, '\n\n'); // collapse blank lines
|
||||
},
|
||||
|
||||
_decode(buffer) {
|
||||
try {
|
||||
return new TextDecoder('utf-8').decode(new Uint8Array(buffer));
|
||||
} catch (e) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let s = '';
|
||||
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
||||
try { return decodeURIComponent(escape(s)); } catch (_) { return s; }
|
||||
}
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
this.setData({ scrollAnchor: 'scroll-bottom' });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"navigationBarTitleText": "植物AI助手",
|
||||
"navigationBarBackgroundColor": "#558B2F",
|
||||
"navigationBarTextStyle": "white",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<!--pages/wiki/chat/index.wxml-->
|
||||
<view class="chat-page">
|
||||
<!-- Messages Area -->
|
||||
<scroll-view
|
||||
class="chat-messages"
|
||||
scroll-y
|
||||
scroll-into-view="{{scrollAnchor}}"
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
scroll-with-animation
|
||||
>
|
||||
<!-- Floating History Button (always visible) -->
|
||||
<view class="chat-history-float" bindtap="goToHistory" wx:if="{{messages.length > 0}}">
|
||||
<t-icon name="time" size="28rpx" color="#558B2F" />
|
||||
<text>历史记录</text>
|
||||
</view>
|
||||
<!-- Welcome -->
|
||||
<view class="welcome" wx:if="{{messages.length === 0}}">
|
||||
<view class="welcome-glow"></view>
|
||||
<view class="welcome-icon anim-float">🌿</view>
|
||||
<view class="welcome-title">植物AI百科</view>
|
||||
<view class="welcome-sub">基于知识库的智能问答助手</view>
|
||||
<view class="history-entry" bindtap="goToHistory">
|
||||
<t-icon name="time" size="32rpx" color="#558B2F" />
|
||||
<text>查看问答历史</text>
|
||||
</view>
|
||||
<view class="quick-grid">
|
||||
<view class="quick-card" bindtap="onQuickAsk" data-q="龟背竹怎么养护?">
|
||||
<text class="qc-emoji">🌱</text>
|
||||
<text class="qc-text">龟背竹怎么养护?</text>
|
||||
</view>
|
||||
<view class="quick-card" bindtap="onQuickAsk" data-q="哪些植物适合室内?">
|
||||
<text class="qc-emoji">🏠</text>
|
||||
<text class="qc-text">哪些植物适合室内?</text>
|
||||
</view>
|
||||
<view class="quick-card" bindtap="onQuickAsk" data-q="多肉浇水注意什么?">
|
||||
<text class="qc-emoji">💧</text>
|
||||
<text class="qc-text">多肉浇水注意什么?</text>
|
||||
</view>
|
||||
<view class="quick-card" bindtap="onQuickAsk" data-q="植物叶子发黄怎么办?">
|
||||
<text class="qc-emoji">🍂</text>
|
||||
<text class="qc-text">叶子发黄怎么办?</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Chat Bubbles -->
|
||||
<block wx:for="{{messages}}" wx:key="id">
|
||||
<view id="msg-{{item.id}}" class="msg-row {{item.role}}">
|
||||
<!-- AI avatar -->
|
||||
<view wx:if="{{item.role === 'ai'}}" class="ai-avatar-wrap">
|
||||
<text class="ai-avatar-emoji">🌱</text>
|
||||
</view>
|
||||
|
||||
<view class="msg-bubble {{item.role}}">
|
||||
<!-- AI: typing state -->
|
||||
<view wx:if="{{item.role === 'ai' && !item.content}}" class="typing-wrap">
|
||||
<view class="typing-dots">
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
</view>
|
||||
<text class="typing-label">思考中...</text>
|
||||
</view>
|
||||
<text wx:elif="{{item.role === 'ai'}}" class="ai-text" user-select>{{item.content}}</text>
|
||||
<!-- User text -->
|
||||
<text wx:else class="msg-text" user-select>{{item.content}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view style="height: 32rpx;" id="scroll-bottom"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- Input -->
|
||||
<view class="input-area">
|
||||
<view class="input-row">
|
||||
<input
|
||||
class="chat-input"
|
||||
placeholder="{{isTyping ? 'AI正在回答中...' : '问我任何植物相关的问题...'}}"
|
||||
value="{{inputValue}}"
|
||||
bindinput="onInput"
|
||||
bindconfirm="onSend"
|
||||
confirm-type="send"
|
||||
disabled="{{isTyping}}"
|
||||
adjust-position="{{true}}"
|
||||
cursor-spacing="20"
|
||||
/>
|
||||
<view class="send-btn {{inputValue && !isTyping ? 'active' : ''}}" bindtap="onSend">
|
||||
<t-icon name="arrow-up" size="36rpx" color="#fff" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="quota-bar" wx:if="{{quotaLimit > 0}}">
|
||||
<text class="quota-text {{quotaRemaining <= 2 ? 'warn' : ''}}">今日剩余 {{quotaRemaining}}/{{quotaLimit}} 次</text>
|
||||
</view>
|
||||
<view class="safe-bottom"></view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,299 @@
|
||||
/** pages/wiki/chat/index.wxss **/
|
||||
.chat-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, #EEF3E5 0%, #F4F6F0 35%, #F4F6F0 100%);
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 20rpx 24rpx;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.chat-history-float {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #558B2F;
|
||||
width: fit-content;
|
||||
margin: 0 auto 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* ── Welcome ── */
|
||||
.welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx 20rpx 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.welcome-glow {
|
||||
position: absolute;
|
||||
top: -60rpx;
|
||||
width: 480rpx;
|
||||
height: 480rpx;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(85,139,47,0.15) 0%, rgba(85,139,47,0.04) 50%, transparent 75%);
|
||||
pointer-events: none;
|
||||
animation: glowPulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes glowPulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.8; }
|
||||
50% { transform: scale(1.08); opacity: 1; }
|
||||
}
|
||||
|
||||
.welcome-icon { font-size: 88rpx; margin-bottom: 12rpx; position: relative; }
|
||||
|
||||
.anim-float { animation: floatUp 3s ease-in-out infinite; }
|
||||
|
||||
@keyframes floatUp {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12rpx); }
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 42rpx;
|
||||
font-weight: 800;
|
||||
color: #2E7D32;
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.welcome-sub {
|
||||
font-size: 24rpx;
|
||||
color: #90A4AE;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.history-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255,255,255,0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 2rpx 12rpx rgba(85,139,47,0.08);
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #558B2F;
|
||||
margin-bottom: 32rpx;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.history-entry:active {
|
||||
transform: scale(0.96);
|
||||
background: #F0F7EB;
|
||||
}
|
||||
|
||||
/* Quick Ask Grid */
|
||||
.quick-grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
background: rgba(255,255,255,0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
box-shadow: 0 2rpx 16rpx rgba(85,139,47,0.06);
|
||||
border: 1rpx solid rgba(85,139,47,0.06);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.quick-card:active {
|
||||
transform: scale(0.96);
|
||||
background: #F0F7EB;
|
||||
border-color: rgba(85,139,47,0.15);
|
||||
}
|
||||
|
||||
.qc-emoji { font-size: 40rpx; }
|
||||
|
||||
.qc-text {
|
||||
font-size: 25rpx;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ── Message Row ── */
|
||||
.msg-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24rpx;
|
||||
gap: 12rpx;
|
||||
animation: fadeSlideIn 0.25s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeSlideIn {
|
||||
from { opacity: 0; transform: translateY(16rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.msg-row.user { flex-direction: row-reverse; }
|
||||
|
||||
.ai-avatar-wrap {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #E8F5E9, #C8E6C9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4rpx 12rpx rgba(85,139,47,0.12);
|
||||
}
|
||||
|
||||
.ai-avatar-emoji { font-size: 32rpx; }
|
||||
|
||||
/* ── Bubbles ── */
|
||||
.msg-bubble {
|
||||
max-width: 78%;
|
||||
padding: 22rpx 26rpx;
|
||||
border-radius: 24rpx;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.msg-bubble.ai {
|
||||
max-width: 88%;
|
||||
background: rgba(255,255,255,0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
border-top-left-radius: 6rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.msg-bubble.user {
|
||||
background: linear-gradient(135deg, #558B2F, #7CB342);
|
||||
border-top-right-radius: 6rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(85,139,47,0.2);
|
||||
}
|
||||
|
||||
.msg-text {
|
||||
font-size: 29rpx;
|
||||
line-height: 1.7;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ai-text {
|
||||
font-size: 29rpx;
|
||||
line-height: 1.85;
|
||||
color: #1F2937;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* ── Typing ── */
|
||||
.typing-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.typing-dots {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #A5D6A7;
|
||||
animation: bounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) { animation-delay: 0.16s; }
|
||||
.dot:nth-child(3) { animation-delay: 0.32s; }
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: scale(0.5); opacity: 0.4; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.typing-label {
|
||||
font-size: 22rpx;
|
||||
color: #90A4AE;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Input ── */
|
||||
.input-area {
|
||||
background: rgba(255,255,255,0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
border-top: 1rpx solid rgba(0,0,0,0.03);
|
||||
padding: 14rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
height: 78rpx;
|
||||
background: #F0F4E8;
|
||||
border-radius: 40rpx;
|
||||
padding: 0 28rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 74rpx;
|
||||
height: 74rpx;
|
||||
border-radius: 50%;
|
||||
background: #D1D5DB;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.send-btn.active {
|
||||
background: linear-gradient(135deg, #558B2F, #7CB342);
|
||||
box-shadow: 0 4rpx 20rpx rgba(85,139,47,0.35);
|
||||
}
|
||||
|
||||
.send-btn:active { transform: scale(0.85); }
|
||||
|
||||
.safe-bottom {
|
||||
height: calc(14rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* Quota Bar */
|
||||
.quota-bar {
|
||||
text-align: center;
|
||||
padding: 6rpx 0 2rpx;
|
||||
}
|
||||
|
||||
.quota-text {
|
||||
font-size: 22rpx;
|
||||
color: #90A4AE;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quota-text.warn {
|
||||
color: #EF4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -142,5 +142,12 @@ Page({
|
||||
title: `植物百科 - ${this.data.plant.name}`,
|
||||
path: `/pages/wiki/detail/index?id=${this.data.plant.id}`
|
||||
};
|
||||
},
|
||||
|
||||
askAiAboutPlant() {
|
||||
const name = this.data.plant ? this.data.plant.name : '';
|
||||
wx.navigateTo({
|
||||
url: `/pages/wiki/chat/index?prefillQuestion=${encodeURIComponent(name + '怎么养护?')}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -195,9 +195,14 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 40rpx;"></view>
|
||||
<view style="height: 120rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- Ask AI FAB -->
|
||||
<view class="ask-ai-fab" bindtap="askAiAboutPlant">
|
||||
<text class="fab-emoji">🤖</text>
|
||||
<text>问 AI</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
@@ -286,3 +286,27 @@ page {
|
||||
transform: scale(0.9);
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
/* Ask AI FAB */
|
||||
.ask-ai-fab {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
bottom: 48rpx;
|
||||
background: linear-gradient(135deg, rgba(21,101,192,0.92), rgba(25,118,210,0.92));
|
||||
backdrop-filter: blur(12px);
|
||||
color: #fff;
|
||||
padding: 20rpx 32rpx;
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8rpx 28rpx rgba(21,101,192,0.3);
|
||||
z-index: 100;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.ask-ai-fab:active { transform: scale(0.92); }
|
||||
|
||||
.fab-emoji { font-size: 32rpx; line-height: 1; }
|
||||
|
||||
@@ -10,30 +10,32 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
padding: 48rpx;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 20%; /* Push up visually */
|
||||
}
|
||||
|
||||
.state-card {
|
||||
background: #fff;
|
||||
border-radius: 40rpx;
|
||||
padding: 56rpx 48rpx;
|
||||
border-radius: 48rpx;
|
||||
padding: 64rpx 48rpx;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(85, 139, 47, 0.08);
|
||||
gap: 40rpx;
|
||||
box-shadow: 0 20rpx 60rpx rgba(85, 139, 47, 0.15);
|
||||
}
|
||||
|
||||
/* ========== Loading State ========== */
|
||||
.loading-image-wrap {
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
border-radius: 32rpx;
|
||||
width: 440rpx;
|
||||
height: 440rpx;
|
||||
border-radius: 40rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.1);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.loading-preview {
|
||||
|
||||
+16
-4
@@ -21,7 +21,8 @@ Page({
|
||||
scrollTop: 0,
|
||||
|
||||
// Modal State
|
||||
showIdentifyModal: false
|
||||
showIdentifyModal: false,
|
||||
isRefreshing: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -40,6 +41,13 @@ Page({
|
||||
this.setData({ scrollTop: Math.random() * 0.01 });
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.setData({ isRefreshing: true });
|
||||
this.fetchWikiList(true).finally(() => {
|
||||
this.setData({ isRefreshing: false });
|
||||
});
|
||||
},
|
||||
|
||||
// Fetch categories from API
|
||||
fetchCategories() {
|
||||
request.get('/wiki-class/list').then(res => {
|
||||
@@ -52,8 +60,8 @@ Page({
|
||||
|
||||
// Fetch wiki list from API
|
||||
fetchWikiList(reset = false) {
|
||||
if (this.data.isLoading) return;
|
||||
if (!reset && !this.data.hasMore) return;
|
||||
if (this.data.isLoading) return Promise.resolve();
|
||||
if (!reset && !this.data.hasMore) return Promise.resolve();
|
||||
|
||||
const current = reset ? 1 : this.data.current;
|
||||
|
||||
@@ -75,7 +83,7 @@ Page({
|
||||
params.classId = [this.data.activeCategory];
|
||||
}
|
||||
|
||||
request.post('/wiki/page', params).then(res => {
|
||||
return request.post('/wiki/page', params).then(res => {
|
||||
const data = res || {};
|
||||
const list = data.list || [];
|
||||
const total = data.total || 0;
|
||||
@@ -207,6 +215,10 @@ Page({
|
||||
|
||||
openIdentifyModal() { this.setData({ showIdentifyModal: true }); },
|
||||
|
||||
goToAiChat() {
|
||||
wx.navigateTo({ url: '/pages/wiki/chat/index' });
|
||||
},
|
||||
|
||||
onPopupVisibleChange(e) {
|
||||
this.setData({
|
||||
showIdentifyModal: e.detail.visible
|
||||
|
||||
+16
-5
@@ -17,6 +17,9 @@
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
scroll-top="{{scrollTop}}"
|
||||
refresher-enabled="{{true}}"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
refresher-triggered="{{isRefreshing}}"
|
||||
>
|
||||
<view class="search-section">
|
||||
<view class="search-box-card">
|
||||
@@ -111,13 +114,9 @@
|
||||
</view>
|
||||
|
||||
<!-- Spacer -->
|
||||
<view style="height: 160rpx;"></view>
|
||||
<view style="height: 200rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="floating-add-btn" bindtap="openIdentifyModal">
|
||||
<t-icon name="scan" size="40rpx" color="#FFF" />
|
||||
<text>植物识别</text>
|
||||
</view>
|
||||
|
||||
<!-- Identify Popup -->
|
||||
<t-popup visible="{{showIdentifyModal}}" bind:visible-change="onPopupVisibleChange" placement="bottom">
|
||||
@@ -147,4 +146,16 @@
|
||||
</view>
|
||||
</view>
|
||||
</t-popup>
|
||||
|
||||
<!-- Floating Buttons (must be after popup in DOM to stay on top) -->
|
||||
<view class="floating-btns" wx:if="{{!showIdentifyModal}}">
|
||||
<view class="floating-btn chat-btn" bindtap="goToAiChat">
|
||||
<text class="btn-emoji">🤖</text>
|
||||
<text>AI问答</text>
|
||||
</view>
|
||||
<view class="floating-btn scan-btn" bindtap="openIdentifyModal">
|
||||
<t-icon name="scan" size="40rpx" color="#FFF" />
|
||||
<text>植物识别</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
+44
-22
@@ -60,12 +60,12 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 36rpx;
|
||||
padding: 0 28rpx;
|
||||
height: 72rpx;
|
||||
background: #fff;
|
||||
border-radius: 36rpx;
|
||||
margin-right: 24rpx;
|
||||
font-size: 28rpx;
|
||||
margin-right: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #546E7A;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04);
|
||||
@@ -79,11 +79,12 @@
|
||||
}
|
||||
|
||||
.category-item.active {
|
||||
background: #558B2F;
|
||||
background: linear-gradient(135deg, #558B2F, #689F38);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8rpx 20rpx rgba(85, 139, 47, 0.3);
|
||||
box-shadow: 0 6rpx 20rpx rgba(85, 139, 47, 0.3);
|
||||
border-color: #558B2F;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.wiki-list {
|
||||
@@ -179,28 +180,49 @@
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* Floating Action Button */
|
||||
.floating-add-btn {
|
||||
/* Floating Action Buttons */
|
||||
.floating-btns {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 60rpx;
|
||||
background: #558B2F;
|
||||
color: white;
|
||||
padding: 24rpx 40rpx;
|
||||
border-radius: 60rpx;
|
||||
bottom: 48rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4);
|
||||
z-index: 1000;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s ease;
|
||||
flex-direction: row;
|
||||
gap: 24rpx;
|
||||
z-index: 11600;
|
||||
}
|
||||
|
||||
.floating-add-btn:active {
|
||||
.floating-btn {
|
||||
color: white;
|
||||
padding: 18rpx 28rpx;
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.floating-btn:active {
|
||||
transform: scale(0.92);
|
||||
box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2);
|
||||
}
|
||||
|
||||
.scan-btn {
|
||||
background: rgba(85, 139, 47, 0.92);
|
||||
box-shadow: 0 8rpx 28rpx rgba(85, 139, 47, 0.35);
|
||||
}
|
||||
|
||||
.chat-btn {
|
||||
background: linear-gradient(135deg, rgba(21,101,192,0.92), rgba(25,118,210,0.92));
|
||||
box-shadow: 0 8rpx 28rpx rgba(21, 101, 192, 0.3);
|
||||
}
|
||||
|
||||
.btn-emoji {
|
||||
font-size: 32rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Popup Styles */
|
||||
|
||||
+53
-3
@@ -63,7 +63,7 @@ class WxRequest {
|
||||
if ((statusCode === 401 || data.code === 401) && !options.skipToken && !options._retry) {
|
||||
const app = getApp();
|
||||
if (app && app.forceRefreshLogin) {
|
||||
console.log('401 detected, refreshing token...');
|
||||
|
||||
app.forceRefreshLogin().then(() => {
|
||||
// Retry Original Request
|
||||
this.request({ ...options, _retry: true })
|
||||
@@ -266,12 +266,62 @@ class WxRequest {
|
||||
post(url, data = {}, header = {}) {
|
||||
return this.request({ url, method: 'POST', data, header });
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE stream request with chunked transfer.
|
||||
* Reuses baseUrl + token interceptor.
|
||||
* @param {string} url API path, e.g. '/plant/chat/stream'
|
||||
* @param {Object} data Query params (GET) or body (POST)
|
||||
* @param {Object} callbacks { onChunk, onDone, onError }
|
||||
* @returns {Object} wx.request task (call task.abort() to cancel)
|
||||
*/
|
||||
stream(url, data = {}, callbacks = {}) {
|
||||
const fullUrl = url.startsWith('http')
|
||||
? url
|
||||
: this.baseUrl + url;
|
||||
|
||||
// Build query string for GET
|
||||
const qs = Object.keys(data)
|
||||
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`)
|
||||
.join('&');
|
||||
const reqUrl = qs ? `${fullUrl}?${qs}` : fullUrl;
|
||||
|
||||
// Resolve token via same logic as interceptor
|
||||
let token = wx.getStorageSync('token');
|
||||
|
||||
const mergedHeader = {
|
||||
...this.header,
|
||||
'Accept': 'text/event-stream',
|
||||
};
|
||||
if (token) {
|
||||
mergedHeader['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const task = wx.request({
|
||||
url: reqUrl,
|
||||
method: 'GET',
|
||||
enableChunked: true,
|
||||
header: mergedHeader,
|
||||
success: () => {
|
||||
if (callbacks.onDone) callbacks.onDone();
|
||||
},
|
||||
fail: (err) => {
|
||||
if (callbacks.onError) callbacks.onError(err);
|
||||
},
|
||||
});
|
||||
|
||||
if (callbacks.onChunk) {
|
||||
task.onChunkReceived(callbacks.onChunk);
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize with default instance
|
||||
const request = new WxRequest({
|
||||
baseUrl: 'http://192.168.0.184:8889',
|
||||
//baseUrl: 'https://go.sundynix.cn/api',
|
||||
//baseUrl: 'http://192.168.0.184:8889',
|
||||
baseUrl: 'https://go.sundynix.cn/api',
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
+59
-16
@@ -1,38 +1,81 @@
|
||||
/**
|
||||
* Request WeChat Mini Program Subscription Message
|
||||
* Template ID: R7fh3NDpuV8DYqI83HpEQvC8mLJy5xMWFl1qeGN9JIo
|
||||
* Template ID: iG5GYMPQAgKxIE9zZNOgKS6tCURhM9p9AC8iZ3Uj3uA
|
||||
*/
|
||||
const TEMPLATE_ID = 'R7fh3NDpuV8DYqI83HpEQvC8mLJy5xMWFl1qeGN9JIo';
|
||||
const DEFAULT_TEMPLATE_ID = 'iG5GYMPQAgKxIE9zZNOgKS6tCURhM9p9AC8iZ3Uj3uA';
|
||||
const MIN_SUB_INTERVAL = 5000; // Minimum 5 seconds between real requests
|
||||
let lastSubTime = 0;
|
||||
|
||||
export const requestSubscription = () => {
|
||||
export const requestSubscription = (tmplIds = [DEFAULT_TEMPLATE_ID], silent = false) => {
|
||||
return new Promise((resolve) => {
|
||||
// Check if subscription capability is available (basic check)
|
||||
const now = Date.now();
|
||||
|
||||
// Throttling for silent requests to avoid "frequent request" errors
|
||||
if (silent && (now - lastSubTime < MIN_SUB_INTERVAL)) {
|
||||
console.log('[Subscribe] Throttled: Skipping real request to avoid frequency errors');
|
||||
resolve({ success: true, throttled: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wx.requestSubscribeMessage) {
|
||||
console.warn('Current version does not support subscribe message');
|
||||
if (!silent) console.warn('当前版本不支持订阅消息');
|
||||
resolve({ success: false, errMsg: 'Not supported' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update lastSubTime before the call
|
||||
lastSubTime = now;
|
||||
|
||||
wx.requestSubscribeMessage({
|
||||
tmplIds: [TEMPLATE_ID],
|
||||
tmplIds: tmplIds,
|
||||
success(res) {
|
||||
if (res[TEMPLATE_ID] === 'accept') {
|
||||
console.log('Subscription accepted');
|
||||
resolve({ success: true, status: 'accept' });
|
||||
} else {
|
||||
console.log('Subscription rejected or other status', res[TEMPLATE_ID]);
|
||||
resolve({ success: false, status: res[TEMPLATE_ID] });
|
||||
// Log detailed results for debugging
|
||||
console.log('[Subscribe] Result:', res);
|
||||
|
||||
// Check if any requested IDs were accepted
|
||||
const acceptedIds = tmplIds.filter(id => res[id] === 'accept');
|
||||
const isAccepted = acceptedIds.length > 0;
|
||||
|
||||
// Identify if user has opted for "Always Reject"
|
||||
const rejectedIds = tmplIds.filter(id => res[id] === 'reject');
|
||||
|
||||
if (rejectedIds.length > 0 && !silent) {
|
||||
console.warn('[Subscribe] User rejected templates (possibly "Always Reject"):', rejectedIds);
|
||||
}
|
||||
|
||||
resolve({
|
||||
success: isAccepted,
|
||||
res,
|
||||
isAlwaysRejected: rejectedIds.length === tmplIds.length
|
||||
});
|
||||
},
|
||||
fail(err) {
|
||||
console.error('Subscription failed', err);
|
||||
resolve({ success: false, errMsg: err.errMsg });
|
||||
console.error('[Subscribe] Request Failed:', err);
|
||||
|
||||
// 20004: User closed main switch in settings
|
||||
if (err.errCode === 20004) {
|
||||
if (!silent) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '订阅通知主开关已关闭,请在设置中开启',
|
||||
confirmText: '去开启',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
wx.openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('[Subscribe] Main subscription switch is OFF');
|
||||
}
|
||||
}
|
||||
resolve({ success: false, error: err, isMainSwitchOff: err.errCode === 20004 });
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const checkSubscriptionSettings = () => {
|
||||
export const checkSubscriptionSettings = (tmplId = DEFAULT_TEMPLATE_ID) => {
|
||||
return new Promise((resolve) => {
|
||||
if (!wx.getSetting) {
|
||||
resolve(undefined);
|
||||
@@ -43,7 +86,7 @@ export const checkSubscriptionSettings = () => {
|
||||
withSubscriptions: true,
|
||||
success(res) {
|
||||
const itemSettings = (res.subscriptionsSetting && res.subscriptionsSetting.itemSettings) || {};
|
||||
resolve(itemSettings[TEMPLATE_ID]);
|
||||
resolve(itemSettings[tmplId]);
|
||||
},
|
||||
fail() {
|
||||
resolve(undefined);
|
||||
|
||||
Reference in New Issue
Block a user