init: initial commit

This commit is contained in:
2026-10-07 09:28:43 +08:00
commit a68a4ef411
251 changed files with 38279 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
const { getLayout } = require('../../utils/layout');
// pages/pet/achievements.js
const { api } = require('../../utils/request');
Page({
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:true, 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();
}
});
+7
View File
@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "宠物成就",
"usingComponents": {
"app-nav": "/components/app-nav/app-nav",
"page-state": "/components/page-state/page-state"
}
}
+43
View File
@@ -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>
+183
View File
@@ -0,0 +1,183 @@
/* pages/pet/achievements.wxss */
@import '../../styles/common.wxss';
page {
background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 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, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 100%);
}
/* ===== 顶部导航(与其他页面一致,fixed 定位与微信胶囊对齐) ===== */
.top-nav-card {
background: linear-gradient(180deg, #F4E2E3 0%, #FAF5EE 55%, #FAF5EE 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; }
+309
View File
@@ -0,0 +1,309 @@
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({
onResize: function() { this.setData(getLayout()); },
data: { loading: true, loadError: 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:true, loadError:false});
try {
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) {
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' });
},
// 返回
goBack: function() {
wx.navigateBack();
},
stopPropagation: function() {}
});
+7
View File
@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "宠物养成",
"usingComponents": {
"app-nav": "/components/app-nav/app-nav",
"page-state": "/components/page-state/page-state"
}
}
+225
View File
@@ -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="{{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 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" 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>
+19
View File
@@ -0,0 +1,19 @@
@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; }