This commit is contained in:
2025-08-14 08:52:56 +08:00
parent 58ac884203
commit c338c5d3d0
15 changed files with 10773 additions and 0 deletions
+908
View File
@@ -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>
+994
View File
@@ -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>
+815
View File
@@ -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>
+764
View File
@@ -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
+581
View File
@@ -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>