更新最新代码
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/pet/achievements.js
|
||||
const { api } = require('../../utils/request');
|
||||
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false,
|
||||
achievements: [],
|
||||
unlockedCount: 0,
|
||||
totalCount: 0,
|
||||
progressPercent: 0
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.loadAchievements();
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
this.loadAchievements();
|
||||
},
|
||||
|
||||
loadAchievements: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
const res = await api.petAchievements();
|
||||
let list = res.achievements || [];
|
||||
// 排序:已解锁的按解锁时间倒序(最新获得的排最前),未解锁的排后面
|
||||
list = list.sort(function(a, b) {
|
||||
var aUnlocked = a.unlocked ? 1 : 0;
|
||||
var bUnlocked = b.unlocked ? 1 : 0;
|
||||
if (aUnlocked !== bUnlocked) {
|
||||
return bUnlocked - aUnlocked;
|
||||
}
|
||||
if (aUnlocked && bUnlocked) {
|
||||
var ta = (a.unlocked_at || '').replace(/-/g, '').replace(/[: ]/g, '');
|
||||
var tb = (b.unlocked_at || '').replace(/-/g, '').replace(/[: ]/g, '');
|
||||
return tb.localeCompare(ta);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const unlocked = list.filter(a => a.unlocked);
|
||||
const total = list.length;
|
||||
var pageData = {
|
||||
achievements: list,
|
||||
unlockedCount: unlocked.length,
|
||||
totalCount: total,
|
||||
progressPercent: total > 0 ? Math.round(unlocked.length / total * 100) : 0
|
||||
};
|
||||
this.setData(pageData);
|
||||
} catch (e) {
|
||||
this.setData({loadError:true});
|
||||
console.error(e);
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
goBack: function() {
|
||||
wx.navigateBack();
|
||||
}
|
||||
}, {"loadAchievements": ["petAchievements"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "宠物成就",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/pet/achievements.wxml -->
|
||||
<app-nav title="宠物成就" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="container">
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadAchievements" /><block wx:else>
|
||||
<!-- 成就概览 -->
|
||||
<view class="ach-summary">
|
||||
<view class="ach-deco ach-deco-1">🌟</view>
|
||||
<view class="ach-deco ach-deco-2">🏅</view>
|
||||
<text class="ach-count">{{unlockedCount}}<text class="ach-total">/{{totalCount}}</text></text>
|
||||
<text class="ach-progress-text">已解锁成就</text>
|
||||
<view class="ach-progress-track">
|
||||
<view class="ach-progress-fill" style="width:{{progressPercent}}%;"></view>
|
||||
</view>
|
||||
<text class="ach-percent">{{progressPercent}}%</text>
|
||||
</view>
|
||||
|
||||
<!-- 成就列表 -->
|
||||
<view class="ach-list">
|
||||
<view wx:for="{{achievements}}" wx:key="key" class="ach-card {{item.unlocked?'':'locked'}}">
|
||||
<view class="ach-card-icon">
|
||||
<text>{{item.unlocked ? item.icon : '🔒'}}</text>
|
||||
</view>
|
||||
<view class="ach-card-info">
|
||||
<view class="ach-card-name-row">
|
||||
<text class="ach-card-name">{{item.name}}</text>
|
||||
<text class="ach-card-status {{item.unlocked?'unlocked':'locked-badge'}}">{{item.unlocked ? '已解锁' : '未解锁'}}</text>
|
||||
</view>
|
||||
<text class="ach-card-desc">{{item.desc}}</text>
|
||||
<text wx:if="{{item.unlocked && item.unlocked_at}}" class="ach-card-time">🏅 {{item.unlocked_at}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,187 @@
|
||||
/* pages/pet/achievements.wxss */
|
||||
@import '../../styles/common.wxss';
|
||||
|
||||
page {
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
scroll-view.page-bg {
|
||||
height: 100vh;
|
||||
padding-top: 196rpx;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
}
|
||||
|
||||
/* ===== 顶部导航(与其他页面一致,fixed 定位与微信胶囊对齐) ===== */
|
||||
.top-nav-card {
|
||||
background: linear-gradient(180deg, #FDE4EC 0%, #FFF0F5 48%, #FFF7FA 100%);
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #C96A85;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 右侧占位,保持标题真正居中 */
|
||||
.nav-spacer {
|
||||
width: 56rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 成就概览 ===== */
|
||||
.ach-summary {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #FFD3E0, #FFB6C6 55%, #FF9AB0);
|
||||
border-radius: 32rpx;
|
||||
padding: 40rpx 40rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
color: #fff;
|
||||
box-shadow: 0 12rpx 30rpx rgba(255, 140, 170, 0.22);
|
||||
}
|
||||
.ach-deco {
|
||||
position: absolute;
|
||||
font-size: 60rpx;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.ach-deco-1 {
|
||||
top: 24rpx;
|
||||
left: 32rpx;
|
||||
}
|
||||
.ach-deco-2 {
|
||||
bottom: 24rpx;
|
||||
right: 32rpx;
|
||||
}
|
||||
.ach-count {
|
||||
font-size: 88rpx;
|
||||
font-weight: 800;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
.ach-total {
|
||||
font-size: 40rpx;
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.ach-progress-text {
|
||||
font-size: 26rpx;
|
||||
opacity: 0.85;
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.ach-progress-track {
|
||||
height: 14rpx;
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-radius: 999rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ach-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #FFE066, #FFD700);
|
||||
border-radius: 999rpx;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
.ach-percent {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ===== 成就列表 ===== */
|
||||
.ach-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.ach-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(160deg, #FFF8FA 0%, #FFF0F4 100%);
|
||||
border: 2rpx solid rgba(255,190,210,0.3);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 140, 170, 0.08);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.ach-card.locked {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.ach-card-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #FFD3E0, #FFB6C6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48rpx;
|
||||
flex-shrink: 0;
|
||||
box-shadow: inset 0 0 0 2rpx rgba(255,107,129,0.15);
|
||||
}
|
||||
.ach-card.locked .ach-card-icon {
|
||||
background: #F5F5F5;
|
||||
box-shadow: inset 0 0 0 2rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.ach-card-info {
|
||||
flex: 1;
|
||||
}
|
||||
.ach-card-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.ach-card-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #5A4A4E;
|
||||
}
|
||||
.ach-card-desc {
|
||||
font-size: 24rpx;
|
||||
color: #B08A8A;
|
||||
display: block;
|
||||
}
|
||||
.ach-card-time {
|
||||
font-size: 20rpx;
|
||||
color: #C9A8A8;
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.ach-card-status {
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ach-card-status.unlocked {
|
||||
background: #E8F5E9;
|
||||
color: #66BB6A;
|
||||
}
|
||||
.ach-card-status.locked-badge {
|
||||
background: #F5F5F5;
|
||||
color: #C9A8A8;
|
||||
}
|
||||
|
||||
@import '../../styles/design.wxss';
|
||||
|
||||
@import '../../styles/journal.wxss';
|
||||
|
||||
.adopt-card,.stat-card,.action-card,.dynamics-card,.ach-card { background:#FFFDF8; border:1rpx solid #E9DDD0; box-shadow:none; }.adopt-title,.ach-card-name { color:#352E2A; }.adopt-desc,.ach-card-desc { color:#85776D; }.ach-summary { background:#F4E2E3; color:#943C58; box-shadow:none; }.ach-deco { display:none; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
@@ -0,0 +1,325 @@
|
||||
const { withDataCache } = require('../../utils/page-cache');
|
||||
const { getLayout } = require('../../utils/layout');
|
||||
// pages/pet/pet.js
|
||||
const { api } = require('../../utils/request');
|
||||
|
||||
const BREED_NAMES = {
|
||||
'golden': '金毛',
|
||||
'husky': '哈士奇',
|
||||
'corgi': '柯基',
|
||||
'shiba': '柴犬'
|
||||
};
|
||||
|
||||
const BREED_EMOJIS = {
|
||||
'golden': '🐕',
|
||||
'husky': '🐺',
|
||||
'corgi': '🦊',
|
||||
'shiba': '🐶'
|
||||
};
|
||||
|
||||
const STAGE_NAMES = ['', '幼犬期', '少年期', '青年期', '成年期', '终极期'];
|
||||
|
||||
Page(withDataCache({
|
||||
onResize: function() { this.setData(getLayout()); },
|
||||
data: { loading: true, loadError: false, needsBinding: false,
|
||||
hasPet: false,
|
||||
pet: null,
|
||||
daily: null,
|
||||
achievements: [],
|
||||
dynamics: [],
|
||||
displayDynamics: [],
|
||||
equipped: [],
|
||||
|
||||
// 领养表单
|
||||
breedSelect: 'golden',
|
||||
genderSelect: 1,
|
||||
petName: '',
|
||||
adopting: false,
|
||||
|
||||
// 宠物状态
|
||||
dailyStatus: {},
|
||||
partnerDaily: {},
|
||||
allDone: false,
|
||||
expProgress: 0,
|
||||
expForNextLevel: 0,
|
||||
breedName: '',
|
||||
petEmoji: '',
|
||||
stageName: '',
|
||||
equippedCollar: false,
|
||||
|
||||
// 最近成就
|
||||
recentAchievements: [],
|
||||
achievementCount: 0,
|
||||
|
||||
// 改名
|
||||
showRename: false,
|
||||
renameValue: '',
|
||||
renaming: false,
|
||||
// 动态折叠(默认收起,收起时显示一条)
|
||||
showAllDynamics: false
|
||||
},
|
||||
|
||||
onLoad: function() {
|
||||
this.setData(getLayout());
|
||||
this.loadPetInfo();
|
||||
},
|
||||
|
||||
onShow: function() {
|
||||
this.loadPetInfo();
|
||||
},
|
||||
|
||||
loadPetInfo: async function() {
|
||||
if (this._pageLoading) return;
|
||||
this._pageLoading = true;
|
||||
this.setData({loading: !this._hasCachedData, loadError:false});
|
||||
try {
|
||||
const relationship = await api.coupleInfo();
|
||||
if (relationship.not_bound) {
|
||||
this.setData({needsBinding:true,hasPet:false,pet:null,daily:null,achievements:[],dynamics:[],displayDynamics:[],showRename:false});
|
||||
return;
|
||||
}
|
||||
this.setData({needsBinding:false});
|
||||
const res = await api.petInfo();
|
||||
if (res.has_pet && res.pet) {
|
||||
const pet = res.pet;
|
||||
const dailyStatus = {};
|
||||
const partnerDaily = {};
|
||||
if (res.daily) {
|
||||
const d = res.daily;
|
||||
const suffix = res.is_user1 ? 'u1' : 'u2';
|
||||
const partnerSuffix = res.is_user1 ? 'u2' : 'u1';
|
||||
['feed', 'water', 'walk', 'bath', 'sleep', 'play'].forEach(a => {
|
||||
dailyStatus[a] = d[a + '_' + suffix] == 1;
|
||||
partnerDaily[a] = d[a + '_' + partnerSuffix] == 1;
|
||||
});
|
||||
}
|
||||
// 获取已装备物品
|
||||
const equipped = res.equipped || [];
|
||||
const equippedCollar = equipped.some(e => e.item_type === 'collar');
|
||||
// 计算最近成就
|
||||
const allAch = this.getAllAchievementDefs();
|
||||
const recentAch = allAch.filter(a => res.achievements.indexOf(a.key) >= 0).slice(0, 5);
|
||||
|
||||
// 计算领养天数
|
||||
var adoptDays = 0;
|
||||
if (pet.adopted_at) {
|
||||
var adoptDate = new Date(pet.adopted_at.replace(/-/g, '/'));
|
||||
if (!isNaN(adoptDate.getTime())) {
|
||||
adoptDays = Math.floor((Date.now() - adoptDate.getTime()) / 86400000);
|
||||
}
|
||||
}
|
||||
var pageData = {
|
||||
hasPet: true,
|
||||
pet: pet,
|
||||
daily: res.daily,
|
||||
achievements: res.achievements,
|
||||
dynamics: this._formatDynamics(res.dynamics || []),
|
||||
showAllDynamics: false,
|
||||
equipped: equipped,
|
||||
dailyStatus: dailyStatus,
|
||||
partnerDaily: partnerDaily,
|
||||
allDone: res.daily ? res.daily.all_done == 1 : false,
|
||||
expProgress: res.exp_progress || 0,
|
||||
expForNextLevel: res.exp_for_next_level || 0,
|
||||
breedName: BREED_NAMES[pet.breed] || '金毛',
|
||||
petEmoji: BREED_EMOJIS[pet.breed] || '🐕',
|
||||
stageName: STAGE_NAMES[pet.stage] || '幼犬期',
|
||||
equippedCollar: equippedCollar,
|
||||
recentAchievements: recentAch,
|
||||
achievementCount: (res.achievements || []).length,
|
||||
adoptDays: adoptDays
|
||||
};
|
||||
|
||||
this.setData(pageData);
|
||||
this._buildDisplayDynamics();
|
||||
|
||||
} else {
|
||||
this.setData({ hasPet: false, pet:null, daily:null, achievements:[], dynamics:[], displayDynamics:[] });
|
||||
}
|
||||
} catch (e) {
|
||||
if (e && e.error === '请先绑定情侣') {
|
||||
this.setData({needsBinding:true,hasPet:false,pet:null,showRename:false});
|
||||
} else {
|
||||
this.setData({loadError:true});
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
finally { this._pageLoading = false; this.setData({loading:false}); }
|
||||
},
|
||||
|
||||
// 格式化动态数据:时间转为友好显示,昵称取首字
|
||||
_formatDynamics: function(list) {
|
||||
var now = Date.now();
|
||||
return list.map(function(d) {
|
||||
var timeText = '';
|
||||
if (d.created_at) {
|
||||
var t = new Date(String(d.created_at).replace(/-/g, '/').replace('T', ' '));
|
||||
if (!isNaN(t.getTime())) {
|
||||
var diff = Math.floor((now - t.getTime()) / 1000);
|
||||
if (diff < 60) timeText = '刚刚';
|
||||
else if (diff < 3600) timeText = Math.floor(diff / 60) + '分钟前';
|
||||
else if (diff < 86400) timeText = Math.floor(diff / 3600) + '小时前';
|
||||
else if (diff < 172800) timeText = '昨天';
|
||||
else timeText = (t.getMonth() + 1) + '月' + t.getDate() + '日';
|
||||
}
|
||||
}
|
||||
var initial = d.user_id == 0 ? '🐾' : (d.nickname ? String(d.nickname).trim()[0] : '?');
|
||||
return {
|
||||
user_id: d.user_id,
|
||||
nickname: d.nickname,
|
||||
avatar: d.avatar || '',
|
||||
action: d.action,
|
||||
message: d.message,
|
||||
time_text: timeText,
|
||||
initial: initial
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getAllAchievementDefs: function() {
|
||||
return [
|
||||
{ key: 'first_adopt', name: '初次相遇', icon: '🐣' },
|
||||
{ key: 'level_5', name: '成长之路', icon: '⭐' },
|
||||
{ key: 'level_10', name: '小有成就', icon: '🌟' },
|
||||
{ key: 'level_20', name: '资深铲屎官', icon: '👑' },
|
||||
{ key: 'level_30', name: '终极陪伴', icon: '💎' },
|
||||
{ key: 'stage_2', name: '少年初长成', icon: '🐕' },
|
||||
{ key: 'stage_3', name: '青春活力', icon: '🦮' },
|
||||
{ key: 'stage_4', name: '成熟稳重', icon: '🐩' },
|
||||
{ key: 'stage_5', name: '终极形态', icon: '🦁' },
|
||||
{ key: 'all_done_7', name: '一周全勤', icon: '📅' },
|
||||
{ key: 'all_done_30', name: '月度之星', icon: '🏆' },
|
||||
{ key: 'feed_100', name: '大胃王', icon: '🍖' },
|
||||
{ key: 'walk_50', name: '散步达人', icon: '🚶' },
|
||||
{ key: 'bath_30', name: '爱干净', icon: '🛁' },
|
||||
{ key: 'play_50', name: '玩伴', icon: '🎾' },
|
||||
{ key: 'happiness_100', name: '快乐满分', icon: '😊' },
|
||||
{ key: 'rename_first', name: '取名大师', icon: '✏️' },
|
||||
{ key: 'adopt_7days', name: '一周陪伴', icon: '📆' },
|
||||
{ key: 'adopt_30days', name: '一月相伴', icon: '🗓️' },
|
||||
{ key: 'adopt_100days', name: '百日纪念', icon: '💯' }
|
||||
];
|
||||
},
|
||||
|
||||
// 选择品种
|
||||
selectBreed: function(e) {
|
||||
this.setData({ breedSelect: e.currentTarget.dataset.breed });
|
||||
},
|
||||
|
||||
// 选择性别
|
||||
selectGender: function(e) {
|
||||
this.setData({ genderSelect: parseInt(e.currentTarget.dataset.gender) });
|
||||
},
|
||||
|
||||
// 输入名字
|
||||
onNameInput: function(e) {
|
||||
this.setData({ petName: e.detail.value });
|
||||
},
|
||||
|
||||
// 领养
|
||||
doAdopt: async function() {
|
||||
var name = this.data.petName.trim();
|
||||
if (!name) {
|
||||
wx.showToast({ title: '请给宠物取个名字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ adopting: true });
|
||||
try {
|
||||
const res = await api.petAdopt({
|
||||
name: name,
|
||||
breed: this.data.breedSelect,
|
||||
gender: this.data.genderSelect
|
||||
});
|
||||
wx.showToast({ title: '🎉 领养成功!', icon: 'success' });
|
||||
this.loadPetInfo();
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '领养失败', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ adopting: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 执行日常任务
|
||||
doDailyAction: async function(e) {
|
||||
var action = e.currentTarget.dataset.action;
|
||||
if (this.data.dailyStatus[action]) {
|
||||
wx.showToast({ title: '已完成', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.petDailyAction({ action: action });
|
||||
wx.showToast({ title: '✅ +' + (res.exp_gain || 0) + '经验', icon: 'success', duration: 1500 });
|
||||
this.loadPetInfo();
|
||||
} catch (e) {
|
||||
if (e.already_done) {
|
||||
wx.showToast({ title: '今天已完成啦', icon: 'none' });
|
||||
this.loadPetInfo();
|
||||
} else {
|
||||
wx.showToast({ title: e.error || '操作失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 改名
|
||||
showRenameModal: function() {
|
||||
this.setData({
|
||||
showRename: true,
|
||||
renameValue: this.data.pet ? this.data.pet.name : ''
|
||||
});
|
||||
},
|
||||
hideRenameModal: function() {
|
||||
this.setData({ showRename: false });
|
||||
},
|
||||
onRenameInput: function(e) {
|
||||
this.setData({ renameValue: e.detail.value });
|
||||
},
|
||||
doRename: async function() {
|
||||
var name = this.data.renameValue.trim();
|
||||
if (!name) {
|
||||
wx.showToast({ title: '请输入名字', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.setData({ renaming: true });
|
||||
try {
|
||||
await api.petRename({ name: name });
|
||||
wx.showToast({ title: '改名成功 ✏️', icon: 'success' });
|
||||
this.setData({ showRename: false });
|
||||
this.loadPetInfo();
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.error || '改名失败', icon: 'none' });
|
||||
} finally {
|
||||
this.setData({ renaming: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 展开/收起动态
|
||||
toggleDynamics: function() {
|
||||
this.setData({ showAllDynamics: !this.data.showAllDynamics }, () => {
|
||||
this._buildDisplayDynamics();
|
||||
});
|
||||
},
|
||||
|
||||
// 根据折叠状态计算要展示的动态列表(收起时只显示一条)
|
||||
_buildDisplayDynamics: function() {
|
||||
var list = this.data.dynamics || [];
|
||||
var show = this.data.showAllDynamics;
|
||||
this.setData({ displayDynamics: show ? list : list.slice(0, 1) });
|
||||
},
|
||||
|
||||
// 跳转到成就页
|
||||
goToAchievements: function() {
|
||||
wx.navigateTo({ url: '/pages/pet/achievements' });
|
||||
},
|
||||
|
||||
bindPartner: function() {
|
||||
getApp().globalData.openProfileBind = true;
|
||||
wx.switchTab({url:'/pages/profile/profile'});
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack: function() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
stopPropagation: function() {}
|
||||
}, {"loadPetInfo": ["coupleInfo", "petInfo"]}));
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "宠物养成",
|
||||
"usingComponents": {
|
||||
"app-nav": "/components/app-nav/app-nav",
|
||||
"page-state": "/components/page-state/page-state"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<view class="app-shell app-shell--secondary" style="{{layoutStyle}}">
|
||||
<launch-transition />
|
||||
<!-- pages/pet/pet.wxml -->
|
||||
<app-nav title="宠物养成" back="{{true}}" />
|
||||
|
||||
<scroll-view class="page-bg" scroll-y show-scrollbar="{{false}}" bounces="{{true}}">
|
||||
<view class="c-body">
|
||||
<page-state wx:if="{{loading}}" kind="loading" title="正在加载" /><page-state wx:elif="{{needsBinding}}" title="绑定另一半后一起养宠物" description="邀请TA加入,一起领养和照顾你们的伙伴" action="去绑定" bind:action="bindPartner" /><page-state wx:elif="{{loadError}}" kind="error" title="加载失败" action="重试" bind:action="loadPetInfo" /><block wx:else>
|
||||
|
||||
<!-- ===== 未领养:领养引导 ===== -->
|
||||
<view wx:if="{{!hasPet}}" class="adopt-section">
|
||||
<view class="adopt-hero">
|
||||
<text class="adopt-hero-emoji">🐾</text>
|
||||
</view>
|
||||
<view class="adopt-card">
|
||||
<text class="adopt-title">领养一只属于我们的宠物</text>
|
||||
<text class="adopt-desc">一起照顾它,见证它成长</text>
|
||||
<view class="breed-select">
|
||||
<view class="breed-option {{breedSelect=='golden'?'active':''}}" bindtap="selectBreed" data-breed="golden">
|
||||
<text class="breed-emoji">🐕</text>
|
||||
<text class="breed-name">金毛</text>
|
||||
</view>
|
||||
<view class="breed-option {{breedSelect=='husky'?'active':''}}" bindtap="selectBreed" data-breed="husky">
|
||||
<text class="breed-emoji">🐺</text>
|
||||
<text class="breed-name">哈士奇</text>
|
||||
</view>
|
||||
<view class="breed-option {{breedSelect=='corgi'?'active':''}}" bindtap="selectBreed" data-breed="corgi">
|
||||
<text class="breed-emoji">🦊</text>
|
||||
<text class="breed-name">柯基</text>
|
||||
</view>
|
||||
<view class="breed-option {{breedSelect=='shiba'?'active':''}}" bindtap="selectBreed" data-breed="shiba">
|
||||
<text class="breed-emoji">🐶</text>
|
||||
<text class="breed-name">柴犬</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="input-group" style="margin-top:32rpx;">
|
||||
<input placeholder-class="handwritten-placeholder" class="input" placeholder="给宠物取个名字吧" value="{{petName}}" bindinput="onNameInput" maxlength="20" />
|
||||
</view>
|
||||
<view class="gender-select">
|
||||
<view class="gender-option {{genderSelect==1?'active':''}}" bindtap="selectGender" data-gender="1">
|
||||
<text>♂ 男孩</text>
|
||||
</view>
|
||||
<view class="gender-option {{genderSelect==2?'active':''}}" bindtap="selectGender" data-gender="2">
|
||||
<text>♀ 女孩</text>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn btn-primary adopt-btn" bindtap="doAdopt" loading="{{adopting}}">领养我们的伙伴</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 已领养:宠物主界面 ===== -->
|
||||
<view wx:else class="pet-main">
|
||||
|
||||
<view class="pet-profile">
|
||||
<view class="pet-profile-heading"><text class="j-eyebrow">OUR LITTLE COMPANION</text><view class="pet-profile-actions"><view bindtap="showRenameModal">改名</view><view bindtap="goToAchievements">成就</view></view></view>
|
||||
<view class="pet-profile-main"><view class="pet-avatar-wrap"><view class="pet-avatar-ring"><text class="pet-avatar">{{petEmoji}}</text></view><view class="pet-equip" wx:if="{{equippedCollar}}"><text>🎀</text></view></view><view class="pet-profile-info"><view class="pet-name-row"><text class="pet-name">{{pet.name}}</text><text class="pet-gender-text">{{pet.gender==1?'♂':'♀'}}</text></view><text class="pet-description">{{breedName}} · {{stageName}}</text><text class="pet-adopt-days">一起照顾了 {{adoptDays || 0}} 天</text></view></view>
|
||||
<view class="pet-level-bar"><text class="level-label">Lv.{{pet.level}}</text><view class="exp-track"><view class="exp-fill" style="width:{{expProgress}}%;" /></view><text class="exp-text">{{pet.exp}} / {{expForNextLevel}}</text></view>
|
||||
</view>
|
||||
|
||||
<!-- 属性卡片 -->
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card stat-happiness">
|
||||
<view class="stat-head">
|
||||
<text class="stat-icon">😊</text>
|
||||
<text class="stat-label">心情</text>
|
||||
<text class="stat-value">{{pet.happiness}}</text>
|
||||
</view>
|
||||
<view class="stat-bar"><view class="stat-fill" style="width:{{pet.happiness}}%;"></view></view>
|
||||
</view>
|
||||
<view class="stat-card stat-hunger">
|
||||
<view class="stat-head">
|
||||
<text class="stat-icon">🍖</text>
|
||||
<text class="stat-label">饱腹</text>
|
||||
<text class="stat-value">{{pet.hunger}}</text>
|
||||
</view>
|
||||
<view class="stat-bar"><view class="stat-fill" style="width:{{pet.hunger}}%;"></view></view>
|
||||
</view>
|
||||
<view class="stat-card stat-energy">
|
||||
<view class="stat-head">
|
||||
<text class="stat-icon">⚡</text>
|
||||
<text class="stat-label">精力</text>
|
||||
<text class="stat-value">{{pet.energy}}</text>
|
||||
</view>
|
||||
<view class="stat-bar"><view class="stat-fill" style="width:{{pet.energy}}%;"></view></view>
|
||||
</view>
|
||||
<view class="stat-card stat-cleanliness">
|
||||
<view class="stat-head">
|
||||
<text class="stat-icon">✨</text>
|
||||
<text class="stat-label">清洁</text>
|
||||
<text class="stat-value">{{pet.cleanliness}}</text>
|
||||
</view>
|
||||
<view class="stat-bar"><view class="stat-fill" style="width:{{pet.cleanliness}}%;"></view></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日任务 -->
|
||||
<view class="section-title">
|
||||
<text>今日照顾</text>
|
||||
<text class="section-subtitle">一起完成</text>
|
||||
</view>
|
||||
<view class="daily-grid">
|
||||
<view class="daily-item {{dailyStatus.feed?'done':''}}" bindtap="doDailyAction" data-action="feed">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">🍖</text>
|
||||
<view class="daily-check {{dailyStatus.feed?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.feed}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">喂食</text>
|
||||
<text class="daily-partner {{partnerDaily.feed?'partner-done':''}}">{{partnerDaily.feed ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.water?'done':''}}" bindtap="doDailyAction" data-action="water">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">💧</text>
|
||||
<view class="daily-check {{dailyStatus.water?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.water}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">喂水</text>
|
||||
<text class="daily-partner {{partnerDaily.water?'partner-done':''}}">{{partnerDaily.water ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.walk?'done':''}}" bindtap="doDailyAction" data-action="walk">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">🐾</text>
|
||||
<view class="daily-check {{dailyStatus.walk?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.walk}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">遛狗</text>
|
||||
<text class="daily-partner {{partnerDaily.walk?'partner-done':''}}">{{partnerDaily.walk ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.bath?'done':''}}" bindtap="doDailyAction" data-action="bath">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">🛁</text>
|
||||
<view class="daily-check {{dailyStatus.bath?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.bath}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">洗澡</text>
|
||||
<text class="daily-partner {{partnerDaily.bath?'partner-done':''}}">{{partnerDaily.bath ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.sleep?'done':''}}" bindtap="doDailyAction" data-action="sleep">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">😴</text>
|
||||
<view class="daily-check {{dailyStatus.sleep?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.sleep}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">睡觉</text>
|
||||
<text class="daily-partner {{partnerDaily.sleep?'partner-done':''}}">{{partnerDaily.sleep ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
<view class="daily-item {{dailyStatus.play?'done':''}}" bindtap="doDailyAction" data-action="play">
|
||||
<view class="daily-icon-wrap">
|
||||
<text class="daily-icon">🎾</text>
|
||||
<view class="daily-check {{dailyStatus.play?'checked':''}}">
|
||||
<text wx:if="{{dailyStatus.play}}">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="daily-label">玩耍</text>
|
||||
<text class="daily-partner {{partnerDaily.play?'partner-done':''}}">{{partnerDaily.play ? 'TA已完成' : 'TA未完成'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 全部完成庆祝 -->
|
||||
<view wx:if="{{allDone}}" class="all-done-banner">
|
||||
<text>今天的照顾已完成,辛苦啦</text>
|
||||
</view>
|
||||
|
||||
<!-- 成就入口 -->
|
||||
<view class="section-title">
|
||||
<text>成长纪念</text>
|
||||
<text class="section-subtitle" bindtap="goToAchievements">查看全部 →</text>
|
||||
</view>
|
||||
<view class="achievement-strip">
|
||||
<view wx:for="{{recentAchievements}}" wx:key="key" class="ach-badge">
|
||||
<text class="ach-icon">{{item.icon}}</text>
|
||||
<text class="ach-name">{{item.name}}</text>
|
||||
</view>
|
||||
<view wx:if="{{achievementCount==0}}" class="ach-empty">
|
||||
<text>完成日常任务解锁成就</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态 -->
|
||||
<view class="section-title">
|
||||
<text>最近的陪伴</text>
|
||||
<text class="section-subtitle" wx:if="{{dynamics.length>1}}" bindtap="toggleDynamics">{{showAllDynamics?'收起':'查看全部'}} ({{dynamics.length}})</text>
|
||||
</view>
|
||||
<view class="dynamics-list">
|
||||
<view wx:for="{{displayDynamics}}" wx:key="id" class="dynamic-item" wx:for-item="d">
|
||||
<view class="dynamic-avatar">
|
||||
<image wx:if="{{d.avatar}}" class="dynamic-avatar-img" src="{{d.avatar}}" mode="aspectFill" />
|
||||
<text wx:else>{{d.initial}}</text>
|
||||
</view>
|
||||
<view class="dynamic-content">
|
||||
<text class="dynamic-text">{{d.message}}</text>
|
||||
<text class="dynamic-time">{{d.time_text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{dynamics.length==0}}" class="empty-state">
|
||||
<text>🐾</text>
|
||||
<text>还没有动态,快去照顾宠物吧</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<view style="height:60rpx;"></view>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 改名弹窗 -->
|
||||
<view wx:if="{{showRename}}" class="modal-overlay" bindtap="hideRenameModal">
|
||||
<view class="rename-modal" catchtap="stopPropagation">
|
||||
<text class="rename-modal-title">给宠物改名</text>
|
||||
<input class="rename-input" placeholder="输入新名字" placeholder-class="rename-placeholder handwritten-placeholder" value="{{renameValue}}" bindinput="onRenameInput" maxlength="20" />
|
||||
<view class="rename-actions">
|
||||
<button class="btn btn-secondary rename-btn" bindtap="hideRenameModal">取消</button>
|
||||
<button class="btn btn-primary rename-btn" bindtap="doRename" loading="{{renaming}}">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,23 @@
|
||||
@import '../../styles/common.wxss';
|
||||
@import '../../styles/design.wxss';
|
||||
@import '../../styles/journal.wxss';
|
||||
@import '../../styles/companion.wxss';
|
||||
.pet-profile { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:12rpx 36rpx 12rpx 12rpx; padding:28rpx; }
|
||||
.pet-profile-heading { display:flex; align-items:center; justify-content:space-between; gap:16rpx; }.pet-profile-heading .j-eyebrow { font-size:17rpx; letter-spacing:1rpx; }
|
||||
.pet-profile-actions { display:flex; align-items:center; gap:18rpx; }.pet-profile-actions > view { font-size:22rpx; color:#B74768; min-height:52rpx; display:flex; align-items:center; }
|
||||
.pet-profile-main { display:flex; align-items:center; gap:26rpx; margin:22rpx 0 30rpx; }.pet-avatar-wrap { position:relative; flex-shrink:0; }.pet-avatar-ring { width:134rpx; height:134rpx; background:#F1EBE2; border:1rpx solid #E9DDD0; border-radius:50%; display:flex; align-items:center; justify-content:center; }.pet-avatar { font-size:76rpx; }.pet-equip { position:absolute; right:-6rpx; top:0; font-size:28rpx; }
|
||||
.pet-profile-info { flex:1; min-width:0; }.pet-name-row { display:flex; align-items:center; gap:12rpx; }.pet-name { font-size:36rpx; line-height:1.4; font-weight:600; color:#352E2A; overflow-wrap:break-word; }.pet-gender-text { font-size:24rpx; color:#85776D; }.pet-description { display:block; font-size:24rpx; color:#65594F; margin-top:10rpx; }.pet-adopt-days { display:block; font-size:23rpx; color:#85776D; margin-top:12rpx; }
|
||||
.pet-level-bar { display:flex; align-items:center; gap:16rpx; }.level-label { font-size:22rpx; color:#943C58; flex-shrink:0; }.exp-track { height:10rpx; flex:1; background:#EDE3D7; border-radius:10rpx; overflow:hidden; }.exp-fill { height:100%; background:#B78F68; border-radius:10rpx; }.exp-text { font-size:20rpx; color:#85776D; flex-shrink:0; }
|
||||
.stats-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:16rpx; margin-top:20rpx; padding:0!important; border:0!important; background:transparent!important; box-shadow:none!important; }
|
||||
.stat-card { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:14rpx; padding:20rpx; }.stat-head { display:flex; align-items:center; gap:10rpx; margin-bottom:16rpx; }.stat-icon { font-size:27rpx; }.stat-label { flex:1; font-size:23rpx; color:#85776D; }.stat-value { font-size:28rpx; font-weight:500; color:#352E2A; }.stat-bar { height:7rpx; background:#EDE3D7; border-radius:8rpx; overflow:hidden; }.stat-fill { height:100%; border-radius:8rpx; }.stat-happiness .stat-fill { background:#C6A06F; }.stat-hunger .stat-fill { background:#CA8E9E; }.stat-energy .stat-fill { background:#91A487; }.stat-cleanliness .stat-fill { background:#87A3B0; }
|
||||
.section-title { display:flex; align-items:center; justify-content:space-between; gap:16rpx; margin:34rpx 0 20rpx; font-size:29rpx; font-weight:600; color:#352E2A; }.section-subtitle { font-size:22rpx; font-weight:400; color:#85776D; padding:10rpx 0; }
|
||||
.daily-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:16rpx; }.daily-item { display:flex; flex-direction:column; align-items:center; gap:12rpx; padding:22rpx 8rpx; background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:14rpx; }.daily-item.done { background:#F0F3EB; border-color:#C7D0BA; }.daily-icon-wrap { position:relative; }.daily-icon { font-size:34rpx; }.daily-check { position:absolute; right:-16rpx; top:-4rpx; width:24rpx; height:24rpx; border:1rpx solid #CBBDAE; background:#FFFDF8; border-radius:50%; }.daily-check.checked { display:flex; align-items:center; justify-content:center; color:#FFFDF8; border-color:#8B9D7E; background:#8B9D7E; font-size:18rpx; }.daily-label { color:#352E2A; font-size:25rpx; }.daily-partner { font-size:18rpx; color:#A39386; }.daily-partner.partner-done { color:#677B59; }.all-done-banner { margin-top:20rpx; border-radius:14rpx; background:#F0F3EB; padding:22rpx; text-align:center; font-size:24rpx; color:#677B59; }
|
||||
.achievement-strip { display:flex; gap:14rpx; overflow-x:auto; padding-bottom:8rpx; }.ach-badge { display:flex; flex-direction:column; align-items:center; gap:12rpx; min-width:116rpx; padding:18rpx 12rpx; background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:14rpx; flex-shrink:0; }.ach-icon { font-size:34rpx; }.ach-name { color:#65594F; font-size:20rpx; }.ach-empty { width:100%; text-align:center; font-size:23rpx; color:#85776D; padding:30rpx 0; }
|
||||
.dynamics-list { border-top:1rpx solid #E9DDD0; }.dynamic-item { display:flex; align-items:flex-start; gap:16rpx; padding:24rpx 0; border-bottom:1rpx solid #E9DDD0; }.dynamic-avatar { width:50rpx; height:50rpx; border-radius:50%; background:#F4E2E3; color:#943C58; display:flex; align-items:center; justify-content:center; font-size:23rpx; flex-shrink:0; overflow:hidden; }.dynamic-avatar-img { width:100%; height:100%; }.dynamic-content { flex:1; min-width:0; }.dynamic-text { display:block; font-size:25rpx; color:#65594F; line-height:1.6; }.dynamic-time { display:block; font-size:21rpx; color:#A39386; margin-top:8rpx; }
|
||||
.adopt-hero { text-align:center; margin:12rpx 0 28rpx; }.adopt-hero-emoji { font-size:64rpx; }.adopt-card { background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:16rpx; padding:28rpx; }.adopt-title { display:block; font-size:31rpx; font-weight:600; color:#352E2A; text-align:center; }.adopt-desc { display:block; font-size:24rpx; line-height:1.6; color:#85776D; margin-top:14rpx; text-align:center; }
|
||||
.breed-select { display:grid; grid-template-columns:repeat(4,1fr); gap:12rpx; margin-top:28rpx; }.breed-option { display:flex; flex-direction:column; align-items:center; gap:10rpx; padding:18rpx 4rpx; border:1rpx solid #E9DDD0; border-radius:12rpx; }.breed-option.active { background:#F4E2E3; border-color:#D5A6AE; }.breed-emoji { font-size:35rpx; }.breed-name { font-size:22rpx; color:#65594F; }.gender-select { display:flex; gap:16rpx; margin:24rpx 0; }.gender-option { display:flex; flex:1; justify-content:center; padding:20rpx 8rpx; border:1rpx solid #E9DDD0; border-radius:12rpx; color:#85776D; font-size:25rpx; }.gender-option.active { background:#F4E2E3; border-color:#D5A6AE; color:#943C58; }.adopt-btn { width:100%; font-size:26rpx; border-radius:18rpx; padding:24rpx 12rpx; }
|
||||
.modal-overlay { z-index:1200; }.rename-modal { width:100%; max-width:600rpx; padding:36rpx; background:#FFFDF8; border:1rpx solid #E9DDD0; border-radius:24rpx; box-sizing:border-box; }.rename-modal-title { display:block; color:#352E2A; font-weight:600; font-size:31rpx; margin-bottom:26rpx; }.rename-input { width:100%; height:84rpx; padding:0 20rpx; background:#FAF5EE; border:1rpx solid #E9DDD0; border-radius:14rpx; box-sizing:border-box; font-size:27rpx; }.rename-actions { display:flex; gap:20rpx; margin-top:28rpx; }.rename-btn { flex:1; min-width:0; padding:24rpx 10rpx; font-size:26rpx; }.rename-placeholder { color:#A39386; }
|
||||
|
||||
@import '../../styles/button-theme.wxss';
|
||||
|
||||
@import '../../styles/typography.wxss';
|
||||
Reference in New Issue
Block a user