This commit is contained in:
parent
58ac884203
commit
c338c5d3d0
102
pages.json
102
pages.json
|
|
@ -169,6 +169,102 @@
|
|||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/user-info",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人信息",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/family",
|
||||
"style": {
|
||||
"navigationBarTitleText": "家庭管理",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/reminders",
|
||||
"style": {
|
||||
"navigationBarTitleText": "提醒事项",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/notifications",
|
||||
"style": {
|
||||
"navigationBarTitleText": "消息通知",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/privacy",
|
||||
"style": {
|
||||
"navigationBarTitleText": "隐私设置",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/feedback",
|
||||
"style": {
|
||||
"navigationBarTitleText": "意见反馈",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/adoption/my-adoption",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的领养",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/adoption/my-published",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我发布的",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/adoption/my-applications",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我申请的",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/adoption/publish",
|
||||
"style": {
|
||||
"navigationBarTitleText": "发布领养",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/review/review",
|
||||
"style": {
|
||||
"navigationBarTitleText": "产品评测",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/review/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "评测详情",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
|
|
@ -207,6 +303,12 @@
|
|||
"iconPath": "static/tabbar/assistant.png",
|
||||
"selectedIconPath": "static/tabbar/assistant-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/review/review",
|
||||
"text": "评测",
|
||||
"iconPath": "static/tabbar/review.png",
|
||||
"selectedIconPath": "static/tabbar/review-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/adoption/adoption",
|
||||
"text": "领养",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,902 @@
|
|||
<template>
|
||||
<view class="my-adoption-container">
|
||||
<!-- 统计概览卡片 -->
|
||||
<view class="stats-overview-card">
|
||||
<view class="overview-header">
|
||||
<view class="header-info">
|
||||
<text class="header-title">我的领养</text>
|
||||
<text class="header-desc">管理我的领养信息和申请记录</text>
|
||||
</view>
|
||||
<view class="header-icon">
|
||||
<text class="icon-text">🏠</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item" @click="navigateTo('/pages/adoption/my-published')">
|
||||
<view class="stat-number">{{ publishedCount }}</view>
|
||||
<view class="stat-label">已发布</view>
|
||||
<view class="stat-icon">📝</view>
|
||||
</view>
|
||||
<view class="stat-item" @click="navigateTo('/pages/adoption/my-applications')">
|
||||
<view class="stat-number">{{ applicationCount }}</view>
|
||||
<view class="stat-label">已申请</view>
|
||||
<view class="stat-icon">📋</view>
|
||||
</view>
|
||||
<view class="stat-item" @click="showSuccessStories">
|
||||
<view class="stat-number">{{ successCount }}</view>
|
||||
<view class="stat-label">成功领养</view>
|
||||
<view class="stat-icon">❤️</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速操作卡片 -->
|
||||
<view class="quick-actions-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">快速操作</text>
|
||||
</view>
|
||||
<view class="actions-grid">
|
||||
<view class="action-item" @click="navigateTo('/pages/adoption/publish')">
|
||||
<view class="action-icon">➕</view>
|
||||
<text class="action-text">发布领养</text>
|
||||
<text class="action-desc">发布新的领养信息</text>
|
||||
</view>
|
||||
<view class="action-item" @click="navigateTo('/pages/adoption/adoption')">
|
||||
<view class="action-icon">🔍</view>
|
||||
<text class="action-text">浏览领养</text>
|
||||
<text class="action-desc">查看可领养宠物</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近发布卡片 -->
|
||||
<view class="recent-published-card" v-if="recentPublished.length > 0">
|
||||
<view class="card-header">
|
||||
<text class="card-title">最近发布</text>
|
||||
<view class="header-action" @click="navigateTo('/pages/adoption/my-published')">
|
||||
<text class="action-text">查看全部</text>
|
||||
<text class="action-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="published-list">
|
||||
<view
|
||||
class="published-item"
|
||||
v-for="item in recentPublished.slice(0, 3)"
|
||||
:key="item.id"
|
||||
@click="viewPublishedDetail(item)"
|
||||
>
|
||||
<view class="item-image">
|
||||
<image class="pet-image" :src="item.photos[0]" mode="aspectFill" />
|
||||
<view class="status-badge" :class="item.status">
|
||||
<text class="status-text">{{ getStatusText(item.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<view class="pet-name">{{ item.name }}</view>
|
||||
<view class="pet-meta">
|
||||
<text class="meta-text">{{ item.breed }} · {{ item.gender }}</text>
|
||||
</view>
|
||||
<view class="publish-time">
|
||||
<text class="time-text">{{ formatTime(item.publishTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-stats">
|
||||
<view class="stat-row">
|
||||
<text class="stat-label">申请</text>
|
||||
<text class="stat-value">{{ item.applicationCount || 0 }}</text>
|
||||
</view>
|
||||
<view class="stat-row">
|
||||
<text class="stat-label">浏览</text>
|
||||
<text class="stat-value">{{ item.viewCount || 0 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近申请卡片 -->
|
||||
<view class="recent-applications-card" v-if="recentApplications.length > 0">
|
||||
<view class="card-header">
|
||||
<text class="card-title">最近申请</text>
|
||||
<view class="header-action" @click="navigateTo('/pages/adoption/my-applications')">
|
||||
<text class="action-text">查看全部</text>
|
||||
<text class="action-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="applications-list">
|
||||
<view
|
||||
class="application-item"
|
||||
v-for="item in recentApplications.slice(0, 3)"
|
||||
:key="item.id"
|
||||
@click="viewApplicationDetail(item)"
|
||||
>
|
||||
<view class="item-image">
|
||||
<image class="pet-image" :src="item.petPhoto" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<view class="pet-name">{{ item.petName }}</view>
|
||||
<view class="pet-meta">
|
||||
<text class="meta-text">{{ item.petBreed }} · {{ item.petGender }}</text>
|
||||
</view>
|
||||
<view class="application-time">
|
||||
<text class="time-text">{{ formatTime(item.applicationTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-status">
|
||||
<view class="status-badge" :class="item.status">
|
||||
<text class="status-text">{{ getApplicationStatusText(item.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="publishedCount === 0 && applicationCount === 0">
|
||||
<view class="empty-icon">🏠</view>
|
||||
<view class="empty-text">还没有领养相关记录</view>
|
||||
<view class="empty-desc">发布领养信息或申请领养可爱的宠物</view>
|
||||
<view class="empty-actions">
|
||||
<view class="empty-action primary" @click="navigateTo('/pages/adoption/publish')">
|
||||
<text class="action-text">发布领养</text>
|
||||
</view>
|
||||
<view class="empty-action" @click="navigateTo('/pages/adoption/adoption')">
|
||||
<text class="action-text">浏览领养</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 帮助提示卡片 -->
|
||||
<view class="help-card">
|
||||
<view class="help-header">
|
||||
<text class="help-icon">💡</text>
|
||||
<text class="help-title">领养小贴士</text>
|
||||
</view>
|
||||
<view class="help-content">
|
||||
<view class="help-item">
|
||||
<text class="help-text">• 发布领养信息时请提供详细的宠物信息和照片</text>
|
||||
</view>
|
||||
<view class="help-item">
|
||||
<text class="help-text">• 申请领养时请如实填写个人信息和养宠经验</text>
|
||||
</view>
|
||||
<view class="help-item">
|
||||
<text class="help-text">• 保持良好的沟通,为宠物找到最合适的家庭</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref, onMounted, computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'MyAdoptionPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const publishedCount = ref(0)
|
||||
const applicationCount = ref(0)
|
||||
const successCount = ref(0)
|
||||
const recentPublished = ref([])
|
||||
const recentApplications = ref([])
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadAdoptionData()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadAdoptionData = () => {
|
||||
try {
|
||||
// 加载发布的领养信息
|
||||
const publishedData = uni.getStorageSync('myPublishedAdoptions') || []
|
||||
publishedCount.value = publishedData.length
|
||||
recentPublished.value = publishedData.sort((a, b) =>
|
||||
new Date(b.publishTime) - new Date(a.publishTime)
|
||||
)
|
||||
|
||||
// 加载申请记录
|
||||
const applicationData = uni.getStorageSync('myAdoptionApplications') || []
|
||||
applicationCount.value = applicationData.length
|
||||
recentApplications.value = applicationData.sort((a, b) =>
|
||||
new Date(b.applicationTime) - new Date(a.applicationTime)
|
||||
)
|
||||
|
||||
// 计算成功领养数量
|
||||
const successfulAdoptions = publishedData.filter(item => item.status === 'completed').length +
|
||||
applicationData.filter(item => item.status === 'approved').length
|
||||
successCount.value = successfulAdoptions
|
||||
|
||||
// 如果没有数据,创建一些模拟数据用于展示
|
||||
if (publishedData.length === 0 && applicationData.length === 0) {
|
||||
createMockData()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载领养数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const createMockData = () => {
|
||||
// 创建模拟发布数据
|
||||
const mockPublished = [
|
||||
{
|
||||
id: 'pub_1',
|
||||
name: '小橘',
|
||||
breed: '橘猫',
|
||||
gender: '公',
|
||||
photos: ['/static/mock-cat1.jpg'],
|
||||
status: 'active',
|
||||
publishTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
applicationCount: 3,
|
||||
viewCount: 25
|
||||
}
|
||||
]
|
||||
|
||||
// 创建模拟申请数据
|
||||
const mockApplications = [
|
||||
{
|
||||
id: 'app_1',
|
||||
petName: '小白',
|
||||
petBreed: '英短',
|
||||
petGender: '母',
|
||||
petPhoto: '/static/mock-cat2.jpg',
|
||||
status: 'pending',
|
||||
applicationTime: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
]
|
||||
|
||||
uni.setStorageSync('myPublishedAdoptions', mockPublished)
|
||||
uni.setStorageSync('myAdoptionApplications', mockApplications)
|
||||
|
||||
publishedCount.value = mockPublished.length
|
||||
applicationCount.value = mockApplications.length
|
||||
recentPublished.value = mockPublished
|
||||
recentApplications.value = mockApplications
|
||||
}
|
||||
|
||||
const navigateTo = (url) => {
|
||||
uni.navigateTo({
|
||||
url,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showSuccessStories = () => {
|
||||
if (successCount.value > 0) {
|
||||
uni.showModal({
|
||||
title: '成功领养',
|
||||
content: `恭喜您!已成功完成 ${successCount.value} 次领养,为宠物们找到了温暖的家。`,
|
||||
showCancel: false
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '暂无成功记录',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const viewPublishedDetail = (item) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/adoption/adoption-detail?id=${item.id}&from=published`,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewApplicationDetail = (item) => {
|
||||
uni.showModal({
|
||||
title: item.petName,
|
||||
content: `申请状态:${getApplicationStatusText(item.status)}\n申请时间:${formatTime(item.applicationTime)}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'active': '招募中',
|
||||
'paused': '已暂停',
|
||||
'completed': '已完成',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
const getApplicationStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'pending': '待审核',
|
||||
'approved': '已通过',
|
||||
'rejected': '已拒绝',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
const formatTime = (timeString) => {
|
||||
if (!timeString) return ''
|
||||
const date = new Date(timeString)
|
||||
const now = new Date()
|
||||
const diffTime = now - date
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) {
|
||||
return '今天'
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天'
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}天前`
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
publishedCount,
|
||||
applicationCount,
|
||||
successCount,
|
||||
recentPublished,
|
||||
recentApplications,
|
||||
navigateTo,
|
||||
showSuccessStories,
|
||||
viewPublishedDetail,
|
||||
viewApplicationDetail,
|
||||
getStatusText,
|
||||
getApplicationStatusText,
|
||||
formatTime
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-adoption-container {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.stats-overview-card,
|
||||
.quick-actions-card,
|
||||
.recent-published-card,
|
||||
.recent-applications-card,
|
||||
.help-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 0 30rpx 24rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.action-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 统计概览卡片 */
|
||||
.stats-overview-card {
|
||||
.overview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
|
||||
.header-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.header-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
.icon-text {
|
||||
font-size: 60rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 24rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #FF8A80;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 快速操作卡片 */
|
||||
.quick-actions-card {
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 32rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.action-desc {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 最近发布卡片 */
|
||||
.recent-published-card {
|
||||
.published-list {
|
||||
.published-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
position: relative;
|
||||
margin-right: 20rpx;
|
||||
|
||||
.pet-image {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 18rpx;
|
||||
|
||||
&.active {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.paused {
|
||||
background: #FF9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
background: #F44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 18rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
|
||||
.pet-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.pet-meta {
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.meta-text {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.publish-time {
|
||||
.time-text {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-stats {
|
||||
text-align: right;
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 最近申请卡片 */
|
||||
.recent-applications-card {
|
||||
.applications-list {
|
||||
.application-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
margin-right: 20rpx;
|
||||
|
||||
.pet-image {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
|
||||
.pet-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.pet-meta {
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.meta-text {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.application-time {
|
||||
.time-text {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-status {
|
||||
.status-badge {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 20rpx;
|
||||
|
||||
&.pending {
|
||||
background: #FFF3E0;
|
||||
color: #FF9800;
|
||||
border: 1rpx solid #FFE0B2;
|
||||
}
|
||||
|
||||
&.approved {
|
||||
background: #E8F5E8;
|
||||
color: #4CAF50;
|
||||
border: 1rpx solid #C8E6C9;
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
background: #FFEBEE;
|
||||
color: #F44336;
|
||||
border: 1rpx solid #FFCDD2;
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
background: #F5F5F5;
|
||||
color: #757575;
|
||||
border: 1rpx solid #E0E0E0;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80rpx 40rpx;
|
||||
margin: 0 30rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
margin-bottom: 40rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
justify-content: center;
|
||||
|
||||
.empty-action {
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 2rpx solid #FF8A80;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.primary {
|
||||
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
|
||||
|
||||
.action-text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.primary) {
|
||||
background: transparent;
|
||||
|
||||
.action-text {
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 帮助提示卡片 */
|
||||
.help-card {
|
||||
.help-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.help-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.help-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.help-content {
|
||||
.help-item {
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.my-adoption-container {
|
||||
.stats-overview-card,
|
||||
.quick-actions-card,
|
||||
.recent-published-card,
|
||||
.recent-applications-card,
|
||||
.help-card,
|
||||
.empty-state {
|
||||
margin: 0 20rpx 20rpx 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
gap: 16rpx;
|
||||
|
||||
.stat-item {
|
||||
padding: 20rpx 12rpx;
|
||||
|
||||
.stat-number {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
gap: 16rpx;
|
||||
|
||||
.action-item {
|
||||
padding: 24rpx 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.my-adoption-container > view {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.my-adoption-container > view:nth-child(1) { animation-delay: 0.1s; }
|
||||
.my-adoption-container > view:nth-child(2) { animation-delay: 0.2s; }
|
||||
.my-adoption-container > view:nth-child(3) { animation-delay: 0.3s; }
|
||||
.my-adoption-container > view:nth-child(4) { animation-delay: 0.4s; }
|
||||
.my-adoption-container > view:nth-child(5) { animation-delay: 0.5s; }
|
||||
.my-adoption-container > view:nth-child(6) { animation-delay: 0.6s; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,831 @@
|
|||
<template>
|
||||
<view class="my-applications-container">
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats-card">
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item" :class="{ active: currentFilter === 'all' }" @click="setFilter('all')">
|
||||
<view class="stat-number">{{ totalCount }}</view>
|
||||
<view class="stat-label">全部</view>
|
||||
</view>
|
||||
<view class="stat-item" :class="{ active: currentFilter === 'pending' }" @click="setFilter('pending')">
|
||||
<view class="stat-number">{{ pendingCount }}</view>
|
||||
<view class="stat-label">待审核</view>
|
||||
</view>
|
||||
<view class="stat-item" :class="{ active: currentFilter === 'approved' }" @click="setFilter('approved')">
|
||||
<view class="stat-number">{{ approvedCount }}</view>
|
||||
<view class="stat-label">已通过</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 申请列表 -->
|
||||
<view class="applications-list" v-if="filteredList.length > 0">
|
||||
<view
|
||||
class="application-item"
|
||||
v-for="item in filteredList"
|
||||
:key="item.id"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<view class="item-image">
|
||||
<image class="pet-image" :src="item.petPhoto || '/static/default-pet.png'" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<view class="item-header">
|
||||
<view class="pet-name">{{ item.petName }}</view>
|
||||
<view class="status-badge" :class="item.status">
|
||||
<text class="status-text">{{ getStatusText(item.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pet-info">
|
||||
<view class="info-row">
|
||||
<text class="info-text">{{ item.petBreed }} · {{ item.petGender }} · {{ item.petAge }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">发布者:</text>
|
||||
<text class="info-value">{{ item.publisherName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="application-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">申请时间:</text>
|
||||
<text class="info-value">{{ formatTime(item.applicationTime) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="item.responseTime">
|
||||
<text class="info-label">回复时间:</text>
|
||||
<text class="info-value">{{ formatTime(item.responseTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="application-message" v-if="item.message">
|
||||
<view class="message-label">申请留言:</view>
|
||||
<view class="message-content">{{ item.message }}</view>
|
||||
</view>
|
||||
<view class="response-message" v-if="item.responseMessage">
|
||||
<view class="message-label">回复消息:</view>
|
||||
<view class="message-content">{{ item.responseMessage }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-actions">
|
||||
<view class="action-button" @click.stop="showActions(item)">
|
||||
<text class="action-icon">⋯</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<view class="empty-icon">📋</view>
|
||||
<view class="empty-text">{{ getEmptyText() }}</view>
|
||||
<view class="empty-action" @click="browseAdoptions" v-if="currentFilter === 'all'">
|
||||
<text class="action-text">去看看可领养的宠物</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<u-action-sheet
|
||||
:show="showActionSheet"
|
||||
:actions="actionList"
|
||||
@close="showActionSheet = false"
|
||||
@select="onActionSelect"
|
||||
></u-action-sheet>
|
||||
|
||||
<!-- 申请详情弹窗 -->
|
||||
<u-modal
|
||||
:show="showDetailModal"
|
||||
:title="currentItem?.petName || '申请详情'"
|
||||
@confirm="closeDetailModal"
|
||||
@cancel="closeDetailModal"
|
||||
:show-cancel-button="false"
|
||||
confirm-text="关闭"
|
||||
>
|
||||
<view class="detail-content" v-if="currentItem">
|
||||
<view class="detail-section">
|
||||
<text class="section-title">宠物信息</text>
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">名称:</text>
|
||||
<text class="detail-value">{{ currentItem.petName }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">品种:</text>
|
||||
<text class="detail-value">{{ currentItem.petBreed }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">性别:</text>
|
||||
<text class="detail-value">{{ currentItem.petGender }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">年龄:</text>
|
||||
<text class="detail-value">{{ currentItem.petAge }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-section">
|
||||
<text class="section-title">申请信息</text>
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">状态:</text>
|
||||
<text class="detail-value" :class="currentItem.status">{{ getStatusText(currentItem.status) }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-label">申请时间:</text>
|
||||
<text class="detail-value">{{ formatDateTime(currentItem.applicationTime) }}</text>
|
||||
</view>
|
||||
<view class="detail-row" v-if="currentItem.responseTime">
|
||||
<text class="detail-label">回复时间:</text>
|
||||
<text class="detail-value">{{ formatDateTime(currentItem.responseTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-section" v-if="currentItem.message">
|
||||
<text class="section-title">申请留言</text>
|
||||
<view class="message-box">{{ currentItem.message }}</view>
|
||||
</view>
|
||||
<view class="detail-section" v-if="currentItem.responseMessage">
|
||||
<text class="section-title">回复消息</text>
|
||||
<view class="message-box response">{{ currentItem.responseMessage }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-modal>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref, onMounted, computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'MyApplicationsPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const applicationsList = ref([])
|
||||
const currentFilter = ref('all')
|
||||
const showActionSheet = ref(false)
|
||||
const showDetailModal = ref(false)
|
||||
const currentItem = ref(null)
|
||||
|
||||
const actionList = ref([
|
||||
{ name: '查看详情', value: 'detail' },
|
||||
{ name: '联系发布者', value: 'contact' },
|
||||
{ name: '撤销申请', value: 'cancel', color: '#FF5722' }
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const totalCount = computed(() => applicationsList.value.length)
|
||||
const pendingCount = computed(() => applicationsList.value.filter(item => item.status === 'pending').length)
|
||||
const approvedCount = computed(() => applicationsList.value.filter(item => item.status === 'approved').length)
|
||||
|
||||
const filteredList = computed(() => {
|
||||
if (currentFilter.value === 'all') {
|
||||
return applicationsList.value
|
||||
}
|
||||
return applicationsList.value.filter(item => item.status === currentFilter.value)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadApplicationsList()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadApplicationsList = () => {
|
||||
try {
|
||||
const savedList = uni.getStorageSync('myAdoptionApplications') || []
|
||||
|
||||
// 如果没有数据,创建模拟数据
|
||||
if (savedList.length === 0) {
|
||||
const mockData = [
|
||||
{
|
||||
id: 'app_1',
|
||||
petName: '小白',
|
||||
petBreed: '英短',
|
||||
petGender: '母',
|
||||
petAge: '1岁',
|
||||
petPhoto: '/static/mock-cat2.jpg',
|
||||
publisherName: '爱心铲屎官',
|
||||
status: 'pending',
|
||||
applicationTime: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
message: '我很喜欢猫咪,有3年的养猫经验,家里环境很适合养宠物,希望能给小白一个温暖的家。',
|
||||
responseTime: null,
|
||||
responseMessage: null
|
||||
},
|
||||
{
|
||||
id: 'app_2',
|
||||
petName: '小黑',
|
||||
petBreed: '田园犬',
|
||||
petGender: '公',
|
||||
petAge: '2岁',
|
||||
petPhoto: '/static/mock-dog1.jpg',
|
||||
publisherName: '宠物救助站',
|
||||
status: 'approved',
|
||||
applicationTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
message: '我想领养小黑,我有充足的时间照顾它,也有养狗的经验。',
|
||||
responseTime: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
responseMessage: '感谢您的申请!经过审核,我们认为您很适合领养小黑。请联系我们安排见面。'
|
||||
},
|
||||
{
|
||||
id: 'app_3',
|
||||
petName: '小花',
|
||||
petBreed: '三花猫',
|
||||
petGender: '母',
|
||||
petAge: '6个月',
|
||||
petPhoto: '/static/mock-cat3.jpg',
|
||||
publisherName: '个人用户',
|
||||
status: 'rejected',
|
||||
applicationTime: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
message: '希望能领养小花,我会好好照顾它的。',
|
||||
responseTime: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
responseMessage: '很抱歉,我们已经为小花找到了合适的家庭。感谢您的关注!'
|
||||
}
|
||||
]
|
||||
|
||||
uni.setStorageSync('myAdoptionApplications', mockData)
|
||||
applicationsList.value = mockData
|
||||
} else {
|
||||
applicationsList.value = savedList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载申请列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveApplicationsList = () => {
|
||||
uni.setStorageSync('myAdoptionApplications', applicationsList.value)
|
||||
}
|
||||
|
||||
const setFilter = (filter) => {
|
||||
currentFilter.value = filter
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'pending': '待审核',
|
||||
'approved': '已通过',
|
||||
'rejected': '已拒绝',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
const getEmptyText = () => {
|
||||
switch (currentFilter.value) {
|
||||
case 'pending':
|
||||
return '暂无待审核的申请'
|
||||
case 'approved':
|
||||
return '暂无已通过的申请'
|
||||
default:
|
||||
return '还没有申请过领养'
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timeString) => {
|
||||
if (!timeString) return ''
|
||||
const date = new Date(timeString)
|
||||
const now = new Date()
|
||||
const diffTime = now - date
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) {
|
||||
return '今天'
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天'
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}天前`
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateTime = (timeString) => {
|
||||
if (!timeString) return ''
|
||||
const date = new Date(timeString)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const viewDetail = (item) => {
|
||||
currentItem.value = item
|
||||
showDetailModal.value = true
|
||||
}
|
||||
|
||||
const closeDetailModal = () => {
|
||||
showDetailModal.value = false
|
||||
currentItem.value = null
|
||||
}
|
||||
|
||||
const showActions = (item) => {
|
||||
currentItem.value = item
|
||||
|
||||
// 根据状态调整操作菜单
|
||||
if (item.status === 'pending') {
|
||||
actionList.value = [
|
||||
{ name: '查看详情', value: 'detail' },
|
||||
{ name: '联系发布者', value: 'contact' },
|
||||
{ name: '撤销申请', value: 'cancel', color: '#FF5722' }
|
||||
]
|
||||
} else {
|
||||
actionList.value = [
|
||||
{ name: '查看详情', value: 'detail' },
|
||||
{ name: '联系发布者', value: 'contact' }
|
||||
]
|
||||
}
|
||||
|
||||
showActionSheet.value = true
|
||||
}
|
||||
|
||||
const onActionSelect = (action) => {
|
||||
switch (action.value) {
|
||||
case 'detail':
|
||||
viewDetail(currentItem.value)
|
||||
break
|
||||
case 'contact':
|
||||
contactPublisher(currentItem.value)
|
||||
break
|
||||
case 'cancel':
|
||||
cancelApplication(currentItem.value)
|
||||
break
|
||||
}
|
||||
showActionSheet.value = false
|
||||
}
|
||||
|
||||
const contactPublisher = (item) => {
|
||||
uni.showModal({
|
||||
title: '联系发布者',
|
||||
content: `发布者:${item.publisherName}\n\n您可以通过以下方式联系:\n• 站内私信\n• 电话联系\n• 微信联系`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
const cancelApplication = (item) => {
|
||||
uni.showModal({
|
||||
title: '撤销申请',
|
||||
content: '确定要撤销这个领养申请吗?撤销后无法恢复。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
item.status = 'cancelled'
|
||||
saveApplicationsList()
|
||||
|
||||
uni.showToast({
|
||||
title: '申请已撤销',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const browseAdoptions = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/adoption/adoption',
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
applicationsList,
|
||||
currentFilter,
|
||||
showActionSheet,
|
||||
showDetailModal,
|
||||
currentItem,
|
||||
actionList,
|
||||
totalCount,
|
||||
pendingCount,
|
||||
approvedCount,
|
||||
filteredList,
|
||||
setFilter,
|
||||
getStatusText,
|
||||
getEmptyText,
|
||||
formatTime,
|
||||
formatDateTime,
|
||||
viewDetail,
|
||||
closeDetailModal,
|
||||
showActions,
|
||||
onActionSelect,
|
||||
browseAdoptions
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-applications-container {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 0 30rpx 24rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 24rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
|
||||
|
||||
.stat-number,
|
||||
.stat-label {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #FF8A80;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 申请列表 */
|
||||
.applications-list {
|
||||
margin: 0 30rpx;
|
||||
|
||||
.application-item {
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.1);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.item-image {
|
||||
margin-right: 20rpx;
|
||||
|
||||
.pet-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.pet-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 20rpx;
|
||||
|
||||
&.pending {
|
||||
background: #FFF3E0;
|
||||
color: #FF9800;
|
||||
border: 1rpx solid #FFE0B2;
|
||||
}
|
||||
|
||||
&.approved {
|
||||
background: #E8F5E8;
|
||||
color: #4CAF50;
|
||||
border: 1rpx solid #C8E6C9;
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
background: #FFEBEE;
|
||||
color: #F44336;
|
||||
border: 1rpx solid #FFCDD2;
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
background: #F5F5F5;
|
||||
color: #757575;
|
||||
border: 1rpx solid #E0E0E0;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pet-info {
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: 6rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application-info {
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: 6rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application-message,
|
||||
.response-message {
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-label {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
padding: 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.response-message {
|
||||
.message-content {
|
||||
background: rgba(76, 175, 80, 0.05);
|
||||
border-left: 4rpx solid #4CAF50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.action-button {
|
||||
padding: 8rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
|
||||
.action-icon {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80rpx 40rpx;
|
||||
margin: 0 30rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
margin-bottom: 32rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
display: inline-block;
|
||||
padding: 16rpx 32rpx;
|
||||
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.action-text {
|
||||
font-size: 26rpx;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 详情弹窗 */
|
||||
.detail-content {
|
||||
padding: 20rpx 0;
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 16rpx;
|
||||
padding-bottom: 8rpx;
|
||||
border-bottom: 2rpx solid rgba(255, 138, 128, 0.2);
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
width: 120rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
flex: 1;
|
||||
|
||||
&.pending {
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
&.approved {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
color: #757575;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-box {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
padding: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
|
||||
&.response {
|
||||
background: rgba(76, 175, 80, 0.05);
|
||||
border-left: 4rpx solid #4CAF50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.my-applications-container {
|
||||
.stats-card,
|
||||
.applications-list,
|
||||
.empty-state {
|
||||
margin-left: 20rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
gap: 16rpx;
|
||||
|
||||
.stat-item {
|
||||
padding: 20rpx 12rpx;
|
||||
|
||||
.stat-number {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application-item {
|
||||
padding: 20rpx;
|
||||
|
||||
.item-image {
|
||||
margin-right: 16rpx;
|
||||
|
||||
.pet-image {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.my-applications-container > view {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.application-item {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 状态过渡动画 */
|
||||
.status-badge {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.action-button:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,852 @@
|
|||
<template>
|
||||
<view class="my-published-container">
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats-card">
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item" :class="{ active: currentFilter === 'all' }" @click="setFilter('all')">
|
||||
<view class="stat-number">{{ totalCount }}</view>
|
||||
<view class="stat-label">全部</view>
|
||||
</view>
|
||||
<view class="stat-item" :class="{ active: currentFilter === 'active' }" @click="setFilter('active')">
|
||||
<view class="stat-number">{{ activeCount }}</view>
|
||||
<view class="stat-label">招募中</view>
|
||||
</view>
|
||||
<view class="stat-item" :class="{ active: currentFilter === 'completed' }" @click="setFilter('completed')">
|
||||
<view class="stat-number">{{ completedCount }}</view>
|
||||
<view class="stat-label">已完成</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 发布列表 -->
|
||||
<view class="published-list" v-if="filteredList.length > 0">
|
||||
<view
|
||||
class="published-item"
|
||||
v-for="item in filteredList"
|
||||
:key="item.id"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<view class="item-image">
|
||||
<image class="pet-image" :src="item.photos[0] || '/static/default-pet.png'" mode="aspectFill" />
|
||||
<view class="status-badge" :class="item.status">
|
||||
<text class="status-text">{{ getStatusText(item.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<view class="item-header">
|
||||
<view class="pet-name">{{ item.name }}</view>
|
||||
<view class="item-actions" @click.stop="showActions(item)">
|
||||
<text class="action-icon">⋯</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pet-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">品种:</text>
|
||||
<text class="info-value">{{ item.breed }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">性别:</text>
|
||||
<text class="info-value">{{ item.gender }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">年龄:</text>
|
||||
<text class="info-value">{{ item.age }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-stats">
|
||||
<view class="stat-group">
|
||||
<text class="stat-icon">👁️</text>
|
||||
<text class="stat-text">{{ item.viewCount || 0 }} 浏览</text>
|
||||
</view>
|
||||
<view class="stat-group">
|
||||
<text class="stat-icon">📝</text>
|
||||
<text class="stat-text">{{ item.applicationCount || 0 }} 申请</text>
|
||||
</view>
|
||||
<view class="stat-group">
|
||||
<text class="stat-icon">🕐</text>
|
||||
<text class="stat-text">{{ formatTime(item.publishTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-description" v-if="item.description">
|
||||
{{ item.description }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-else>
|
||||
<view class="empty-icon">📝</view>
|
||||
<view class="empty-text">{{ getEmptyText() }}</view>
|
||||
<view class="empty-action" @click="publishNew" v-if="currentFilter === 'all'">
|
||||
<text class="action-text">发布第一个领养信息</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 浮动发布按钮 -->
|
||||
<view class="fab-button" @click="publishNew">
|
||||
<text class="fab-icon">+</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<u-action-sheet
|
||||
:show="showActionSheet"
|
||||
:actions="actionList"
|
||||
@close="showActionSheet = false"
|
||||
@select="onActionSelect"
|
||||
></u-action-sheet>
|
||||
|
||||
<!-- 编辑状态弹窗 -->
|
||||
<u-modal
|
||||
:show="showStatusModal"
|
||||
title="修改状态"
|
||||
@confirm="confirmStatusChange"
|
||||
@cancel="showStatusModal = false"
|
||||
>
|
||||
<view class="status-options">
|
||||
<view
|
||||
class="status-option"
|
||||
v-for="status in statusOptions"
|
||||
:key="status.value"
|
||||
:class="{ active: selectedStatus === status.value }"
|
||||
@click="selectStatus(status.value)"
|
||||
>
|
||||
<view class="option-icon" :class="status.value">
|
||||
<text class="icon-text">{{ status.icon }}</text>
|
||||
</view>
|
||||
<view class="option-info">
|
||||
<text class="option-title">{{ status.label }}</text>
|
||||
<text class="option-desc">{{ status.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-modal>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref, onMounted, computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'MyPublishedPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const publishedList = ref([])
|
||||
const currentFilter = ref('all')
|
||||
const showActionSheet = ref(false)
|
||||
const showStatusModal = ref(false)
|
||||
const currentItem = ref(null)
|
||||
const selectedStatus = ref('')
|
||||
|
||||
const actionList = ref([
|
||||
{ name: '查看详情', value: 'detail' },
|
||||
{ name: '查看申请', value: 'applications' },
|
||||
{ name: '编辑信息', value: 'edit' },
|
||||
{ name: '修改状态', value: 'status' },
|
||||
{ name: '删除', value: 'delete', color: '#FF5722' }
|
||||
])
|
||||
|
||||
const statusOptions = ref([
|
||||
{
|
||||
value: 'active',
|
||||
label: '招募中',
|
||||
desc: '正在寻找领养家庭',
|
||||
icon: '🟢'
|
||||
},
|
||||
{
|
||||
value: 'paused',
|
||||
label: '已暂停',
|
||||
desc: '暂时停止接收申请',
|
||||
icon: '🟡'
|
||||
},
|
||||
{
|
||||
value: 'completed',
|
||||
label: '已完成',
|
||||
desc: '已找到合适的领养家庭',
|
||||
icon: '🔵'
|
||||
},
|
||||
{
|
||||
value: 'cancelled',
|
||||
label: '已取消',
|
||||
desc: '取消领养计划',
|
||||
icon: '🔴'
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const totalCount = computed(() => publishedList.value.length)
|
||||
const activeCount = computed(() => publishedList.value.filter(item => item.status === 'active').length)
|
||||
const completedCount = computed(() => publishedList.value.filter(item => item.status === 'completed').length)
|
||||
|
||||
const filteredList = computed(() => {
|
||||
if (currentFilter.value === 'all') {
|
||||
return publishedList.value
|
||||
}
|
||||
return publishedList.value.filter(item => item.status === currentFilter.value)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadPublishedList()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadPublishedList = () => {
|
||||
try {
|
||||
const savedList = uni.getStorageSync('myPublishedAdoptions') || []
|
||||
|
||||
// 如果没有数据,创建模拟数据
|
||||
if (savedList.length === 0) {
|
||||
const mockData = [
|
||||
{
|
||||
id: 'pub_1',
|
||||
name: '小橘',
|
||||
breed: '橘猫',
|
||||
gender: '公',
|
||||
age: '2岁',
|
||||
photos: ['/static/mock-cat1.jpg'],
|
||||
status: 'active',
|
||||
description: '性格温顺,喜欢和人亲近,已绝育,疫苗齐全。',
|
||||
publishTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
applicationCount: 3,
|
||||
viewCount: 25
|
||||
},
|
||||
{
|
||||
id: 'pub_2',
|
||||
name: '小白',
|
||||
breed: '英短',
|
||||
gender: '母',
|
||||
age: '1岁',
|
||||
photos: ['/static/mock-cat2.jpg'],
|
||||
status: 'completed',
|
||||
description: '活泼可爱,适合有经验的铲屎官。',
|
||||
publishTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
applicationCount: 8,
|
||||
viewCount: 45
|
||||
}
|
||||
]
|
||||
|
||||
uni.setStorageSync('myPublishedAdoptions', mockData)
|
||||
publishedList.value = mockData
|
||||
} else {
|
||||
publishedList.value = savedList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载发布列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const savePublishedList = () => {
|
||||
uni.setStorageSync('myPublishedAdoptions', publishedList.value)
|
||||
}
|
||||
|
||||
const setFilter = (filter) => {
|
||||
currentFilter.value = filter
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'active': '招募中',
|
||||
'paused': '已暂停',
|
||||
'completed': '已完成',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
const getEmptyText = () => {
|
||||
switch (currentFilter.value) {
|
||||
case 'active':
|
||||
return '暂无招募中的领养信息'
|
||||
case 'completed':
|
||||
return '暂无已完成的领养信息'
|
||||
default:
|
||||
return '还没有发布过领养信息'
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timeString) => {
|
||||
if (!timeString) return ''
|
||||
const date = new Date(timeString)
|
||||
const now = new Date()
|
||||
const diffTime = now - date
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) {
|
||||
return '今天'
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天'
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}天前`
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
}
|
||||
|
||||
const viewDetail = (item) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/adoption/adoption-detail?id=${item.id}&from=published`,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showActions = (item) => {
|
||||
currentItem.value = item
|
||||
showActionSheet.value = true
|
||||
}
|
||||
|
||||
const onActionSelect = (action) => {
|
||||
switch (action.value) {
|
||||
case 'detail':
|
||||
viewDetail(currentItem.value)
|
||||
break
|
||||
case 'applications':
|
||||
viewApplications(currentItem.value)
|
||||
break
|
||||
case 'edit':
|
||||
editItem(currentItem.value)
|
||||
break
|
||||
case 'status':
|
||||
changeStatus(currentItem.value)
|
||||
break
|
||||
case 'delete':
|
||||
deleteItem(currentItem.value)
|
||||
break
|
||||
}
|
||||
showActionSheet.value = false
|
||||
}
|
||||
|
||||
const viewApplications = (item) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/adoption/applications?publishId=${item.id}`,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const editItem = (item) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/adoption/publish?id=${item.id}&mode=edit`,
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const changeStatus = (item) => {
|
||||
currentItem.value = item
|
||||
selectedStatus.value = item.status
|
||||
showStatusModal.value = true
|
||||
}
|
||||
|
||||
const selectStatus = (status) => {
|
||||
selectedStatus.value = status
|
||||
}
|
||||
|
||||
const confirmStatusChange = () => {
|
||||
if (currentItem.value && selectedStatus.value !== currentItem.value.status) {
|
||||
currentItem.value.status = selectedStatus.value
|
||||
savePublishedList()
|
||||
|
||||
uni.showToast({
|
||||
title: '状态已更新',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
showStatusModal.value = false
|
||||
}
|
||||
|
||||
const deleteItem = (item) => {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '删除后无法恢复,确定要删除这个领养信息吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const index = publishedList.value.findIndex(p => p.id === item.id)
|
||||
if (index > -1) {
|
||||
publishedList.value.splice(index, 1)
|
||||
savePublishedList()
|
||||
uni.showToast({
|
||||
title: '已删除',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const publishNew = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/adoption/publish',
|
||||
fail: () => {
|
||||
uni.showToast({
|
||||
title: '页面开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
publishedList,
|
||||
currentFilter,
|
||||
showActionSheet,
|
||||
showStatusModal,
|
||||
selectedStatus,
|
||||
actionList,
|
||||
statusOptions,
|
||||
totalCount,
|
||||
activeCount,
|
||||
completedCount,
|
||||
filteredList,
|
||||
setFilter,
|
||||
getStatusText,
|
||||
getEmptyText,
|
||||
formatTime,
|
||||
viewDetail,
|
||||
showActions,
|
||||
onActionSelect,
|
||||
selectStatus,
|
||||
confirmStatusChange,
|
||||
publishNew
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-published-container {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 0 30rpx 24rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 24rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
|
||||
|
||||
.stat-number,
|
||||
.stat-label {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #FF8A80;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 发布列表 */
|
||||
.published-list {
|
||||
margin: 0 30rpx;
|
||||
|
||||
.published-item {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.1);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.item-image {
|
||||
position: relative;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.pet-image {
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
right: 16rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 20rpx;
|
||||
|
||||
&.active {
|
||||
background: rgba(76, 175, 80, 0.9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.paused {
|
||||
background: rgba(255, 152, 0, 0.9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: rgba(33, 150, 243, 0.9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
background: rgba(244, 67, 54, 0.9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.pet-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
padding: 8rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
|
||||
.action-icon {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pet-info {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
width: 80rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-stats {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.stat-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stat-icon {
|
||||
font-size: 20rpx;
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
|
||||
.stat-text {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-description {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
padding: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80rpx 40rpx;
|
||||
margin: 0 30rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
margin-bottom: 32rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.empty-action {
|
||||
display: inline-block;
|
||||
padding: 16rpx 32rpx;
|
||||
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.action-text {
|
||||
font-size: 26rpx;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 浮动发布按钮 */
|
||||
.fab-button {
|
||||
position: fixed;
|
||||
bottom: 120rpx;
|
||||
right: 40rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 138, 128, 0.4);
|
||||
z-index: 100;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 48rpx;
|
||||
color: white;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
/* 状态选择弹窗 */
|
||||
.status-options {
|
||||
padding: 20rpx 0;
|
||||
|
||||
.status-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
border-color: #FF8A80;
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
|
||||
&.active {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
&.paused {
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.option-info {
|
||||
flex: 1;
|
||||
|
||||
.option-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.my-published-container {
|
||||
.stats-card,
|
||||
.published-list,
|
||||
.empty-state {
|
||||
margin-left: 20rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
gap: 16rpx;
|
||||
|
||||
.stat-item {
|
||||
padding: 20rpx 12rpx;
|
||||
|
||||
.stat-number {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.published-item {
|
||||
padding: 20rpx;
|
||||
|
||||
.item-image {
|
||||
.pet-image {
|
||||
height: 250rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fab-button {
|
||||
right: 30rpx;
|
||||
bottom: 100rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
|
||||
.fab-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.my-published-container > view {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.published-item {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.fab-button {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* 状态过渡动画 */
|
||||
.status-badge {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.item-actions:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,908 @@
|
|||
<template>
|
||||
<view class="family-container">
|
||||
<!-- 家庭概览卡片 -->
|
||||
<view class="family-overview-card">
|
||||
<view class="overview-header">
|
||||
<view class="family-info">
|
||||
<text class="family-name">{{ familyData.name || '我的家庭' }}</text>
|
||||
<text class="family-desc">{{ familyData.members.length }}位成员 · 共同照顾{{ petCount }}只宠物</text>
|
||||
</view>
|
||||
<view class="family-avatar">
|
||||
<text class="avatar-icon">👨👩👧👦</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-stats">
|
||||
<view class="stat-item">
|
||||
<view class="stat-number">{{ familyData.members.length }}</view>
|
||||
<view class="stat-label">家庭成员</view>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-number">{{ sharedPets }}</view>
|
||||
<view class="stat-label">共享宠物</view>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-number">{{ todayRecords }}</view>
|
||||
<view class="stat-label">今日记录</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 家庭成员卡片 -->
|
||||
<view class="members-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">家庭成员</text>
|
||||
<view class="header-action" @click="inviteMember">
|
||||
<text class="action-icon">➕</text>
|
||||
<text class="action-text">邀请</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="members-list">
|
||||
<view
|
||||
class="member-item"
|
||||
v-for="member in familyData.members"
|
||||
:key="member.id"
|
||||
@click="viewMemberDetail(member)"
|
||||
>
|
||||
<view class="member-avatar">
|
||||
<u-avatar
|
||||
:src="member.avatarUrl || ''"
|
||||
:text="member.nickName ? member.nickName.charAt(0) : '👤'"
|
||||
size="80"
|
||||
shape="circle"
|
||||
bg-color="linear-gradient(135deg, #FF8A80, #FFB6C1)"
|
||||
color="white"
|
||||
font-size="32"
|
||||
></u-avatar>
|
||||
<view class="member-role-badge" :class="member.role">
|
||||
<text class="role-text">{{ getRoleText(member.role) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-info">
|
||||
<view class="member-name">{{ member.nickName }}</view>
|
||||
<view class="member-status">
|
||||
<view class="status-dot" :class="{ online: member.isOnline }"></view>
|
||||
<text class="status-text">{{ member.isOnline ? '在线' : '离线' }}</text>
|
||||
</view>
|
||||
<view class="member-stats">
|
||||
<text class="stats-text">本月记录 {{ member.monthlyRecords || 0 }} 条</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-action">
|
||||
<text class="action-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 权限设置卡片 -->
|
||||
<view class="permissions-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">权限设置</text>
|
||||
</view>
|
||||
<view class="permissions-list">
|
||||
<view class="permission-item">
|
||||
<view class="permission-info">
|
||||
<text class="permission-title">宠物管理权限</text>
|
||||
<text class="permission-desc">允许成员添加、编辑宠物信息</text>
|
||||
</view>
|
||||
<view class="permission-switch">
|
||||
<u-switch
|
||||
v-model="permissions.petManagement"
|
||||
@change="onPermissionChange('petManagement', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="permission-item">
|
||||
<view class="permission-info">
|
||||
<text class="permission-title">记录管理权限</text>
|
||||
<text class="permission-desc">允许成员添加、编辑宠物记录</text>
|
||||
</view>
|
||||
<view class="permission-switch">
|
||||
<u-switch
|
||||
v-model="permissions.recordManagement"
|
||||
@change="onPermissionChange('recordManagement', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="permission-item">
|
||||
<view class="permission-info">
|
||||
<text class="permission-title">成员管理权限</text>
|
||||
<text class="permission-desc">允许成员邀请和管理其他成员</text>
|
||||
</view>
|
||||
<view class="permission-switch">
|
||||
<u-switch
|
||||
v-model="permissions.memberManagement"
|
||||
@change="onPermissionChange('memberManagement', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 家庭设置卡片 -->
|
||||
<view class="settings-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">家庭设置</text>
|
||||
</view>
|
||||
<view class="settings-list">
|
||||
<view class="setting-item" @click="editFamilyName">
|
||||
<view class="setting-left">
|
||||
<text class="setting-icon">✏️</text>
|
||||
<text class="setting-text">修改家庭名称</text>
|
||||
</view>
|
||||
<view class="setting-right">
|
||||
<text class="setting-value">{{ familyData.name || '我的家庭' }}</text>
|
||||
<text class="setting-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item" @click="viewInviteCode">
|
||||
<view class="setting-left">
|
||||
<text class="setting-icon">🔗</text>
|
||||
<text class="setting-text">邀请码管理</text>
|
||||
</view>
|
||||
<view class="setting-right">
|
||||
<text class="setting-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item" @click="viewFamilyHistory">
|
||||
<view class="setting-left">
|
||||
<text class="setting-icon">📊</text>
|
||||
<text class="setting-text">家庭统计</text>
|
||||
</view>
|
||||
<view class="setting-right">
|
||||
<text class="setting-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item danger" @click="leaveFamilyConfirm">
|
||||
<view class="setting-left">
|
||||
<text class="setting-icon">🚪</text>
|
||||
<text class="setting-text">退出家庭</text>
|
||||
</view>
|
||||
<view class="setting-right">
|
||||
<text class="setting-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 邀请成员弹窗 -->
|
||||
<u-modal
|
||||
:show="showInviteModal"
|
||||
title="邀请家庭成员"
|
||||
@confirm="confirmInvite"
|
||||
@cancel="showInviteModal = false"
|
||||
>
|
||||
<view class="invite-content">
|
||||
<view class="invite-method">
|
||||
<text class="method-title">邀请方式</text>
|
||||
<view class="method-options">
|
||||
<view class="method-option" @click="shareInviteCode">
|
||||
<text class="option-icon">📱</text>
|
||||
<text class="option-text">分享邀请码</text>
|
||||
</view>
|
||||
<view class="method-option" @click="shareQRCode">
|
||||
<text class="option-icon">📷</text>
|
||||
<text class="option-text">分享二维码</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="invite-code">
|
||||
<text class="code-title">邀请码</text>
|
||||
<view class="code-display">
|
||||
<text class="code-text">{{ inviteCode }}</text>
|
||||
<view class="code-copy" @click="copyInviteCode">
|
||||
<text class="copy-text">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-modal>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref, onMounted, computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'FamilyPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const familyData = reactive({
|
||||
id: '',
|
||||
name: '我的家庭',
|
||||
members: []
|
||||
})
|
||||
|
||||
const permissions = reactive({
|
||||
petManagement: true,
|
||||
recordManagement: true,
|
||||
memberManagement: false
|
||||
})
|
||||
|
||||
const showInviteModal = ref(false)
|
||||
const inviteCode = ref('')
|
||||
const petCount = ref(0)
|
||||
const sharedPets = ref(0)
|
||||
const todayRecords = ref(0)
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadFamilyData()
|
||||
generateInviteCode()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadFamilyData = () => {
|
||||
try {
|
||||
// 加载家庭数据
|
||||
const savedFamilyData = uni.getStorageSync('familyData') || {
|
||||
id: 'family_' + Date.now(),
|
||||
name: '我的家庭',
|
||||
members: []
|
||||
}
|
||||
|
||||
// 如果没有成员,添加当前用户作为家庭主人
|
||||
if (savedFamilyData.members.length === 0) {
|
||||
const userInfo = uni.getStorageSync('userInfo') || {}
|
||||
savedFamilyData.members.push({
|
||||
id: 'user_' + Date.now(),
|
||||
nickName: userInfo.nickName || '我',
|
||||
avatarUrl: userInfo.avatarUrl || '',
|
||||
role: 'owner',
|
||||
isOnline: true,
|
||||
joinTime: new Date().toISOString(),
|
||||
monthlyRecords: 15
|
||||
})
|
||||
}
|
||||
|
||||
Object.assign(familyData, savedFamilyData)
|
||||
|
||||
// 加载权限设置
|
||||
const savedPermissions = uni.getStorageSync('familyPermissions') || permissions
|
||||
Object.assign(permissions, savedPermissions)
|
||||
|
||||
// 加载统计数据
|
||||
loadFamilyStats()
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载家庭数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadFamilyStats = () => {
|
||||
// 加载宠物数量
|
||||
const pets = uni.getStorageSync('pets') || []
|
||||
petCount.value = pets.length
|
||||
sharedPets.value = pets.filter(pet => pet.shared).length || pets.length
|
||||
|
||||
// 计算今日记录数
|
||||
const today = new Date().toDateString()
|
||||
let todayCount = 0
|
||||
pets.forEach(pet => {
|
||||
const records = uni.getStorageSync(`petRecords_${pet.id}`) || []
|
||||
todayCount += records.filter(record =>
|
||||
new Date(record.date).toDateString() === today
|
||||
).length
|
||||
})
|
||||
todayRecords.value = todayCount
|
||||
}
|
||||
|
||||
const generateInviteCode = () => {
|
||||
// 生成6位邀请码
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
let code = ''
|
||||
for (let i = 0; i < 6; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
inviteCode.value = code
|
||||
}
|
||||
|
||||
const getRoleText = (role) => {
|
||||
const roleMap = {
|
||||
'owner': '主人',
|
||||
'admin': '管理员',
|
||||
'member': '成员',
|
||||
'viewer': '观察者'
|
||||
}
|
||||
return roleMap[role] || '成员'
|
||||
}
|
||||
|
||||
const inviteMember = () => {
|
||||
showInviteModal.value = true
|
||||
}
|
||||
|
||||
const confirmInvite = () => {
|
||||
shareInviteCode()
|
||||
showInviteModal.value = false
|
||||
}
|
||||
|
||||
const shareInviteCode = () => {
|
||||
const shareText = `邀请您加入我的宠物家庭!\n邀请码:${inviteCode.value}\n快来一起记录我们的宠物生活吧~`
|
||||
|
||||
// 复制到剪贴板
|
||||
uni.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '邀请信息已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const shareQRCode = () => {
|
||||
uni.showToast({
|
||||
title: '二维码功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const copyInviteCode = () => {
|
||||
uni.setClipboardData({
|
||||
data: inviteCode.value,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '邀请码已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewMemberDetail = (member) => {
|
||||
uni.showModal({
|
||||
title: member.nickName,
|
||||
content: `角色:${getRoleText(member.role)}\n加入时间:${new Date(member.joinTime).toLocaleDateString()}\n本月记录:${member.monthlyRecords || 0}条`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
const onPermissionChange = (key, value) => {
|
||||
permissions[key] = value
|
||||
uni.setStorageSync('familyPermissions', permissions)
|
||||
|
||||
uni.showToast({
|
||||
title: '权限设置已更新',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const editFamilyName = () => {
|
||||
uni.showModal({
|
||||
title: '修改家庭名称',
|
||||
editable: true,
|
||||
placeholderText: '请输入家庭名称',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content.trim()) {
|
||||
familyData.name = res.content.trim()
|
||||
saveFamilyData()
|
||||
uni.showToast({
|
||||
title: '家庭名称已更新',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewInviteCode = () => {
|
||||
showInviteModal.value = true
|
||||
}
|
||||
|
||||
const viewFamilyHistory = () => {
|
||||
uni.showToast({
|
||||
title: '家庭统计功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const leaveFamilyConfirm = () => {
|
||||
uni.showModal({
|
||||
title: '确认退出',
|
||||
content: '退出后将无法查看家庭共享的宠物信息,确定要退出吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
leaveFamily()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const leaveFamily = () => {
|
||||
// 清除家庭数据
|
||||
uni.removeStorageSync('familyData')
|
||||
uni.removeStorageSync('familyPermissions')
|
||||
|
||||
uni.showToast({
|
||||
title: '已退出家庭',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const saveFamilyData = () => {
|
||||
uni.setStorageSync('familyData', familyData)
|
||||
}
|
||||
|
||||
return {
|
||||
familyData,
|
||||
permissions,
|
||||
showInviteModal,
|
||||
inviteCode,
|
||||
petCount,
|
||||
sharedPets,
|
||||
todayRecords,
|
||||
getRoleText,
|
||||
inviteMember,
|
||||
confirmInvite,
|
||||
shareInviteCode,
|
||||
shareQRCode,
|
||||
copyInviteCode,
|
||||
viewMemberDetail,
|
||||
onPermissionChange,
|
||||
editFamilyName,
|
||||
viewInviteCode,
|
||||
viewFamilyHistory,
|
||||
leaveFamilyConfirm
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.family-container {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.family-overview-card,
|
||||
.members-card,
|
||||
.permissions-card,
|
||||
.settings-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 0 30rpx 24rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 20rpx;
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
border-radius: 20rpx;
|
||||
|
||||
.action-icon {
|
||||
font-size: 24rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 家庭概览卡片 */
|
||||
.family-overview-card {
|
||||
.overview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.family-info {
|
||||
flex: 1;
|
||||
|
||||
.family-name {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.family-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.family-avatar {
|
||||
.avatar-icon {
|
||||
font-size: 60rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 24rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
|
||||
.stat-number {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #FF8A80;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 家庭成员卡片 */
|
||||
.members-list {
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
position: relative;
|
||||
margin-right: 24rpx;
|
||||
|
||||
.member-role-badge {
|
||||
position: absolute;
|
||||
bottom: -4rpx;
|
||||
right: -4rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 18rpx;
|
||||
|
||||
&.owner {
|
||||
background: #FFD700;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
&.admin {
|
||||
background: #FF8A80;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.member {
|
||||
background: #81C784;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.viewer {
|
||||
background: #CCCCCC;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.role-text {
|
||||
font-size: 18rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.member-info {
|
||||
flex: 1;
|
||||
|
||||
.member-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.member-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4rpx;
|
||||
|
||||
.status-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #CCCCCC;
|
||||
margin-right: 8rpx;
|
||||
|
||||
&.online {
|
||||
background: #4CAF50;
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.member-stats {
|
||||
.stats-text {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.member-action {
|
||||
.action-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 权限设置卡片 */
|
||||
.permissions-list {
|
||||
.permission-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.permission-info {
|
||||
flex: 1;
|
||||
|
||||
.permission-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.permission-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-switch {
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 家庭设置卡片 */
|
||||
.settings-list {
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 32rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
.setting-text {
|
||||
color: #FF5722;
|
||||
}
|
||||
|
||||
.setting-arrow {
|
||||
color: #FF5722;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.setting-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.setting-text {
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.setting-value {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.setting-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 邀请弹窗样式 */
|
||||
.invite-content {
|
||||
padding: 20rpx 0;
|
||||
|
||||
.invite-method {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.method-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.method-options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16rpx;
|
||||
|
||||
.method-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-code {
|
||||
.code-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.code-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx dashed #FF8A80;
|
||||
|
||||
.code-text {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #FF8A80;
|
||||
text-align: center;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.code-copy {
|
||||
padding: 8rpx 16rpx;
|
||||
background: #FF8A80;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.copy-text {
|
||||
font-size: 22rpx;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.family-container {
|
||||
.family-overview-card,
|
||||
.members-card,
|
||||
.permissions-card,
|
||||
.settings-card {
|
||||
margin: 0 20rpx 20rpx 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.overview-stats {
|
||||
gap: 16rpx;
|
||||
|
||||
.stat-item {
|
||||
padding: 20rpx 12rpx;
|
||||
|
||||
.stat-number {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.family-container > view {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.family-container > view:nth-child(1) { animation-delay: 0.1s; }
|
||||
.family-container > view:nth-child(2) { animation-delay: 0.2s; }
|
||||
.family-container > view:nth-child(3) { animation-delay: 0.3s; }
|
||||
.family-container > view:nth-child(4) { animation-delay: 0.4s; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,994 @@
|
|||
<template>
|
||||
<view class="feedback-container">
|
||||
<!-- 反馈表单卡片 -->
|
||||
<view class="feedback-form-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">意见反馈</text>
|
||||
<text class="card-desc">您的建议是我们改进的动力</text>
|
||||
</view>
|
||||
<view class="form-content">
|
||||
<view class="form-group">
|
||||
<text class="form-label">反馈类型 *</text>
|
||||
<view class="type-options">
|
||||
<view
|
||||
class="type-option"
|
||||
v-for="type in feedbackTypes"
|
||||
:key="type.value"
|
||||
:class="{ active: formData.type === type.value }"
|
||||
@click="selectType(type.value)"
|
||||
>
|
||||
<view class="option-icon">{{ type.icon }}</view>
|
||||
<text class="option-text">{{ type.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">问题描述 *</text>
|
||||
<u-textarea
|
||||
v-model="formData.content"
|
||||
placeholder="请详细描述您遇到的问题或建议..."
|
||||
:maxlength="500"
|
||||
count
|
||||
:custom-style="textareaStyle"
|
||||
></u-textarea>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">联系方式</text>
|
||||
<u-input
|
||||
v-model="formData.contact"
|
||||
placeholder="请留下您的联系方式(可选)"
|
||||
border="none"
|
||||
:custom-style="inputStyle"
|
||||
></u-input>
|
||||
</view>
|
||||
<view class="form-group">
|
||||
<text class="form-label">截图上传</text>
|
||||
<view class="upload-area">
|
||||
<view
|
||||
class="upload-item"
|
||||
v-for="(image, index) in formData.images"
|
||||
:key="index"
|
||||
@click="previewImage(index)"
|
||||
>
|
||||
<image class="upload-image" :src="image" mode="aspectFill" />
|
||||
<view class="upload-delete" @click.stop="deleteImage(index)">
|
||||
<text class="delete-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="upload-add"
|
||||
v-if="formData.images.length < 3"
|
||||
@click="uploadImage"
|
||||
>
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加截图</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="submit-button">
|
||||
<u-button
|
||||
type="primary"
|
||||
:custom-style="buttonStyle"
|
||||
@click="submitFeedback"
|
||||
:loading="submitting"
|
||||
>
|
||||
{{ submitting ? '提交中...' : '提交反馈' }}
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 历史反馈卡片 -->
|
||||
<view class="history-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">历史反馈</text>
|
||||
<view class="header-action" @click="refreshHistory">
|
||||
<text class="action-text">刷新</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="history-list" v-if="historyList.length > 0">
|
||||
<view
|
||||
class="history-item"
|
||||
v-for="item in historyList"
|
||||
:key="item.id"
|
||||
@click="viewFeedbackDetail(item)"
|
||||
>
|
||||
<view class="item-header">
|
||||
<view class="item-type">
|
||||
<text class="type-icon">{{ getTypeIcon(item.type) }}</text>
|
||||
<text class="type-text">{{ getTypeText(item.type) }}</text>
|
||||
</view>
|
||||
<view class="item-status" :class="item.status">
|
||||
<text class="status-text">{{ getStatusText(item.status) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<text class="content-text">{{ item.content }}</text>
|
||||
</view>
|
||||
<view class="item-footer">
|
||||
<text class="submit-time">{{ formatTime(item.submitTime) }}</text>
|
||||
<text class="item-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-history" v-else>
|
||||
<view class="empty-icon">📝</view>
|
||||
<view class="empty-text">暂无反馈记录</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 常见问题卡片 -->
|
||||
<view class="faq-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">常见问题</text>
|
||||
</view>
|
||||
<view class="faq-list">
|
||||
<view
|
||||
class="faq-item"
|
||||
v-for="faq in faqList"
|
||||
:key="faq.id"
|
||||
@click="toggleFaq(faq)"
|
||||
>
|
||||
<view class="faq-question">
|
||||
<text class="question-text">{{ faq.question }}</text>
|
||||
<text class="question-arrow" :class="{ expanded: faq.expanded }">▼</text>
|
||||
</view>
|
||||
<view class="faq-answer" v-if="faq.expanded">
|
||||
<text class="answer-text">{{ faq.answer }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系我们卡片 -->
|
||||
<view class="contact-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">联系我们</text>
|
||||
</view>
|
||||
<view class="contact-list">
|
||||
<view class="contact-item" @click="copyContact('email')">
|
||||
<view class="contact-info">
|
||||
<text class="contact-icon">📧</text>
|
||||
<text class="contact-text">邮箱:support@petai.com</text>
|
||||
</view>
|
||||
<view class="contact-action">
|
||||
<text class="action-text">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="contact-item" @click="copyContact('qq')">
|
||||
<view class="contact-info">
|
||||
<text class="contact-icon">💬</text>
|
||||
<text class="contact-text">QQ群:123456789</text>
|
||||
</view>
|
||||
<view class="contact-action">
|
||||
<text class="action-text">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="contact-item" @click="copyContact('wechat')">
|
||||
<view class="contact-info">
|
||||
<text class="contact-icon">💚</text>
|
||||
<text class="contact-text">微信:PetAI_Support</text>
|
||||
</view>
|
||||
<view class="contact-action">
|
||||
<text class="action-text">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'FeedbackPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const formData = reactive({
|
||||
type: '',
|
||||
content: '',
|
||||
contact: '',
|
||||
images: []
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
const historyList = ref([])
|
||||
const faqList = ref([])
|
||||
|
||||
const feedbackTypes = ref([
|
||||
{ value: 'bug', label: '功能异常', icon: '🐛' },
|
||||
{ value: 'suggestion', label: '功能建议', icon: '💡' },
|
||||
{ value: 'ui', label: '界面问题', icon: '🎨' },
|
||||
{ value: 'other', label: '其他问题', icon: '❓' }
|
||||
])
|
||||
|
||||
// 样式配置
|
||||
const inputStyle = {
|
||||
fontSize: '28rpx',
|
||||
color: '#333333',
|
||||
backgroundColor: 'rgba(255, 138, 128, 0.05)',
|
||||
borderRadius: '12rpx',
|
||||
padding: '20rpx'
|
||||
}
|
||||
|
||||
const textareaStyle = {
|
||||
fontSize: '26rpx',
|
||||
color: '#333333',
|
||||
backgroundColor: 'rgba(255, 138, 128, 0.05)',
|
||||
borderRadius: '12rpx',
|
||||
padding: '20rpx'
|
||||
}
|
||||
|
||||
const buttonStyle = {
|
||||
background: 'linear-gradient(135deg, #FF8A80, #FFB6C1)',
|
||||
borderRadius: '24rpx',
|
||||
height: '88rpx',
|
||||
fontSize: '32rpx'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadHistory()
|
||||
loadFAQ()
|
||||
loadUserContact()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadHistory = () => {
|
||||
try {
|
||||
const savedHistory = uni.getStorageSync('feedbackHistory') || []
|
||||
|
||||
// 如果没有历史记录,创建一些模拟数据
|
||||
if (savedHistory.length === 0) {
|
||||
const mockHistory = [
|
||||
{
|
||||
id: 'fb_1',
|
||||
type: 'bug',
|
||||
content: '宠物记录页面偶尔会出现加载失败的情况',
|
||||
contact: '',
|
||||
status: 'resolved',
|
||||
submitTime: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
response: '感谢您的反馈,该问题已在最新版本中修复。'
|
||||
},
|
||||
{
|
||||
id: 'fb_2',
|
||||
type: 'suggestion',
|
||||
content: '希望能增加宠物体重变化趋势图功能',
|
||||
contact: '',
|
||||
status: 'processing',
|
||||
submitTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
response: '您的建议很棒,我们正在评估技术可行性。'
|
||||
}
|
||||
]
|
||||
|
||||
uni.setStorageSync('feedbackHistory', mockHistory)
|
||||
historyList.value = mockHistory
|
||||
} else {
|
||||
historyList.value = savedHistory
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载反馈历史失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadFAQ = () => {
|
||||
faqList.value = [
|
||||
{
|
||||
id: 'faq_1',
|
||||
question: '如何添加新的宠物?',
|
||||
answer: '在首页点击"添加宠物"按钮,填写宠物的基本信息和上传照片即可。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
id: 'faq_2',
|
||||
question: '如何设置宠物提醒?',
|
||||
answer: '进入宠物详情页,点击"添加提醒",选择提醒类型和时间即可设置。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
id: 'faq_3',
|
||||
question: '家庭成员如何共享宠物信息?',
|
||||
answer: '在"我的"页面进入家庭管理,邀请成员加入家庭即可共享宠物信息。',
|
||||
expanded: false
|
||||
},
|
||||
{
|
||||
id: 'faq_4',
|
||||
question: '如何发布领养信息?',
|
||||
answer: '在领养页面点击"发布领养",填写宠物信息和领养要求即可发布。',
|
||||
expanded: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const loadUserContact = () => {
|
||||
try {
|
||||
const userInfo = uni.getStorageSync('userInfo') || {}
|
||||
if (userInfo.phone) {
|
||||
formData.contact = userInfo.phone
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载用户联系方式失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const selectType = (type) => {
|
||||
formData.type = type
|
||||
}
|
||||
|
||||
const uploadImage = () => {
|
||||
uni.chooseImage({
|
||||
count: 3 - formData.images.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
formData.images.push(...res.tempFilePaths)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择图片失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择图片失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteImage = (index) => {
|
||||
formData.images.splice(index, 1)
|
||||
}
|
||||
|
||||
const previewImage = (index) => {
|
||||
uni.previewImage({
|
||||
urls: formData.images,
|
||||
current: index
|
||||
})
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
if (!formData.type) {
|
||||
uni.showToast({
|
||||
title: '请选择反馈类型',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!formData.content.trim()) {
|
||||
uni.showToast({
|
||||
title: '请填写问题描述',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const submitFeedback = async () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
// 模拟网络请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
const feedback = {
|
||||
id: 'fb_' + Date.now(),
|
||||
type: formData.type,
|
||||
content: formData.content.trim(),
|
||||
contact: formData.contact.trim(),
|
||||
images: formData.images,
|
||||
status: 'pending',
|
||||
submitTime: new Date().toISOString(),
|
||||
response: ''
|
||||
}
|
||||
|
||||
// 保存到历史记录
|
||||
historyList.value.unshift(feedback)
|
||||
uni.setStorageSync('feedbackHistory', historyList.value)
|
||||
|
||||
// 重置表单
|
||||
Object.assign(formData, {
|
||||
type: '',
|
||||
content: '',
|
||||
contact: formData.contact, // 保留联系方式
|
||||
images: []
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '反馈提交成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交反馈失败:', error)
|
||||
uni.showToast({
|
||||
title: '提交失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshHistory = () => {
|
||||
loadHistory()
|
||||
uni.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const getTypeIcon = (type) => {
|
||||
const typeMap = {
|
||||
'bug': '🐛',
|
||||
'suggestion': '💡',
|
||||
'ui': '🎨',
|
||||
'other': '❓'
|
||||
}
|
||||
return typeMap[type] || '❓'
|
||||
}
|
||||
|
||||
const getTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'bug': '功能异常',
|
||||
'suggestion': '功能建议',
|
||||
'ui': '界面问题',
|
||||
'other': '其他问题'
|
||||
}
|
||||
return typeMap[type] || '其他问题'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'pending': '待处理',
|
||||
'processing': '处理中',
|
||||
'resolved': '已解决',
|
||||
'closed': '已关闭'
|
||||
}
|
||||
return statusMap[status] || '待处理'
|
||||
}
|
||||
|
||||
const formatTime = (timeString) => {
|
||||
if (!timeString) return ''
|
||||
const date = new Date(timeString)
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
const viewFeedbackDetail = (item) => {
|
||||
let content = `类型:${getTypeText(item.type)}\n状态:${getStatusText(item.status)}\n提交时间:${formatTime(item.submitTime)}\n\n问题描述:\n${item.content}`
|
||||
|
||||
if (item.response) {
|
||||
content += `\n\n官方回复:\n${item.response}`
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '反馈详情',
|
||||
content: content,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
const toggleFaq = (faq) => {
|
||||
faq.expanded = !faq.expanded
|
||||
}
|
||||
|
||||
const copyContact = (type) => {
|
||||
let text = ''
|
||||
switch (type) {
|
||||
case 'email':
|
||||
text = 'support@petai.com'
|
||||
break
|
||||
case 'qq':
|
||||
text = '123456789'
|
||||
break
|
||||
case 'wechat':
|
||||
text = 'PetAI_Support'
|
||||
break
|
||||
}
|
||||
|
||||
if (text) {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '已复制到剪贴板',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
formData,
|
||||
submitting,
|
||||
historyList,
|
||||
faqList,
|
||||
feedbackTypes,
|
||||
inputStyle,
|
||||
textareaStyle,
|
||||
buttonStyle,
|
||||
selectType,
|
||||
uploadImage,
|
||||
deleteImage,
|
||||
previewImage,
|
||||
submitFeedback,
|
||||
refreshHistory,
|
||||
getTypeIcon,
|
||||
getTypeText,
|
||||
getStatusText,
|
||||
formatTime,
|
||||
viewFeedbackDetail,
|
||||
toggleFaq,
|
||||
copyContact
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-container {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.feedback-form-card,
|
||||
.history-card,
|
||||
.faq-card,
|
||||
.contact-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 0 30rpx 24rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 反馈表单 */
|
||||
.form-content {
|
||||
.form-group {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.type-options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16rpx;
|
||||
|
||||
.type-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24rpx 16rpx;
|
||||
border: 2rpx solid rgba(255, 138, 128, 0.2);
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
border-color: #FF8A80;
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
|
||||
.option-text {
|
||||
color: #FF8A80;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
font-size: 32rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
.upload-item {
|
||||
position: relative;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.upload-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-delete {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 4rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.delete-icon {
|
||||
color: white;
|
||||
font-size: 16rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-add {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border: 2rpx dashed #FF8A80;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 32rpx;
|
||||
color: #FF8A80;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 20rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 历史反馈 */
|
||||
.history-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
.history-item {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.item-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.type-icon {
|
||||
font-size: 20rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.item-status {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 20rpx;
|
||||
|
||||
&.pending {
|
||||
background: #FFF3E0;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
&.processing {
|
||||
background: #E3F2FD;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
&.resolved {
|
||||
background: #E8F5E8;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
&.closed {
|
||||
background: #F5F5F5;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-content {
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.content-text {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.submit-time {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.item-arrow {
|
||||
font-size: 20rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 空历史状态 */
|
||||
.empty-history {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60rpx;
|
||||
margin-bottom: 16rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 常见问题 */
|
||||
.faq-list {
|
||||
.faq-item {
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.faq-question {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.question-arrow {
|
||||
font-size: 20rpx;
|
||||
color: #FF8A80;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.faq-answer {
|
||||
padding: 0 0 20rpx 0;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
|
||||
.answer-text {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
padding: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 联系我们 */
|
||||
.contact-list {
|
||||
.contact-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.contact-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-action {
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.feedback-container {
|
||||
.feedback-form-card,
|
||||
.history-card,
|
||||
.faq-card,
|
||||
.contact-card {
|
||||
margin: 0 20rpx 20rpx 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.type-options {
|
||||
gap: 12rpx;
|
||||
|
||||
.type-option {
|
||||
padding: 20rpx 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
gap: 12rpx;
|
||||
|
||||
.upload-item,
|
||||
.upload-add {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-container > view {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.feedback-container > view:nth-child(1) { animation-delay: 0.1s; }
|
||||
.feedback-container > view:nth-child(2) { animation-delay: 0.2s; }
|
||||
.feedback-container > view:nth-child(3) { animation-delay: 0.3s; }
|
||||
.feedback-container > view:nth-child(4) { animation-delay: 0.4s; }
|
||||
|
||||
/* 交互反馈 */
|
||||
.type-option:active,
|
||||
.upload-add:active,
|
||||
.history-item:active,
|
||||
.faq-question:active,
|
||||
.contact-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,815 @@
|
|||
<template>
|
||||
<view class="notifications-container">
|
||||
<!-- 通知设置卡片 -->
|
||||
<view class="settings-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">通知设置</text>
|
||||
</view>
|
||||
<view class="settings-list">
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">系统通知</text>
|
||||
<text class="setting-desc">接收系统重要消息和更新通知</text>
|
||||
</view>
|
||||
<view class="setting-switch">
|
||||
<u-switch
|
||||
v-model="notificationSettings.system"
|
||||
@change="onSettingChange('system', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">宠物提醒</text>
|
||||
<text class="setting-desc">宠物喂食、医疗等重要提醒</text>
|
||||
</view>
|
||||
<view class="setting-switch">
|
||||
<u-switch
|
||||
v-model="notificationSettings.petReminder"
|
||||
@change="onSettingChange('petReminder', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">领养消息</text>
|
||||
<text class="setting-desc">领养申请、回复等相关消息</text>
|
||||
</view>
|
||||
<view class="setting-switch">
|
||||
<u-switch
|
||||
v-model="notificationSettings.adoption"
|
||||
@change="onSettingChange('adoption', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">家庭动态</text>
|
||||
<text class="setting-desc">家庭成员活动和共享内容通知</text>
|
||||
</view>
|
||||
<view class="setting-switch">
|
||||
<u-switch
|
||||
v-model="notificationSettings.family"
|
||||
@change="onSettingChange('family', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">营销推广</text>
|
||||
<text class="setting-desc">产品推荐、活动优惠等信息</text>
|
||||
</view>
|
||||
<view class="setting-switch">
|
||||
<u-switch
|
||||
v-model="notificationSettings.marketing"
|
||||
@change="onSettingChange('marketing', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 免打扰设置卡片 -->
|
||||
<view class="dnd-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">免打扰设置</text>
|
||||
</view>
|
||||
<view class="dnd-content">
|
||||
<view class="dnd-toggle">
|
||||
<view class="toggle-info">
|
||||
<text class="toggle-title">开启免打扰</text>
|
||||
<text class="toggle-desc">在指定时间段内不接收通知</text>
|
||||
</view>
|
||||
<view class="toggle-switch">
|
||||
<u-switch
|
||||
v-model="dndSettings.enabled"
|
||||
@change="onDndToggle"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dnd-time" v-if="dndSettings.enabled">
|
||||
<view class="time-item" @click="showStartTimePicker">
|
||||
<text class="time-label">开始时间</text>
|
||||
<text class="time-value">{{ dndSettings.startTime }}</text>
|
||||
</view>
|
||||
<view class="time-item" @click="showEndTimePicker">
|
||||
<text class="time-label">结束时间</text>
|
||||
<text class="time-value">{{ dndSettings.endTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表卡片 -->
|
||||
<view class="messages-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">最近消息</text>
|
||||
<view class="header-action" @click="markAllRead" v-if="unreadCount > 0">
|
||||
<text class="action-text">全部已读</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="messages-list" v-if="messagesList.length > 0">
|
||||
<view
|
||||
class="message-item"
|
||||
v-for="message in messagesList"
|
||||
:key="message.id"
|
||||
:class="{ unread: !message.read }"
|
||||
@click="readMessage(message)"
|
||||
>
|
||||
<view class="message-icon" :class="message.type">
|
||||
<text class="icon-text">{{ getMessageIcon(message.type) }}</text>
|
||||
</view>
|
||||
<view class="message-content">
|
||||
<view class="message-title">{{ message.title }}</view>
|
||||
<view class="message-desc">{{ message.content }}</view>
|
||||
<view class="message-time">{{ formatTime(message.time) }}</view>
|
||||
</view>
|
||||
<view class="message-status" v-if="!message.read">
|
||||
<view class="unread-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-messages" v-else>
|
||||
<view class="empty-icon">📬</view>
|
||||
<view class="empty-text">暂无消息</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 清理设置卡片 -->
|
||||
<view class="cleanup-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">消息管理</text>
|
||||
</view>
|
||||
<view class="cleanup-list">
|
||||
<view class="cleanup-item" @click="clearReadMessages">
|
||||
<view class="cleanup-info">
|
||||
<text class="cleanup-title">清理已读消息</text>
|
||||
<text class="cleanup-desc">删除所有已读的消息记录</text>
|
||||
</view>
|
||||
<view class="cleanup-action">
|
||||
<text class="action-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="cleanup-item" @click="clearAllMessages">
|
||||
<view class="cleanup-info">
|
||||
<text class="cleanup-title">清空所有消息</text>
|
||||
<text class="cleanup-desc">删除所有消息记录(不可恢复)</text>
|
||||
</view>
|
||||
<view class="cleanup-action">
|
||||
<text class="action-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 时间选择器 -->
|
||||
<u-datetime-picker
|
||||
:show="showStartTime"
|
||||
v-model="selectedStartTime"
|
||||
mode="time"
|
||||
@confirm="onStartTimeConfirm"
|
||||
@cancel="showStartTime = false"
|
||||
></u-datetime-picker>
|
||||
|
||||
<u-datetime-picker
|
||||
:show="showEndTime"
|
||||
v-model="selectedEndTime"
|
||||
mode="time"
|
||||
@confirm="onEndTimeConfirm"
|
||||
@cancel="showEndTime = false"
|
||||
></u-datetime-picker>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref, onMounted, computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'NotificationsPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const notificationSettings = reactive({
|
||||
system: true,
|
||||
petReminder: true,
|
||||
adoption: true,
|
||||
family: true,
|
||||
marketing: false
|
||||
})
|
||||
|
||||
const dndSettings = reactive({
|
||||
enabled: false,
|
||||
startTime: '22:00',
|
||||
endTime: '08:00'
|
||||
})
|
||||
|
||||
const messagesList = ref([])
|
||||
const showStartTime = ref(false)
|
||||
const showEndTime = ref(false)
|
||||
const selectedStartTime = ref(Date.now())
|
||||
const selectedEndTime = ref(Date.now())
|
||||
|
||||
// 计算属性
|
||||
const unreadCount = computed(() => {
|
||||
return messagesList.value.filter(msg => !msg.read).length
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadSettings()
|
||||
loadMessages()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadSettings = () => {
|
||||
try {
|
||||
const savedSettings = uni.getStorageSync('notificationSettings')
|
||||
if (savedSettings) {
|
||||
Object.assign(notificationSettings, savedSettings)
|
||||
}
|
||||
|
||||
const savedDndSettings = uni.getStorageSync('dndSettings')
|
||||
if (savedDndSettings) {
|
||||
Object.assign(dndSettings, savedDndSettings)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadMessages = () => {
|
||||
try {
|
||||
const savedMessages = uni.getStorageSync('notifications') || []
|
||||
|
||||
// 如果没有消息,创建一些模拟消息
|
||||
if (savedMessages.length === 0) {
|
||||
const mockMessages = [
|
||||
{
|
||||
id: 'msg_1',
|
||||
type: 'system',
|
||||
title: '系统更新',
|
||||
content: '宠物AI助手已更新到最新版本,新增了家庭共享功能。',
|
||||
time: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
||||
read: false
|
||||
},
|
||||
{
|
||||
id: 'msg_2',
|
||||
type: 'pet',
|
||||
title: '喂食提醒',
|
||||
content: '小橘的晚餐时间到了,记得给它准备食物哦!',
|
||||
time: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
|
||||
read: true
|
||||
},
|
||||
{
|
||||
id: 'msg_3',
|
||||
type: 'adoption',
|
||||
title: '领养申请',
|
||||
content: '有用户申请领养您发布的小白,请及时查看处理。',
|
||||
time: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
read: false
|
||||
},
|
||||
{
|
||||
id: 'msg_4',
|
||||
type: 'family',
|
||||
title: '家庭动态',
|
||||
content: '家庭成员小明为小花添加了一条健康记录。',
|
||||
time: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
read: true
|
||||
}
|
||||
]
|
||||
|
||||
uni.setStorageSync('notifications', mockMessages)
|
||||
messagesList.value = mockMessages
|
||||
} else {
|
||||
messagesList.value = savedMessages
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = () => {
|
||||
uni.setStorageSync('notificationSettings', notificationSettings)
|
||||
uni.setStorageSync('dndSettings', dndSettings)
|
||||
}
|
||||
|
||||
const saveMessages = () => {
|
||||
uni.setStorageSync('notifications', messagesList.value)
|
||||
}
|
||||
|
||||
const onSettingChange = (key, value) => {
|
||||
notificationSettings[key] = value
|
||||
saveSettings()
|
||||
|
||||
uni.showToast({
|
||||
title: value ? '已开启' : '已关闭',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const onDndToggle = (value) => {
|
||||
dndSettings.enabled = value
|
||||
saveSettings()
|
||||
|
||||
if (value) {
|
||||
uni.showToast({
|
||||
title: '免打扰已开启',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '免打扰已关闭',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const showStartTimePicker = () => {
|
||||
const [hours, minutes] = dndSettings.startTime.split(':')
|
||||
const date = new Date()
|
||||
date.setHours(parseInt(hours), parseInt(minutes), 0, 0)
|
||||
selectedStartTime.value = date.getTime()
|
||||
showStartTime.value = true
|
||||
}
|
||||
|
||||
const showEndTimePicker = () => {
|
||||
const [hours, minutes] = dndSettings.endTime.split(':')
|
||||
const date = new Date()
|
||||
date.setHours(parseInt(hours), parseInt(minutes), 0, 0)
|
||||
selectedEndTime.value = date.getTime()
|
||||
showEndTime.value = true
|
||||
}
|
||||
|
||||
const onStartTimeConfirm = (e) => {
|
||||
const date = new Date(e.value)
|
||||
dndSettings.startTime = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
saveSettings()
|
||||
showStartTime.value = false
|
||||
}
|
||||
|
||||
const onEndTimeConfirm = (e) => {
|
||||
const date = new Date(e.value)
|
||||
dndSettings.endTime = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
saveSettings()
|
||||
showEndTime.value = false
|
||||
}
|
||||
|
||||
const getMessageIcon = (type) => {
|
||||
const iconMap = {
|
||||
'system': '🔔',
|
||||
'pet': '🐱',
|
||||
'adoption': '🏠',
|
||||
'family': '👨👩👧👦',
|
||||
'marketing': '📢'
|
||||
}
|
||||
return iconMap[type] || '📬'
|
||||
}
|
||||
|
||||
const formatTime = (timeString) => {
|
||||
if (!timeString) return ''
|
||||
const date = new Date(timeString)
|
||||
const now = new Date()
|
||||
const diffTime = now - date
|
||||
const diffHours = Math.floor(diffTime / (1000 * 60 * 60))
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffHours < 1) {
|
||||
return '刚刚'
|
||||
} else if (diffHours < 24) {
|
||||
return `${diffHours}小时前`
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}天前`
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
}
|
||||
|
||||
const readMessage = (message) => {
|
||||
if (!message.read) {
|
||||
message.read = true
|
||||
saveMessages()
|
||||
}
|
||||
|
||||
// 显示消息详情
|
||||
uni.showModal({
|
||||
title: message.title,
|
||||
content: message.content,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
const markAllRead = () => {
|
||||
messagesList.value.forEach(msg => {
|
||||
msg.read = true
|
||||
})
|
||||
saveMessages()
|
||||
|
||||
uni.showToast({
|
||||
title: '已全部标记为已读',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const clearReadMessages = () => {
|
||||
uni.showModal({
|
||||
title: '清理已读消息',
|
||||
content: '确定要删除所有已读消息吗?此操作不可恢复。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
messagesList.value = messagesList.value.filter(msg => !msg.read)
|
||||
saveMessages()
|
||||
|
||||
uni.showToast({
|
||||
title: '已清理已读消息',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clearAllMessages = () => {
|
||||
uni.showModal({
|
||||
title: '清空所有消息',
|
||||
content: '确定要删除所有消息吗?此操作不可恢复。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
messagesList.value = []
|
||||
saveMessages()
|
||||
|
||||
uni.showToast({
|
||||
title: '已清空所有消息',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
notificationSettings,
|
||||
dndSettings,
|
||||
messagesList,
|
||||
showStartTime,
|
||||
showEndTime,
|
||||
selectedStartTime,
|
||||
selectedEndTime,
|
||||
unreadCount,
|
||||
onSettingChange,
|
||||
onDndToggle,
|
||||
showStartTimePicker,
|
||||
showEndTimePicker,
|
||||
onStartTimeConfirm,
|
||||
onEndTimeConfirm,
|
||||
getMessageIcon,
|
||||
formatTime,
|
||||
readMessage,
|
||||
markAllRead,
|
||||
clearReadMessages,
|
||||
clearAllMessages
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notifications-container {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.settings-card,
|
||||
.dnd-card,
|
||||
.messages-card,
|
||||
.cleanup-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 0 30rpx 24rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 设置列表 */
|
||||
.settings-list {
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
|
||||
.setting-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.setting-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-switch {
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 免打扰设置 */
|
||||
.dnd-content {
|
||||
.dnd-toggle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.toggle-info {
|
||||
flex: 1;
|
||||
|
||||
.toggle-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.toggle-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.dnd-time {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
|
||||
.time-item {
|
||||
padding: 20rpx;
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.time-label {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 消息列表 */
|
||||
.messages-list {
|
||||
.message-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.unread {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
|
||||
&.system {
|
||||
background: rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
&.pet {
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
}
|
||||
|
||||
&.adoption {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
&.family {
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
}
|
||||
|
||||
&.marketing {
|
||||
background: rgba(156, 39, 176, 0.1);
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
|
||||
.message-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.message-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.message-status {
|
||||
margin-left: 16rpx;
|
||||
|
||||
.unread-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
background: #FF8A80;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 空消息状态 */
|
||||
.empty-messages {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60rpx;
|
||||
margin-bottom: 16rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 清理设置 */
|
||||
.cleanup-list {
|
||||
.cleanup-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.cleanup-info {
|
||||
flex: 1;
|
||||
|
||||
.cleanup-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.cleanup-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.cleanup-action {
|
||||
.action-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.notifications-container {
|
||||
.settings-card,
|
||||
.dnd-card,
|
||||
.messages-card,
|
||||
.cleanup-card {
|
||||
margin: 0 20rpx 20rpx 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.dnd-time {
|
||||
gap: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-container > view {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.notifications-container > view:nth-child(1) { animation-delay: 0.1s; }
|
||||
.notifications-container > view:nth-child(2) { animation-delay: 0.2s; }
|
||||
.notifications-container > view:nth-child(3) { animation-delay: 0.3s; }
|
||||
.notifications-container > view:nth-child(4) { animation-delay: 0.4s; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,764 @@
|
|||
<template>
|
||||
<view class="privacy-container">
|
||||
<!-- 隐私权限设置卡片 -->
|
||||
<view class="privacy-settings-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">隐私权限</text>
|
||||
</view>
|
||||
<view class="settings-list">
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">个人资料可见性</text>
|
||||
<text class="setting-desc">控制其他用户查看您的个人信息</text>
|
||||
</view>
|
||||
<view class="setting-value" @click="showProfileVisibilityPicker">
|
||||
<text class="value-text">{{ getVisibilityText(privacySettings.profileVisibility) }}</text>
|
||||
<text class="value-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">宠物信息可见性</text>
|
||||
<text class="setting-desc">控制其他用户查看您的宠物信息</text>
|
||||
</view>
|
||||
<view class="setting-value" @click="showPetVisibilityPicker">
|
||||
<text class="value-text">{{ getVisibilityText(privacySettings.petVisibility) }}</text>
|
||||
<text class="value-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">允许陌生人联系</text>
|
||||
<text class="setting-desc">是否允许非好友用户向您发送消息</text>
|
||||
</view>
|
||||
<view class="setting-switch">
|
||||
<u-switch
|
||||
v-model="privacySettings.allowStrangerContact"
|
||||
@change="onSettingChange('allowStrangerContact', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">显示在线状态</text>
|
||||
<text class="setting-desc">是否向其他用户显示您的在线状态</text>
|
||||
</view>
|
||||
<view class="setting-switch">
|
||||
<u-switch
|
||||
v-model="privacySettings.showOnlineStatus"
|
||||
@change="onSettingChange('showOnlineStatus', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<view class="setting-info">
|
||||
<text class="setting-title">允许搜索</text>
|
||||
<text class="setting-desc">是否允许其他用户通过搜索找到您</text>
|
||||
</view>
|
||||
<view class="setting-switch">
|
||||
<u-switch
|
||||
v-model="privacySettings.allowSearch"
|
||||
@change="onSettingChange('allowSearch', $event)"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据管理卡片 -->
|
||||
<view class="data-management-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">数据管理</text>
|
||||
</view>
|
||||
<view class="data-list">
|
||||
<view class="data-item" @click="exportData">
|
||||
<view class="data-info">
|
||||
<text class="data-title">导出我的数据</text>
|
||||
<text class="data-desc">下载您在应用中的所有数据</text>
|
||||
</view>
|
||||
<view class="data-action">
|
||||
<text class="action-icon">📥</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="data-item" @click="viewDataUsage">
|
||||
<view class="data-info">
|
||||
<text class="data-title">数据使用情况</text>
|
||||
<text class="data-desc">查看应用收集和使用的数据类型</text>
|
||||
</view>
|
||||
<view class="data-action">
|
||||
<text class="action-icon">📊</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="data-item" @click="clearCache">
|
||||
<view class="data-info">
|
||||
<text class="data-title">清理缓存数据</text>
|
||||
<text class="data-desc">清除应用缓存以释放存储空间</text>
|
||||
</view>
|
||||
<view class="data-action">
|
||||
<text class="action-icon">🗑️</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 账号安全卡片 -->
|
||||
<view class="security-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">账号安全</text>
|
||||
</view>
|
||||
<view class="security-list">
|
||||
<view class="security-item" @click="changePassword">
|
||||
<view class="security-info">
|
||||
<text class="security-title">修改密码</text>
|
||||
<text class="security-desc">定期更换密码保护账号安全</text>
|
||||
</view>
|
||||
<view class="security-action">
|
||||
<text class="action-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="security-item" @click="viewLoginHistory">
|
||||
<view class="security-info">
|
||||
<text class="security-title">登录记录</text>
|
||||
<text class="security-desc">查看最近的登录活动</text>
|
||||
</view>
|
||||
<view class="security-action">
|
||||
<text class="action-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="security-item">
|
||||
<view class="security-info">
|
||||
<text class="security-title">两步验证</text>
|
||||
<text class="security-desc">开启两步验证增强账号安全</text>
|
||||
</view>
|
||||
<view class="security-switch">
|
||||
<u-switch
|
||||
v-model="privacySettings.twoFactorAuth"
|
||||
@change="onTwoFactorChange"
|
||||
active-color="#FF8A80"
|
||||
></u-switch>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 危险操作卡片 -->
|
||||
<view class="danger-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">危险操作</text>
|
||||
</view>
|
||||
<view class="danger-list">
|
||||
<view class="danger-item" @click="deleteAllData">
|
||||
<view class="danger-info">
|
||||
<text class="danger-title">删除所有数据</text>
|
||||
<text class="danger-desc">永久删除您的所有数据(不可恢复)</text>
|
||||
</view>
|
||||
<view class="danger-action">
|
||||
<text class="action-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="danger-item" @click="deleteAccount">
|
||||
<view class="danger-info">
|
||||
<text class="danger-title">注销账号</text>
|
||||
<text class="danger-desc">永久删除账号和所有相关数据</text>
|
||||
</view>
|
||||
<view class="danger-action">
|
||||
<text class="action-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选择器 -->
|
||||
<u-picker
|
||||
:show="showProfileVisibility"
|
||||
:columns="visibilityColumns"
|
||||
@confirm="onProfileVisibilityConfirm"
|
||||
@cancel="showProfileVisibility = false"
|
||||
></u-picker>
|
||||
|
||||
<u-picker
|
||||
:show="showPetVisibility"
|
||||
:columns="visibilityColumns"
|
||||
@confirm="onPetVisibilityConfirm"
|
||||
@cancel="showPetVisibility = false"
|
||||
></u-picker>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'PrivacyPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const privacySettings = reactive({
|
||||
profileVisibility: 'friends',
|
||||
petVisibility: 'public',
|
||||
allowStrangerContact: false,
|
||||
showOnlineStatus: true,
|
||||
allowSearch: true,
|
||||
twoFactorAuth: false
|
||||
})
|
||||
|
||||
const showProfileVisibility = ref(false)
|
||||
const showPetVisibility = ref(false)
|
||||
|
||||
const visibilityColumns = ref([
|
||||
[
|
||||
{ text: '公开', value: 'public' },
|
||||
{ text: '仅好友', value: 'friends' },
|
||||
{ text: '仅自己', value: 'private' }
|
||||
]
|
||||
])
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadSettings()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadSettings = () => {
|
||||
try {
|
||||
const savedSettings = uni.getStorageSync('privacySettings')
|
||||
if (savedSettings) {
|
||||
Object.assign(privacySettings, savedSettings)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载隐私设置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = () => {
|
||||
uni.setStorageSync('privacySettings', privacySettings)
|
||||
}
|
||||
|
||||
const getVisibilityText = (value) => {
|
||||
const textMap = {
|
||||
'public': '公开',
|
||||
'friends': '仅好友',
|
||||
'private': '仅自己'
|
||||
}
|
||||
return textMap[value] || '公开'
|
||||
}
|
||||
|
||||
const onSettingChange = (key, value) => {
|
||||
privacySettings[key] = value
|
||||
saveSettings()
|
||||
|
||||
uni.showToast({
|
||||
title: value ? '已开启' : '已关闭',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const onTwoFactorChange = (value) => {
|
||||
if (value) {
|
||||
uni.showModal({
|
||||
title: '开启两步验证',
|
||||
content: '开启两步验证后,登录时需要额外的验证码。确定要开启吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
privacySettings.twoFactorAuth = true
|
||||
saveSettings()
|
||||
uni.showToast({
|
||||
title: '两步验证已开启',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
privacySettings.twoFactorAuth = false
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
privacySettings.twoFactorAuth = false
|
||||
saveSettings()
|
||||
uni.showToast({
|
||||
title: '两步验证已关闭',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const showProfileVisibilityPicker = () => {
|
||||
showProfileVisibility.value = true
|
||||
}
|
||||
|
||||
const showPetVisibilityPicker = () => {
|
||||
showPetVisibility.value = true
|
||||
}
|
||||
|
||||
const onProfileVisibilityConfirm = (e) => {
|
||||
privacySettings.profileVisibility = e.value[0].value
|
||||
saveSettings()
|
||||
showProfileVisibility.value = false
|
||||
|
||||
uni.showToast({
|
||||
title: '设置已保存',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const onPetVisibilityConfirm = (e) => {
|
||||
privacySettings.petVisibility = e.value[0].value
|
||||
saveSettings()
|
||||
showPetVisibility.value = false
|
||||
|
||||
uni.showToast({
|
||||
title: '设置已保存',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
uni.showLoading({
|
||||
title: '正在导出数据...'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showModal({
|
||||
title: '数据导出',
|
||||
content: '您的数据已导出完成,将通过邮件发送给您。',
|
||||
showCancel: false
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const viewDataUsage = () => {
|
||||
const dataTypes = [
|
||||
'• 个人基本信息(昵称、头像等)',
|
||||
'• 宠物信息和照片',
|
||||
'• 使用行为数据',
|
||||
'• 设备信息',
|
||||
'• 位置信息(仅在使用相关功能时)'
|
||||
]
|
||||
|
||||
uni.showModal({
|
||||
title: '数据使用情况',
|
||||
content: `我们收集以下类型的数据:\n\n${dataTypes.join('\n')}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
const clearCache = () => {
|
||||
uni.showModal({
|
||||
title: '清理缓存',
|
||||
content: '确定要清理应用缓存吗?这将删除临时文件和图片缓存。',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({
|
||||
title: '正在清理...'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '缓存清理完成',
|
||||
icon: 'success'
|
||||
})
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const changePassword = () => {
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const viewLoginHistory = () => {
|
||||
const loginHistory = [
|
||||
'今天 14:30 - iPhone (当前设备)',
|
||||
'昨天 09:15 - iPhone',
|
||||
'3天前 20:45 - iPhone',
|
||||
'1周前 16:20 - iPhone'
|
||||
]
|
||||
|
||||
uni.showModal({
|
||||
title: '登录记录',
|
||||
content: `最近登录记录:\n\n${loginHistory.join('\n')}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
const deleteAllData = () => {
|
||||
uni.showModal({
|
||||
title: '删除所有数据',
|
||||
content: '此操作将永久删除您的所有数据,包括宠物信息、记录等,且无法恢复。确定要继续吗?',
|
||||
confirmColor: '#FF5722',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showModal({
|
||||
title: '最终确认',
|
||||
content: '请再次确认:您真的要删除所有数据吗?此操作不可撤销!',
|
||||
confirmColor: '#FF5722',
|
||||
success: (res2) => {
|
||||
if (res2.confirm) {
|
||||
// 清除所有本地数据
|
||||
uni.clearStorageSync()
|
||||
|
||||
uni.showToast({
|
||||
title: '数据已删除',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteAccount = () => {
|
||||
uni.showModal({
|
||||
title: '注销账号',
|
||||
content: '注销账号将永久删除您的账号和所有相关数据,此操作不可恢复。确定要继续吗?',
|
||||
confirmColor: '#FF5722',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showModal({
|
||||
title: '最终确认',
|
||||
content: '请再次确认:您真的要注销账号吗?注销后将无法找回!',
|
||||
confirmColor: '#FF5722',
|
||||
success: (res2) => {
|
||||
if (res2.confirm) {
|
||||
uni.showLoading({
|
||||
title: '正在注销...'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.clearStorageSync()
|
||||
|
||||
uni.showToast({
|
||||
title: '账号已注销',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 1500)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
privacySettings,
|
||||
showProfileVisibility,
|
||||
showPetVisibility,
|
||||
visibilityColumns,
|
||||
getVisibilityText,
|
||||
onSettingChange,
|
||||
onTwoFactorChange,
|
||||
showProfileVisibilityPicker,
|
||||
showPetVisibilityPicker,
|
||||
onProfileVisibilityConfirm,
|
||||
onPetVisibilityConfirm,
|
||||
exportData,
|
||||
viewDataUsage,
|
||||
clearCache,
|
||||
changePassword,
|
||||
viewLoginHistory,
|
||||
deleteAllData,
|
||||
deleteAccount
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.privacy-container {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.privacy-settings-card,
|
||||
.data-management-card,
|
||||
.security-card,
|
||||
.danger-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 0 30rpx 24rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 设置列表 */
|
||||
.settings-list {
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
|
||||
.setting-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.setting-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 24rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: 26rpx;
|
||||
color: #FF8A80;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.value-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-switch {
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 数据管理列表 */
|
||||
.data-list {
|
||||
.data-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.data-info {
|
||||
flex: 1;
|
||||
|
||||
.data-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.data-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.data-action {
|
||||
margin-left: 24rpx;
|
||||
|
||||
.action-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 安全设置列表 */
|
||||
.security-list {
|
||||
.security-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 138, 128, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.security-info {
|
||||
flex: 1;
|
||||
|
||||
.security-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.security-desc {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.security-action {
|
||||
margin-left: 24rpx;
|
||||
|
||||
.action-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
|
||||
.security-switch {
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 危险操作卡片 */
|
||||
.danger-card {
|
||||
border: 2rpx solid rgba(255, 87, 34, 0.2);
|
||||
|
||||
.card-title {
|
||||
color: #FF5722;
|
||||
}
|
||||
}
|
||||
|
||||
.danger-list {
|
||||
.danger-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 87, 34, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 87, 34, 0.05);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.danger-info {
|
||||
flex: 1;
|
||||
|
||||
.danger-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #FF5722;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.danger-desc {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A65;
|
||||
}
|
||||
}
|
||||
|
||||
.danger-action {
|
||||
margin-left: 24rpx;
|
||||
|
||||
.action-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #FF5722;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.privacy-container {
|
||||
.privacy-settings-card,
|
||||
.data-management-card,
|
||||
.security-card,
|
||||
.danger-card {
|
||||
margin: 0 20rpx 20rpx 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.privacy-container > view {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.privacy-container > view:nth-child(1) { animation-delay: 0.1s; }
|
||||
.privacy-container > view:nth-child(2) { animation-delay: 0.2s; }
|
||||
.privacy-container > view:nth-child(3) { animation-delay: 0.3s; }
|
||||
.privacy-container > view:nth-child(4) { animation-delay: 0.4s; }
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,581 @@
|
|||
<template>
|
||||
<view class="user-info-container">
|
||||
<!-- 头像设置卡片 -->
|
||||
<view class="avatar-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">头像设置</text>
|
||||
</view>
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-wrapper" @click="chooseAvatar">
|
||||
<u-avatar
|
||||
:src="userInfo.avatarUrl || ''"
|
||||
:text="userInfo.nickName ? userInfo.nickName.charAt(0) : '👤'"
|
||||
size="120"
|
||||
shape="circle"
|
||||
bg-color="linear-gradient(135deg, #FF8A80, #FFB6C1)"
|
||||
color="white"
|
||||
font-size="50"
|
||||
></u-avatar>
|
||||
<view class="avatar-edit-icon">
|
||||
<text class="edit-text">📷</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="avatar-tips">
|
||||
<text class="tips-text">点击更换头像</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息卡片 -->
|
||||
<view class="info-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">基本信息</text>
|
||||
</view>
|
||||
<view class="info-list">
|
||||
<view class="info-item">
|
||||
<view class="info-label">
|
||||
<text class="label-text">昵称</text>
|
||||
</view>
|
||||
<view class="info-input">
|
||||
<u-input
|
||||
v-model="userInfo.nickName"
|
||||
placeholder="请输入昵称"
|
||||
border="none"
|
||||
:custom-style="inputStyle"
|
||||
@change="onNickNameChange"
|
||||
></u-input>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-label">
|
||||
<text class="label-text">性别</text>
|
||||
</view>
|
||||
<view class="info-value" @click="showGenderPicker">
|
||||
<text class="value-text">{{ userInfo.gender || '请选择' }}</text>
|
||||
<text class="arrow-icon">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-label">
|
||||
<text class="label-text">生日</text>
|
||||
</view>
|
||||
<view class="info-value" @click="showDatePicker">
|
||||
<text class="value-text">{{ userInfo.birthday || '请选择' }}</text>
|
||||
<text class="arrow-icon">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<view class="info-label">
|
||||
<text class="label-text">地区</text>
|
||||
</view>
|
||||
<view class="info-value" @click="showRegionPicker">
|
||||
<text class="value-text">{{ userInfo.region || '请选择' }}</text>
|
||||
<text class="arrow-icon">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人简介卡片 -->
|
||||
<view class="bio-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">个人简介</text>
|
||||
</view>
|
||||
<view class="bio-section">
|
||||
<u-textarea
|
||||
v-model="userInfo.bio"
|
||||
placeholder="介绍一下自己和你的宠物吧..."
|
||||
:maxlength="200"
|
||||
count
|
||||
:custom-style="textareaStyle"
|
||||
@change="onBioChange"
|
||||
></u-textarea>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 账号信息卡片 -->
|
||||
<view class="account-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">账号信息</text>
|
||||
</view>
|
||||
<view class="account-list">
|
||||
<view class="account-item">
|
||||
<view class="account-label">
|
||||
<text class="label-text">注册时间</text>
|
||||
</view>
|
||||
<view class="account-value">
|
||||
<text class="value-text">{{ formatDate(userInfo.registerTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="account-item">
|
||||
<view class="account-label">
|
||||
<text class="label-text">使用天数</text>
|
||||
</view>
|
||||
<view class="account-value">
|
||||
<text class="value-text">{{ loginDays }} 天</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<view class="save-section">
|
||||
<u-button
|
||||
type="primary"
|
||||
:custom-style="buttonStyle"
|
||||
@click="saveUserInfo"
|
||||
:loading="saving"
|
||||
>
|
||||
{{ saving ? '保存中...' : '保存修改' }}
|
||||
</u-button>
|
||||
</view>
|
||||
|
||||
<!-- 性别选择器 -->
|
||||
<u-picker
|
||||
:show="showGender"
|
||||
:columns="genderColumns"
|
||||
@confirm="onGenderConfirm"
|
||||
@cancel="showGender = false"
|
||||
></u-picker>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<u-datetime-picker
|
||||
:show="showDate"
|
||||
v-model="selectedDate"
|
||||
mode="date"
|
||||
@confirm="onDateConfirm"
|
||||
@cancel="showDate = false"
|
||||
></u-datetime-picker>
|
||||
|
||||
<!-- 地区选择器 -->
|
||||
<u-picker
|
||||
:show="showRegion"
|
||||
:columns="regionColumns"
|
||||
@confirm="onRegionConfirm"
|
||||
@cancel="showRegion = false"
|
||||
></u-picker>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref, onMounted, computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'UserInfoPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const userInfo = reactive({
|
||||
nickName: '',
|
||||
avatarUrl: '',
|
||||
gender: '',
|
||||
birthday: '',
|
||||
region: '',
|
||||
bio: '',
|
||||
registerTime: ''
|
||||
})
|
||||
|
||||
const loginDays = ref(0)
|
||||
const saving = ref(false)
|
||||
|
||||
// 选择器状态
|
||||
const showGender = ref(false)
|
||||
const showDate = ref(false)
|
||||
const showRegion = ref(false)
|
||||
const selectedDate = ref(Date.now())
|
||||
|
||||
// 选择器数据
|
||||
const genderColumns = ref([
|
||||
['男', '女', '保密']
|
||||
])
|
||||
|
||||
const regionColumns = ref([
|
||||
['北京市', '上海市', '广州市', '深圳市', '杭州市', '成都市', '武汉市', '西安市', '南京市', '重庆市']
|
||||
])
|
||||
|
||||
// 样式配置
|
||||
const inputStyle = {
|
||||
fontSize: '30rpx',
|
||||
color: '#333333'
|
||||
}
|
||||
|
||||
const textareaStyle = {
|
||||
fontSize: '28rpx',
|
||||
color: '#333333'
|
||||
}
|
||||
|
||||
const buttonStyle = {
|
||||
background: 'linear-gradient(135deg, #FF8A80, #FFB6C1)',
|
||||
borderRadius: '24rpx',
|
||||
height: '88rpx',
|
||||
fontSize: '32rpx'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadUserInfo()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadUserInfo = () => {
|
||||
try {
|
||||
const savedUserInfo = uni.getStorageSync('userInfo') || {}
|
||||
Object.assign(userInfo, {
|
||||
nickName: savedUserInfo.nickName || '',
|
||||
avatarUrl: savedUserInfo.avatarUrl || '',
|
||||
gender: savedUserInfo.gender || '',
|
||||
birthday: savedUserInfo.birthday || '',
|
||||
region: savedUserInfo.region || '',
|
||||
bio: savedUserInfo.bio || '',
|
||||
registerTime: savedUserInfo.registerTime || new Date().toISOString()
|
||||
})
|
||||
|
||||
calculateLoginDays()
|
||||
} catch (error) {
|
||||
console.error('加载用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const calculateLoginDays = () => {
|
||||
const loginDate = uni.getStorageSync('loginDate')
|
||||
if (loginDate) {
|
||||
const now = new Date()
|
||||
const login = new Date(loginDate)
|
||||
const diffTime = Math.abs(now - login)
|
||||
loginDays.value = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
}
|
||||
|
||||
const chooseAvatar = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
userInfo.avatarUrl = res.tempFilePaths[0]
|
||||
uni.showToast({
|
||||
title: '头像已更新',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择头像失败:', err)
|
||||
uni.showToast({
|
||||
title: '选择头像失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showGenderPicker = () => {
|
||||
showGender.value = true
|
||||
}
|
||||
|
||||
const showDatePicker = () => {
|
||||
showDate.value = true
|
||||
}
|
||||
|
||||
const showRegionPicker = () => {
|
||||
showRegion.value = true
|
||||
}
|
||||
|
||||
const onGenderConfirm = (e) => {
|
||||
userInfo.gender = e.value[0]
|
||||
showGender.value = false
|
||||
}
|
||||
|
||||
const onDateConfirm = (e) => {
|
||||
const date = new Date(e.value)
|
||||
userInfo.birthday = date.toISOString().split('T')[0]
|
||||
showDate.value = false
|
||||
}
|
||||
|
||||
const onRegionConfirm = (e) => {
|
||||
userInfo.region = e.value[0]
|
||||
showRegion.value = false
|
||||
}
|
||||
|
||||
const onNickNameChange = (value) => {
|
||||
userInfo.nickName = value
|
||||
}
|
||||
|
||||
const onBioChange = (value) => {
|
||||
userInfo.bio = value
|
||||
}
|
||||
|
||||
const saveUserInfo = async () => {
|
||||
if (!userInfo.nickName.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入昵称',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
// 保存到本地存储
|
||||
uni.setStorageSync('userInfo', userInfo)
|
||||
|
||||
// 模拟网络请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
uni.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 返回上一页
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存用户信息失败:', error)
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '未设置'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
loginDays,
|
||||
saving,
|
||||
showGender,
|
||||
showDate,
|
||||
showRegion,
|
||||
selectedDate,
|
||||
genderColumns,
|
||||
regionColumns,
|
||||
inputStyle,
|
||||
textareaStyle,
|
||||
buttonStyle,
|
||||
chooseAvatar,
|
||||
showGenderPicker,
|
||||
showDatePicker,
|
||||
showRegionPicker,
|
||||
onGenderConfirm,
|
||||
onDateConfirm,
|
||||
onRegionConfirm,
|
||||
onNickNameChange,
|
||||
onBioChange,
|
||||
saveUserInfo,
|
||||
formatDate
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-info-container {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.avatar-card,
|
||||
.info-card,
|
||||
.bio-card,
|
||||
.account-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 0 30rpx 24rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 头像设置卡片 */
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.avatar-edit-icon {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
right: 8rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.edit-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-tips {
|
||||
.tips-text {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 基本信息卡片 */
|
||||
.info-list {
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 160rpx;
|
||||
|
||||
.label-text {
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.info-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.value-text {
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 24rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 个人简介卡片 */
|
||||
.bio-section {
|
||||
.bio-textarea {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
padding: 20rpx;
|
||||
border: 1rpx solid rgba(255, 138, 128, 0.2);
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* 账号信息卡片 */
|
||||
.account-list {
|
||||
.account-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.account-label {
|
||||
.label-text {
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.account-value {
|
||||
.value-text {
|
||||
font-size: 30rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 保存按钮 */
|
||||
.save-section {
|
||||
margin: 40rpx 30rpx 0 30rpx;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.user-info-container {
|
||||
.avatar-card,
|
||||
.info-card,
|
||||
.bio-card,
|
||||
.account-card {
|
||||
margin: 0 20rpx 20rpx 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.save-section {
|
||||
margin: 32rpx 20rpx 0 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.user-info-container > view {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.user-info-container > view:nth-child(1) { animation-delay: 0.1s; }
|
||||
.user-info-container > view:nth-child(2) { animation-delay: 0.2s; }
|
||||
.user-info-container > view:nth-child(3) { animation-delay: 0.3s; }
|
||||
.user-info-container > view:nth-child(4) { animation-delay: 0.4s; }
|
||||
.user-info-container > view:nth-child(5) { animation-delay: 0.5s; }
|
||||
</style>
|
||||
|
|
@ -0,0 +1,824 @@
|
|||
<template>
|
||||
<view class="detail-container">
|
||||
<!-- 产品信息卡片 -->
|
||||
<view class="product-info-card">
|
||||
<view class="product-images">
|
||||
<swiper class="product-swiper" indicator-dots circular>
|
||||
<swiper-item v-for="(image, index) in product.images" :key="index">
|
||||
<image class="swiper-image" :src="image" mode="aspectFill" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
<view class="product-details">
|
||||
<view class="product-header">
|
||||
<view class="product-brand">{{ product.brand }}</view>
|
||||
<view class="product-favorite" @click="toggleFavorite">
|
||||
<text class="favorite-icon" :class="{ active: product.isFavorite }">♥</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="product-name">{{ product.name }}</view>
|
||||
<view class="product-rating">
|
||||
<view class="rating-stars">
|
||||
<text
|
||||
class="star"
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
:class="{ active: i <= product.rating }"
|
||||
>★</text>
|
||||
</view>
|
||||
<text class="rating-score">{{ product.rating }}</text>
|
||||
<text class="rating-count">({{ product.reviewCount }}条评价)</text>
|
||||
</view>
|
||||
<view class="product-price">
|
||||
<text class="price-label">参考价格:</text>
|
||||
<text class="price-range">¥{{ product.priceRange }}</text>
|
||||
</view>
|
||||
<view class="product-tags">
|
||||
<text
|
||||
class="product-tag"
|
||||
v-for="tag in product.tags"
|
||||
:key="tag"
|
||||
>{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 专业评测卡片 -->
|
||||
<view class="review-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">专业评测</text>
|
||||
<view class="expert-info">
|
||||
<text class="expert-name">宠物营养师 · 李医生</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="review-content">
|
||||
<view class="review-section">
|
||||
<text class="section-title">营养成分分析</text>
|
||||
<view class="nutrition-list">
|
||||
<view class="nutrition-item" v-for="item in product.nutrition" :key="item.name">
|
||||
<text class="nutrition-name">{{ item.name }}</text>
|
||||
<text class="nutrition-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="review-section">
|
||||
<text class="section-title">适用对象</text>
|
||||
<text class="section-content">{{ product.suitableFor }}</text>
|
||||
</view>
|
||||
<view class="review-section">
|
||||
<text class="section-title">优点</text>
|
||||
<view class="pros-list">
|
||||
<text class="pros-item" v-for="pro in product.pros" :key="pro">• {{ pro }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="review-section">
|
||||
<text class="section-title">缺点</text>
|
||||
<view class="cons-list">
|
||||
<text class="cons-item" v-for="con in product.cons" :key="con">• {{ con }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="review-section">
|
||||
<text class="section-title">推荐指数</text>
|
||||
<view class="recommendation">
|
||||
<view class="recommendation-stars">
|
||||
<text
|
||||
class="rec-star"
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
:class="{ active: i <= product.recommendation }"
|
||||
>★</text>
|
||||
</view>
|
||||
<text class="recommendation-text">{{ getRecommendationText(product.recommendation) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户评价卡片 -->
|
||||
<view class="user-reviews-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">用户评价</text>
|
||||
<text class="reviews-count">{{ product.reviewCount }}条</text>
|
||||
</view>
|
||||
<view class="reviews-list">
|
||||
<view class="review-item" v-for="review in userReviews" :key="review.id">
|
||||
<view class="review-header">
|
||||
<view class="user-info">
|
||||
<image class="user-avatar" :src="review.avatar" mode="aspectFill" />
|
||||
<text class="user-name">{{ review.userName }}</text>
|
||||
</view>
|
||||
<view class="review-rating">
|
||||
<text
|
||||
class="review-star"
|
||||
v-for="i in 5"
|
||||
:key="i"
|
||||
:class="{ active: i <= review.rating }"
|
||||
>★</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="review-content">
|
||||
<text class="review-text">{{ review.content }}</text>
|
||||
</view>
|
||||
<view class="review-images" v-if="review.images && review.images.length > 0">
|
||||
<image
|
||||
class="review-image"
|
||||
v-for="(image, index) in review.images"
|
||||
:key="index"
|
||||
:src="image"
|
||||
mode="aspectFill"
|
||||
@click="previewImage(review.images, index)"
|
||||
/>
|
||||
</view>
|
||||
<view class="review-footer">
|
||||
<text class="review-time">{{ formatTime(review.time) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<view class="action-button secondary" @click="shareProduct">
|
||||
<text class="action-icon">📤</text>
|
||||
<text class="action-text">分享</text>
|
||||
</view>
|
||||
<view class="action-button primary" @click="buyProduct">
|
||||
<text class="action-text">去购买</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'ReviewDetailPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const product = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
brand: '',
|
||||
rating: 0,
|
||||
reviewCount: 0,
|
||||
priceRange: '',
|
||||
images: [],
|
||||
tags: [],
|
||||
nutrition: [],
|
||||
suitableFor: '',
|
||||
pros: [],
|
||||
cons: [],
|
||||
recommendation: 0,
|
||||
isFavorite: false
|
||||
})
|
||||
|
||||
const userReviews = ref([])
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadProductDetail()
|
||||
loadUserReviews()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const loadProductDetail = () => {
|
||||
// 获取产品ID
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const productId = currentPage.options.id || 'p1'
|
||||
|
||||
// 模拟产品详情数据
|
||||
const mockProduct = {
|
||||
id: productId,
|
||||
name: '成猫粮 室内猫配方',
|
||||
brand: '皇家',
|
||||
rating: 4.8,
|
||||
reviewCount: 1256,
|
||||
priceRange: '89-156',
|
||||
images: [
|
||||
'/static/product1.jpg',
|
||||
'/static/product1-2.jpg',
|
||||
'/static/product1-3.jpg'
|
||||
],
|
||||
tags: ['室内猫', '营养均衡', '毛球护理'],
|
||||
nutrition: [
|
||||
{ name: '粗蛋白', value: '≥32%' },
|
||||
{ name: '粗脂肪', value: '≥13%' },
|
||||
{ name: '粗纤维', value: '≤5.1%' },
|
||||
{ name: '粗灰分', value: '≤7.5%' },
|
||||
{ name: '水分', value: '≤10%' }
|
||||
],
|
||||
suitableFor: '1岁以上成年室内猫,特别适合运动量较少、容易发胖的室内猫咪',
|
||||
pros: [
|
||||
'营养配比科学,适合室内猫需求',
|
||||
'添加毛球护理配方,减少毛球形成',
|
||||
'颗粒大小适中,适口性好',
|
||||
'品牌信誉度高,质量有保障'
|
||||
],
|
||||
cons: [
|
||||
'价格相对较高',
|
||||
'部分猫咪可能需要适应期',
|
||||
'包装密封性有待改进'
|
||||
],
|
||||
recommendation: 4,
|
||||
isFavorite: false
|
||||
}
|
||||
|
||||
Object.assign(product, mockProduct)
|
||||
|
||||
// 检查收藏状态
|
||||
const favorites = uni.getStorageSync('reviewFavorites') || []
|
||||
product.isFavorite = favorites.includes(product.id)
|
||||
}
|
||||
|
||||
const loadUserReviews = () => {
|
||||
// 模拟用户评价数据
|
||||
const mockReviews = [
|
||||
{
|
||||
id: 'r1',
|
||||
userName: '铲屎官小王',
|
||||
avatar: '/static/avatar1.jpg',
|
||||
rating: 5,
|
||||
content: '我家猫咪很喜欢吃,而且吃了之后毛色变得更亮了,推荐!',
|
||||
images: ['/static/review1.jpg'],
|
||||
time: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'r2',
|
||||
userName: '猫奴一枚',
|
||||
avatar: '/static/avatar2.jpg',
|
||||
rating: 4,
|
||||
content: '质量不错,就是价格有点贵,不过为了猫主子的健康还是值得的。',
|
||||
images: [],
|
||||
time: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'r3',
|
||||
userName: '宠物达人',
|
||||
avatar: '/static/avatar3.jpg',
|
||||
rating: 5,
|
||||
content: '用了半年了,猫咪身体状况很好,体重控制得也不错,会继续购买。',
|
||||
images: ['/static/review2.jpg', '/static/review3.jpg'],
|
||||
time: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
]
|
||||
|
||||
userReviews.value = mockReviews
|
||||
}
|
||||
|
||||
const getRecommendationText = (score) => {
|
||||
const textMap = {
|
||||
5: '强烈推荐',
|
||||
4: '推荐',
|
||||
3: '一般推荐',
|
||||
2: '不太推荐',
|
||||
1: '不推荐'
|
||||
}
|
||||
return textMap[score] || '一般推荐'
|
||||
}
|
||||
|
||||
const toggleFavorite = () => {
|
||||
let favorites = uni.getStorageSync('reviewFavorites') || []
|
||||
const index = favorites.indexOf(product.id)
|
||||
|
||||
if (index > -1) {
|
||||
favorites.splice(index, 1)
|
||||
product.isFavorite = false
|
||||
} else {
|
||||
favorites.push(product.id)
|
||||
product.isFavorite = true
|
||||
}
|
||||
|
||||
uni.setStorageSync('reviewFavorites', favorites)
|
||||
|
||||
uni.showToast({
|
||||
title: product.isFavorite ? '已收藏' : '已取消收藏',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const previewImage = (images, current) => {
|
||||
uni.previewImage({
|
||||
urls: images,
|
||||
current: current
|
||||
})
|
||||
}
|
||||
|
||||
const formatTime = (timeString) => {
|
||||
if (!timeString) return ''
|
||||
const date = new Date(timeString)
|
||||
const now = new Date()
|
||||
const diffTime = now - date
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) {
|
||||
return '今天'
|
||||
} else if (diffDays === 1) {
|
||||
return '昨天'
|
||||
} else if (diffDays < 30) {
|
||||
return `${diffDays}天前`
|
||||
} else {
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
}
|
||||
|
||||
const shareProduct = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['分享到微信好友', '分享到朋友圈', '复制链接'],
|
||||
success: (res) => {
|
||||
const actions = ['微信好友', '朋友圈', '复制链接']
|
||||
uni.showToast({
|
||||
title: `已分享到${actions[res.tapIndex]}`,
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const buyProduct = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['淘宝', '京东', '天猫', '拼多多'],
|
||||
success: (res) => {
|
||||
const platforms = ['淘宝', '京东', '天猫', '拼多多']
|
||||
uni.showToast({
|
||||
title: `即将跳转到${platforms[res.tapIndex]}`,
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
product,
|
||||
userReviews,
|
||||
getRecommendationText,
|
||||
toggleFavorite,
|
||||
previewImage,
|
||||
formatTime,
|
||||
shareProduct,
|
||||
buyProduct
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-container {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.product-info-card,
|
||||
.review-card,
|
||||
.user-reviews-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 0 30rpx 24rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 产品信息卡片 */
|
||||
.product-info-card {
|
||||
.product-images {
|
||||
.product-swiper {
|
||||
height: 400rpx;
|
||||
|
||||
.swiper-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-details {
|
||||
padding: 32rpx;
|
||||
|
||||
.product-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.product-brand {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.product-favorite {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.favorite-icon {
|
||||
font-size: 28rpx;
|
||||
color: #CCCCCC;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
margin-bottom: 16rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.product-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.rating-stars {
|
||||
margin-right: 12rpx;
|
||||
|
||||
.star {
|
||||
font-size: 24rpx;
|
||||
color: #DDDDDD;
|
||||
|
||||
&.active {
|
||||
color: #FFD700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rating-score {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 600;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.rating-count {
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.product-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.price-label {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.price-range {
|
||||
font-size: 32rpx;
|
||||
color: #FF8A80;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.product-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
|
||||
.product-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
border-radius: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 评测卡片 */
|
||||
.review-card {
|
||||
padding: 32rpx;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.expert-info {
|
||||
.expert-name {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.review-content {
|
||||
.review-section {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.nutrition-list {
|
||||
.nutrition-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.nutrition-name {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.nutrition-value {
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pros-list,
|
||||
.cons-list {
|
||||
.pros-item,
|
||||
.cons-item {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.recommendation-stars {
|
||||
margin-right: 16rpx;
|
||||
|
||||
.rec-star {
|
||||
font-size: 28rpx;
|
||||
color: #DDDDDD;
|
||||
|
||||
&.active {
|
||||
color: #FF8A80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recommendation-text {
|
||||
font-size: 26rpx;
|
||||
color: #FF8A80;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 用户评价卡片 */
|
||||
.user-reviews-card {
|
||||
padding: 32rpx;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.reviews-count {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
.reviews-list {
|
||||
.review-item {
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.review-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.user-avatar {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.review-rating {
|
||||
.review-star {
|
||||
font-size: 20rpx;
|
||||
color: #DDDDDD;
|
||||
|
||||
&.active {
|
||||
color: #FFD700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.review-content {
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.review-text {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.review-images {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.review-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.review-footer {
|
||||
.review-time {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.bottom-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
padding: 20rpx 30rpx;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
z-index: 100;
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.secondary {
|
||||
background: rgba(255, 138, 128, 0.1);
|
||||
border: 2rpx solid #FF8A80;
|
||||
|
||||
.action-icon {
|
||||
font-size: 24rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 28rpx;
|
||||
color: #FF8A80;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
|
||||
|
||||
.action-text {
|
||||
font-size: 28rpx;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 375px) {
|
||||
.detail-container {
|
||||
.product-info-card,
|
||||
.review-card,
|
||||
.user-reviews-card {
|
||||
margin: 0 20rpx 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.product-details,
|
||||
.review-card,
|
||||
.user-reviews-card {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
padding: 16rpx 20rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-container > view {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
.detail-container > view:nth-child(1) { animation-delay: 0.1s; }
|
||||
.detail-container > view:nth-child(2) { animation-delay: 0.2s; }
|
||||
.detail-container > view:nth-child(3) { animation-delay: 0.3s; }
|
||||
|
||||
/* 交互反馈 */
|
||||
.product-favorite:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.review-image:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
Loading…
Reference in New Issue