Compare commits

...
26 Commits
Author SHA1 Message Date
yvan bcc93fad2a wechat 2025-09-10 23:33:53 +08:00
yvan cb710b47df 1 2025-09-10 21:56:53 +08:00
yvan e78a7b95a1 1 2025-09-10 17:27:39 +08:00
yvan 6ab8f89770 1 2025-09-09 14:43:04 +08:00
yvan c191002cc6 1 2025-09-08 23:04:11 +08:00
yvan 2b4a3b6f1b 1 2025-09-05 23:35:32 +08:00
yvan dba965344a 1 2025-09-05 22:43:39 +08:00
yvan 913c2ba036 1 2025-09-05 21:39:28 +08:00
yvan 089c9f8494 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	http/api/profile.js
#	http/config/config.js
#	pages/profile/profile.vue
2025-09-05 21:16:59 +08:00
yvan 53155db139 1 2025-09-05 21:16:21 +08:00
yvan 304e6e94fc 1 2025-09-05 18:20:10 +08:00
yvan 3cbbd87a77 1 2025-09-05 17:35:26 +08:00
yvan 23123f3f9a 1 2025-09-05 17:35:21 +08:00
yvan b8ee12f68d 1 2025-08-23 18:04:38 +08:00
yvan 7629c52bb7 1 2025-08-23 17:43:36 +08:00
yvan f47534b583 1 2025-08-23 17:22:06 +08:00
yvan 1dde942ff1 1 2025-08-23 17:09:34 +08:00
yvan 6678bce527 1 2025-08-23 16:00:52 +08:00
yvan befb1dd8f9 1 2025-08-23 14:40:48 +08:00
yvan 7bc3d491b8 1 2025-08-23 14:28:20 +08:00
yvan 66aa21085e 1 2025-08-23 11:44:58 +08:00
yvan 2459662c5a 1 2025-08-23 10:26:42 +08:00
yvan 68aba1ad0c 1 2025-08-23 09:45:00 +08:00
yvan 4ae72d107c 1 2025-08-19 22:07:30 +08:00
yvan 10b714e441 1 2025-08-15 16:55:43 +08:00
yvan 2986b212a0 1 2025-08-15 16:52:03 +08:00
32 changed files with 5182 additions and 1019 deletions
+112
View File
@@ -1,13 +1,125 @@
<script> <script>
import { wxLogin } from '@/http/api/auth.js'
export default { export default {
onLaunch: function() { onLaunch: function() {
console.log('App Launch') console.log('App Launch')
this.initApp()
}, },
onShow: function() { onShow: function() {
console.log('App Show') console.log('App Show')
}, },
onHide: function() { onHide: function() {
console.log('App Hide') console.log('App Hide')
},
methods: {
// 应用初始化
async initApp() {
console.log('应用初始化开始')
await this.checkAutoLogin()
},
// 等待HTTP配置初始化
async waitForHttpInit() {
// 等待uni.$u.http可用
let retryCount = 0
const maxRetries = 10
while (!uni.$u || !uni.$u.http) {
if (retryCount >= maxRetries) {
throw new Error('HTTP配置初始化超时')
}
await new Promise(resolve => setTimeout(resolve, 100))
retryCount++
}
console.log('HTTP配置初始化完成')
},
// 检查自动登录
async checkAutoLogin() {
try {
// 检查本地是否有有效token
const token = uni.getStorageSync('token')
if (token) {
// 已有token,直接跳过WebSocket初始化
console.log('已有token,跳过WebSocket初始化')
return
}
// 等待HTTP配置初始化完成
await this.waitForHttpInit()
// 获取微信登录code
const loginRes = await new Promise((resolve, reject) => {
uni.login({
success: resolve,
fail: (error) => {
console.warn('微信登录失败:', error)
// 不reject,而是resolve一个空对象,避免阻止应用启动
resolve({ code: null, error })
}
})
})
if (loginRes.code) {
// 调用后端登录接口
const result = await uni.$u.http.post('/wechat/user/mini/login', {
code: loginRes.code
}, {
custom: {
loading: false,
toast: false,
auth: false
}
})
if (result && result.token) {
// 用户已授权过手机号,保存登录信息
await this.saveLoginData(result)
} else {
// 用户未授权手机号,保存微信登录code供后续使用
this.saveWxCode(loginRes.code)
}
}
} catch (error) {
// 静默处理错误,不影响应用正常启动
console.error('自动登录失败:', error)
}
},
// 保存登录数据
async saveLoginData(result) {
uni.setStorageSync('token', result.token)
if (result.refreshToken) {
uni.setStorageSync('refreshToken', result.refreshToken)
}
if (result.userInfo) {
uni.setStorageSync('wxUserInfo', result.userInfo)
uni.setStorageSync('userInfo', result.userInfo)
}
// 设置登录状态
const loginStep = result.userInfo && result.userInfo.profileCompleted
? 'profile_completed'
: 'phone_authed'
uni.setStorageSync('loginStep', loginStep)
// 跳过WebSocket初始化
console.log('登录成功,跳过WebSocket初始化')
},
// 保存微信code
saveWxCode(code) {
uni.setStorageSync('wxLoginCode', code)
uni.setStorageSync('loginDate', new Date().toISOString())
uni.setStorageSync('loginStep', 'wx_logged')
},
// WebSocket功能已移除,保留方法避免调用错误
async initWebSocketConnection(token) {
console.log('WebSocket功能已移除,使用HTTP API替代')
}
} }
} }
</script> </script>
+126
View File
@@ -0,0 +1,126 @@
/**
* 领养专区相关API接口模块
*/
import BaseRequest from '../utils/request-helper.js'
import { LOADING_TEXTS } from '../config/constants.js'
/**
* 获取待领养宠物列表
* @param {Object} params 查询参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getAdoptionPets = (params = {}, config = {}) => {
return BaseRequest.get('/adoption/pets', params, 'SILENT_REQUEST', config)
}
/**
* 搜索宠物
* @param {Object} searchParams 搜索参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const searchPets = (searchParams, config = {}) => {
return BaseRequest.get('/adoption/pets/search', searchParams, 'SILENT_REQUEST', config)
}
/**
* 筛选宠物
* @param {Object} filterParams 筛选参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const filterPets = (filterParams, config = {}) => {
return BaseRequest.post('/adoption/pets/filter', filterParams, 'PUBLIC_REQUEST', null, config)
}
/**
* 获取宠物详情
* @param {String|Number} petId 宠物ID
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getAdoptionPetDetail = (petId, config = {}) => {
return BaseRequest.get(`/adoption/pets/${petId}`, {}, 'PUBLIC_REQUEST', config)
}
/**
* 申请领养
* @param {Object} adoptionData 领养申请数据
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const applyAdoption = (adoptionData, config = {}) => {
return BaseRequest.post('/adoption/apply', adoptionData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.SUBMIT_APPLICATION, config)
}
/**
* 获取我的领养申请
* @param {Object} params 查询参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getMyAdoptionApplications = (params = {}, config = {}) => {
return BaseRequest.get('/adoption/my-applications', params, 'AUTHENTICATED_QUERY_WITH_LOADING', config)
}
/**
* 取消领养申请
* @param {String|Number} applicationId 申请ID
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const cancelAdoptionApplication = (applicationId, config = {}) => {
return BaseRequest.delete(`/adoption/applications/${applicationId}`, {}, 'AUTHENTICATED_DELETE', LOADING_TEXTS.CANCEL_APPLICATION, config)
}
/**
* 获取领养机构列表
* @param {Object} params 查询参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getAdoptionOrganizations = (params = {}, config = {}) => {
return BaseRequest.get('/adoption/organizations', params, 'PUBLIC_REQUEST', config)
}
/**
* 获取领养机构详情
* @param {String|Number} orgId 机构ID
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getOrganizationDetail = (orgId, config = {}) => {
return BaseRequest.get(`/adoption/organizations/${orgId}`, {}, 'PUBLIC_REQUEST', config)
}
/**
* 收藏宠物
* @param {String|Number} petId 宠物ID
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const favoritePet = (petId, config = {}) => {
return BaseRequest.post(`/adoption/pets/${petId}/favorite`, {}, 'AUTHENTICATED_UPDATE', null, config)
}
/**
* 取消收藏宠物
* @param {String|Number} petId 宠物ID
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const unfavoritePet = (petId, config = {}) => {
return BaseRequest.delete(`/adoption/pets/${petId}/favorite`, {}, 'AUTHENTICATED_DELETE', null, config)
}
/**
* 获取收藏的宠物列表
* @param {Object} params 查询参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getFavoritePets = (params = {}, config = {}) => {
return BaseRequest.get('/adoption/favorites', params, 'AUTHENTICATED_QUERY_WITH_LOADING', config)
}
+213
View File
@@ -0,0 +1,213 @@
/**
* 宠物助手相关API接口模块
* 对接后端宠物助手服务接口
*/
import BaseRequest from '../utils/request-helper.js'
import { LOADING_TEXTS } from '../config/constants.js'
// ==================== 宠物助手聊天接口 ====================
/**
* 向宠物助手提问(非流式)
* @description 发送消息给宠物助手,获取专业的宠物护理建议
* @param {Object} messageData 消息数据对象
* @param {string} messageData.message 用户消息内容,必填,1-2000字符
* @param {string} [messageData.sessionId] 会话ID,可选,UUID格式
* @param {boolean} [messageData.stream=false] 是否流式响应
* @param {number} [messageData.temperature=0.7] 温度参数,控制随机性,范围0-2
* @param {number} [messageData.maxTokens=1000] 最大生成token数,范围1-4000
* @param {string} [messageData.model] 模型名称,可选
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回聊天响应对象
* @example
* // 基本用法
* const response = await askPetAssistant({
* message: '我的狗狗不吃饭怎么办?'
* })
*
* // 指定会话和参数
* const response = await askPetAssistant({
* message: '还有其他建议吗?',
* sessionId: 'uuid-session-id',
* temperature: 0.8,
* maxTokens: 1500
* })
*/
export const askPetAssistant = (messageData, config = {}) => {
return BaseRequest.post('/pet/user/assistant/ask', messageData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.AI_THINKING, config)
}
/**
* 向宠物助手流式提问
* @description 发送消息给宠物助手,获取流式响应(实时显示回复过程)
* @param {Object} messageData 消息数据对象
* @param {string} messageData.message 用户消息内容
* @param {string} [messageData.sessionId] 会话ID,可选
* @param {number} [messageData.temperature=0.7] 温度参数
* @param {number} [messageData.maxTokens=1000] 最大token数
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回流式响应对象
* @example
* const response = await streamAskPetAssistant({
* message: '我的猫咪呕吐是什么原因?',
* sessionId: 'uuid-session-id'
* })
*/
export const streamAskPetAssistant = (messageData, config = {}) => {
// 强制设置为流式响应
const streamData = { ...messageData, stream: true }
return BaseRequest.post('/pet/user/assistant/stream-ask', streamData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.AI_THINKING, config)
}
/**
* 获取宠物助手对话历史
* @description 获取用户与宠物助手的对话历史记录(单一会话)
* @param {Object} [params={}] 查询参数
* @param {number} [params.page=1] 页码,默认第1页
* @param {number} [params.pageSize=20] 每页数量,默认20条
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回历史记录列表
* @example
* // 获取历史记录
* const history = await getAssistantHistory({
* page: 1,
* pageSize: 50
* })
*/
export const getAssistantHistory = (params = {}, config = {}) => {
return BaseRequest.get('/pet/user/assistant/history', params, 'AUTHENTICATED_QUERY', config)
}
/**
* 清空宠物助手对话历史
* @description 清空用户与宠物助手的对话历史记录
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回操作结果
* @example
* // 清空历史记录
* await clearAssistantHistory()
*/
export const clearAssistantHistory = (config = {}) => {
return BaseRequest.delete('/pet/user/assistant/clear-history', {}, 'AUTHENTICATED_DELETE', '正在清除历史记录...', config)
}
// ==================== 兼容性接口(保持向后兼容) ====================
/**
* 发送消息给AI助手(兼容旧版本)
* @deprecated 请使用 askPetAssistant 替代
* @param {Object} messageData 消息数据
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const sendMessage = (messageData, config = {}) => {
console.warn('sendMessage is deprecated, please use askPetAssistant instead')
return askPetAssistant(messageData, config)
}
/**
* 获取聊天历史(兼容旧版本)
* @deprecated 请使用 getAssistantHistory 替代
* @param {Object} params 查询参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getChatHistory = (params = {}, config = {}) => {
console.warn('getChatHistory is deprecated, please use getAssistantHistory instead')
return getAssistantHistory(params, config)
}
/**
* 清除聊天历史(兼容旧版本)
* @deprecated 请使用 clearAssistantHistory 替代
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const clearChatHistory = (config = {}) => {
console.warn('clearChatHistory is deprecated, please use clearAssistantHistory instead')
return clearAssistantHistory({}, config)
}
// ==================== 工具函数 ====================
/**
* 验证消息内容
* @description 验证用户输入的消息是否符合要求
* @param {string} message 消息内容
* @returns {Object} 验证结果 { valid: boolean, error?: string }
*/
export const validateMessage = (message) => {
if (!message || typeof message !== 'string') {
return { valid: false, error: '消息内容不能为空' }
}
const trimmedMessage = message.trim()
if (trimmedMessage.length === 0) {
return { valid: false, error: '消息内容不能为空' }
}
if (trimmedMessage.length > 2000) {
return { valid: false, error: '消息内容不能超过2000个字符' }
}
return { valid: true }
}
/**
* 格式化聊天消息
* @description 将API返回的消息格式化为前端显示格式
* @param {Object} apiMessage API返回的消息对象
* @returns {Object} 格式化后的消息对象
*/
export const formatChatMessage = (apiMessage) => {
return {
id: apiMessage.id,
type: apiMessage.role === 'user' ? 'user' : 'ai',
content: apiMessage.messageContent || apiMessage.message || '',
time: formatMessageTime(apiMessage.createdAt || apiMessage.created_at),
sessionId: apiMessage.sessionId,
isSensitive: apiMessage.isSensitive || false,
tokenCount: apiMessage.tokenCount || 0,
responseTime: apiMessage.responseTime || 0
}
}
/**
* 格式化消息时间
* @description 将时间戳或时间字符串格式化为显示格式
* @param {string|number|Date} timestamp 时间戳或时间对象
* @returns {string} 格式化后的时间字符串
*/
export const formatMessageTime = (timestamp) => {
if (!timestamp) return ''
const date = new Date(timestamp)
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
const timeStr = date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false
})
if (messageDate.getTime() === today.getTime()) {
return timeStr
} else if (messageDate.getTime() === today.getTime() - 24 * 60 * 60 * 1000) {
return `昨天 ${timeStr}`
} else {
return date.toLocaleDateString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
}
+151
View File
@@ -0,0 +1,151 @@
/**
* 用户认证相关API接口模块
* 提供用户登录、注册、认证、短信验证等相关的API接口
*/
import BaseRequest from '../utils/request-helper.js'
import { LOADING_TEXTS } from '../config/constants.js'
// ==================== 用户登录相关API ====================
/**
* 用户账号密码登录
* @param {Object} loginData 登录数据对象
* @param {string} loginData.username 用户名或邮箱
* @param {string} loginData.password 用户密码
* @param {boolean} [loginData.rememberMe] 是否记住登录状态
* @param {Object} [config] 自定义请求配置
* @returns {Promise} 返回认证结果,包含token和用户信息
*/
export const userLogin = (loginData, config = {}) => {
return BaseRequest.post('/auth/login', loginData, 'PUBLIC_REQUEST', LOADING_TEXTS.LOGIN, config)
}
/**
* 微信授权登录
* @param {Object} wxData 微信登录数据对象
* @param {string} wxData.code 微信登录凭证code
* @param {string} [wxData.encryptedData] 加密的用户数据
* @param {string} [wxData.iv] 初始向量
* @param {Object} [config] 自定义请求配置
* @returns {Promise} 返回认证结果,包含token和用户信息
*/
export const wxLogin = (wxData, config = {}) => {
return BaseRequest.post('/wechat/user/mini/login', wxData, 'PUBLIC_REQUEST', LOADING_TEXTS.LOGIN, config)
}
/**
* 微信手机号登录
* @param {Object} phoneData 手机号登录数据对象
* @param {string} phoneData.code 微信登录code
* @param {string} phoneData.encryptedData 加密的手机号数据
* @param {string} phoneData.iv 初始向量
* @param {Object} [config] 自定义请求配置
* @returns {Promise} 返回登录结果,包含token和用户信息
*/
export const wxPhoneLogin = (phoneData, config = {}) => {
return BaseRequest.post('/wechat/user/mini/phone-login', phoneData, 'PUBLIC_REQUEST', LOADING_TEXTS.LOGIN, config)
}
/**
* 更新用户手机号
* @param {Object} phoneData 手机号授权数据对象
* @param {string} phoneData.encryptedData 加密的手机号数据
* @param {string} phoneData.iv 初始向量
* @param {Object} [config] 自定义请求配置
* @returns {Promise} 返回解密后的手机号
*/
export const updatePhoneNumber = (phoneData, config = {}) => {
return BaseRequest.post('/user/wechat/mini/phone-update', phoneData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.PHONE_VERIFY, config)
}
/**
* 获取用户信息
* @param {Object} [config] 自定义请求配置
* @returns {Promise} 返回用户信息
*/
export const getUserInfo = (config = {}) => {
return BaseRequest.get('/user/wechat/mini/userinfo', {}, 'AUTHENTICATED_QUERY_WITH_LOADING', config)
}
/**
* 更新用户信息
* @param {Object} userData 用户数据对象
* @param {Object} [config] 自定义请求配置
* @returns {Promise} 返回更新结果
*/
export const updateUserInfo = (userData, config = {}) => {
return BaseRequest.put('/user/wechat/mini/userinfo', userData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.UPDATING_USER_INFO, config)
}
// ==================== 用户注册相关API ====================
/**
* 用户账号注册
* @param {Object} registerData 注册数据对象
* @param {string} registerData.username 用户名
* @param {string} registerData.password 密码
* @param {string} registerData.email 邮箱地址
* @param {string} [registerData.phoneNumber] 手机号码
* @param {string} [registerData.inviteCode] 邀请码
* @param {Object} [config] 自定义请求配置
* @returns {Promise} 返回注册结果,包含token和用户信息
*/
export const userRegister = (registerData, config = {}) => {
return BaseRequest.post('/auth/register', registerData, 'PUBLIC_REQUEST', LOADING_TEXTS.REGISTER, config)
}
// ==================== 会话管理相关API ====================
/**
* 用户登出
* @param {Object} [config] 自定义请求配置
* @returns {Promise} 返回登出结果
*/
export const userLogout = (config = {}) => {
return BaseRequest.post('/auth/logout', {}, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.LOGOUT, config)
}
/**
* 刷新访问令牌
* @param {Object} [config] 自定义请求配置
* @param {string} [config.refreshToken] 自定义刷新令牌,不传则从本地存储获取
* @returns {Promise} 返回新的token信息
*/
export const refreshToken = (config = {}) => {
const refreshTokenValue = config.refreshToken || uni.getStorageSync('refreshToken')
return BaseRequest.post('/auth/refresh', { refreshToken: refreshTokenValue }, 'SILENT_REQUEST', null, config)
}
// ==================== 短信验证相关API ====================
/**
* 发送短信验证码
* @param {Object} phoneData 手机号数据对象
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回发送结果
*/
export const sendSmsCode = (phoneData, config = {}) => {
return BaseRequest.post('/sms/send', phoneData, 'PUBLIC_REQUEST', LOADING_TEXTS.SEND_SMS, config)
}
/**
* 验证短信验证码
* @param {Object} verifyData 验证数据对象
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回验证结果
*/
export const verifySmsCode = (verifyData, config = {}) => {
return BaseRequest.post('/sms/verify', verifyData, 'PUBLIC_REQUEST', LOADING_TEXTS.VERIFY_SMS, config)
}
/**
* 重置用户密码
* @param {Object} resetData 重置密码数据对象
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回重置结果
*/
export const resetPassword = (resetData, config = {}) => {
return BaseRequest.post('/auth/reset-password', resetData, 'PUBLIC_REQUEST', LOADING_TEXTS.RESET_PASSWORD, config)
}
+232
View File
@@ -0,0 +1,232 @@
/**
* 通用API接口模块
* 提供文件上传、系统配置、地区数据等通用功能的API接口
*/
import BaseRequest from '../utils/request-helper.js'
import { LOADING_TEXTS } from '../config/constants.js'
// ==================== API方法 ====================
// ==================== 文件上传相关API ====================
/**
* 上传单张图片
* @param {Object} imageData 图片上传数据对象
* @param {Object} config 自定义请求配置
* @returns {Promise} 返回上传结果
*/
export const uploadImage = (imageData, config = {}) => {
return BaseRequest.upload('/upload/image', imageData, LOADING_TEXTS.UPLOAD_IMAGE, config)
}
/**
* 批量上传多张图片
* @param {Array} imageList 图片列表数组
* @param {Object} config 自定义请求配置
* @returns {Promise} 返回所有图片的上传结果数组
*/
export const uploadImages = async (imageList, config = {}) => {
const { maxConcurrent = 3, showProgress = true } = config
// 显示整体进度loading
if (showProgress) {
uni.showLoading({
title: config.custom?.loadingText || COMMON_LOADING_TEXTS.UPLOAD_IMAGES
})
}
try {
// 分批并发上传
const results = []
for (let i = 0; i < imageList.length; i += maxConcurrent) {
const batch = imageList.slice(i, i + maxConcurrent)
const batchPromises = batch.map(imageData => {
return uploadImage(imageData, {
...config,
custom: {
loading: false, // 批量上传时不显示单个loading
...config.custom
}
})
})
const batchResults = await Promise.all(batchPromises)
results.push(...batchResults)
}
return results
} finally {
if (showProgress) {
uni.hideLoading()
}
}
}
/**
* 上传通用文件
* @param {Object} fileData 文件上传数据对象
* @param {Object} config 自定义请求配置
* @returns {Promise} 返回上传结果
*/
export const uploadFile = (fileData, config = {}) => {
return BaseRequest.upload('/upload/file', fileData, LOADING_TEXTS.UPLOAD_FILE, config)
}
/**
* 下载文件到本地
* @param {string} url 文件下载URL
* @param {Object} config 自定义请求配置
* @returns {Promise} 返回下载结果
*/
export const downloadFile = (url, config = {}) => {
return BaseRequest.download(url, config.savePath, LOADING_TEXTS.DOWNLOAD_FILE, config)
}
// ==================== 云存储相关API ====================
/**
* 获取七牛云上传凭证
* @param {Object} params 请求参数
* @param {Object} config 自定义请求配置
* @returns {Promise} 返回七牛云上传配置信息
*/
export const getQiniuToken = (params = {}, config = {}) => {
return BaseRequest.get('/upload/qiniu-token', params, 'SILENT_REQUEST', config)
}
/**
* 获取阿里云OSS上传签名
* @param {Object} params 请求参数
* @param {Object} config 自定义请求配置
* @returns {Promise} 返回OSS上传配置信息
*/
export const getOSSSignature = (params = {}, config = {}) => {
return BaseRequest.get('/upload/oss-signature', params, 'SILENT_REQUEST', config)
}
// ==================== 系统信息相关API ====================
/**
* 获取系统配置信息
* @param {Object} params 查询参数
* @param {Object} config 自定义请求配置
* @returns {Promise} 返回系统配置对象
*/
export const getSystemConfig = (params = {}, config = {}) => {
return BaseRequest.get('/system/config', params, 'SILENT_REQUEST', config)
}
/**
* 获取应用版本信息
* @description 获取当前应用的版本信息和更新历史
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回版本信息对象
* @example
* // 获取版本信息
* const versionInfo = await getVersionInfo()
* // 返回格式:
* // {
* // currentVersion: '1.0.0',
* // latestVersion: '1.1.0',
* // updateAvailable: true,
* // updateLog: ['修复bug', '新增功能']
* // }
*/
export const getVersionInfo = (config = {}) => {
return BaseRequest.get('/system/version', {}, 'SILENT_REQUEST', config)
}
/**
* 检查应用更新
* @description 检查是否有新版本可用,并获取更新信息
* @param {Object} versionData 当前版本数据
* @param {string} versionData.currentVersion 当前版本号
* @param {string} versionData.platform 平台:'android' | 'ios' | 'h5' | 'mp-weixin'
* @param {string} [versionData.channel] 更新渠道
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回更新检查结果
* @example
* // 检查更新
* const updateInfo = await checkUpdate({
* currentVersion: '1.0.0',
* platform: 'android'
* })
*
* // 指定更新渠道
* const updateInfo = await checkUpdate({
* currentVersion: '1.0.0',
* platform: 'android',
* channel: 'beta'
* })
*/
export const checkUpdate = (versionData, config = {}) => {
return BaseRequest.post('/system/check-update', versionData, 'PUBLIC_REQUEST', LOADING_TEXTS.CHECK_UPDATE, config)
}
// 短信验证码相关API已移至 http/api/auth.js 文件中
// ==================== 基础数据相关API ====================
/**
* 获取地区数据
* @description 获取省市区三级联动的地区数据
* @param {Object} [params={}] 查询参数
* @param {string} [params.level] 数据层级:'province' | 'city' | 'district' | 'all'
* @param {string} [params.parentCode] 父级地区代码
* @param {boolean} [params.includeCoordinates] 是否包含经纬度信息
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object[]>} 返回地区数据数组
* @example
* // 获取所有省份
* const provinces = await getRegionData({ level: 'province' })
*
* // 获取指定省份下的城市
* const cities = await getRegionData({
* level: 'city',
* parentCode: '110000'
* })
*
* // 获取完整的三级数据
* const allRegions = await getRegionData({ level: 'all' })
*/
export const getRegionData = (params = {}, config = {}) => {
return BaseRequest.get('/common/regions', params, 'SILENT_REQUEST', config)
}
/**
* 提交用户反馈
* @description 提交用户的意见反馈或问题报告
* @param {Object} feedbackData 反馈数据对象
* @param {string} feedbackData.type 反馈类型:'bug' | 'suggestion' | 'complaint' | 'other'
* @param {string} feedbackData.title 反馈标题
* @param {string} feedbackData.content 反馈内容
* @param {string} [feedbackData.contact] 联系方式
* @param {string[]} [feedbackData.images] 相关图片URL数组
* @param {Object} [feedbackData.deviceInfo] 设备信息
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回提交结果,包含反馈ID
* @example
* // 提交bug反馈
* const result = await submitFeedback({
* type: 'bug',
* title: '登录页面异常',
* content: '点击登录按钮后页面卡死',
* contact: 'user@example.com',
* images: ['https://example.com/screenshot.jpg']
* })
*
* // 提交功能建议
* const result = await submitFeedback({
* type: 'suggestion',
* title: '希望增加夜间模式',
* content: '建议应用支持夜间模式,保护用户视力'
* })
*/
export const submitFeedback = (feedbackData, config = {}) => {
return BaseRequest.post('/feedback', feedbackData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.SUBMIT_FEEDBACK, config)
}
+30
View File
@@ -0,0 +1,30 @@
// API接口统一导出文件
// 认证相关API
export * from './auth.js'
// 用户信息相关API
export * from './profile.js'
// 宠物相关API
export * from './pets.js'
// 领养相关API
export * from './adoption.js'
// 评价相关API
export * from './review.js'
// AI助手相关API
export * from './assistant.js'
// 通用API
export * from './common.js'
// 为了向后兼容,保留一些常用的别名导出
export {
wxPhoneLogin as phoneLogin,
userLogin as login,
userLogout as logout,
userRegister as register
} from './auth.js'
+180
View File
@@ -0,0 +1,180 @@
/**
* 宠物管理相关API接口模块
*/
import BaseRequest from '../utils/request-helper.js'
import { LOADING_TEXTS } from '../config/constants.js'
// ==================== API方法 ====================
// ==================== 宠物基础信息管理API ====================
/**
* 获取用户宠物列表
* @param {Object} [params={}] 查询参数
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回宠物列表和分页信息
*/
export const getPetsList = (params = {}, config = {}) => {
return BaseRequest.get('/pets', params, 'AUTHENTICATED_QUERY', config)
}
/**
* 获取宠物详细信息
* @param {string|number} petId 宠物ID
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise} 返回宠物详细信息
*/
export const getPetDetail = (petId, config = {}) => {
const params = {}
if (config.includeRecords) params.includeRecords = true
if (config.includeHealth) params.includeHealth = true
return BaseRequest.get(`/pets/${petId}`, params, 'AUTHENTICATED_QUERY_WITH_LOADING', config)
}
/**
* 添加新宠物
* @param {Object} petData 宠物信息数据
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise} 返回新添加的宠物信息
*/
export const addPet = (petData, config = {}) => {
return BaseRequest.post('/pets', petData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.ADD_PET, config)
}
/**
* 更新宠物信息
* @param {string|number} petId 宠物ID
* @param {Object} petData 要更新的宠物数据
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise} 返回更新后的宠物信息
*/
export const updatePet = (petId, petData, config = {}) => {
return BaseRequest.put(`/pets/${petId}`, petData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.UPDATE_PET, config)
}
/**
* 删除宠物
* @param {string|number} petId 宠物ID
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回删除结果
*/
export const deletePet = (petId, config = {}) => {
const data = config.force ? { force: true } : {}
return BaseRequest.delete(`/pets/${petId}`, data, 'AUTHENTICATED_DELETE', LOADING_TEXTS.DELETE_PET, config)
}
// ==================== 宠物记录管理API ====================
/**
* 获取宠物记录列表
* @param {string|number} petId 宠物ID
* @param {Object} [params={}] 查询参数
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回记录列表和分页信息
*/
export const getPetRecords = (petId, params = {}, config = {}) => {
return BaseRequest.get(`/pets/${petId}/records`, params, 'AUTHENTICATED_QUERY', config)
}
/**
* 添加宠物记录
* @description 为指定宠物添加一条新记录
* @param {string|number} petId 宠物ID
* @param {Object} recordData 记录数据
* @param {string} recordData.type 记录类型:'feeding' | 'health' | 'exercise' | 'grooming' | 'other'
* @param {string} recordData.title 记录标题
* @param {string} recordData.content 记录内容
* @param {string} [recordData.recordTime] 记录时间,不传则使用当前时间
* @param {string[]} [recordData.images] 相关图片URL数组
* @param {Object} [recordData.metadata] 额外的元数据(如体重、体温等)
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<PetRecord>} 返回新添加的记录信息
* @example
* // 添加喂食记录
* const feedingRecord = await addPetRecord(123, {
* type: 'feeding',
* title: '晚餐',
* content: '吃了狗粮200g',
* recordTime: '2024-01-15 18:00:00'
* })
*
* // 添加健康记录
* const healthRecord = await addPetRecord(123, {
* type: 'health',
* title: '体重测量',
* content: '今日体重测量结果',
* images: ['https://example.com/weight.jpg'],
* metadata: {
* weight: 25.5,
* temperature: 38.5
* }
* })
*/
export const addPetRecord = (petId, recordData, config = {}) => {
return BaseRequest.post(`/pets/${petId}/records`, recordData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.ADD_RECORD, config)
}
/**
* 更新宠物记录
* @description 更新指定的宠物记录信息
* @param {string|number} petId 宠物ID
* @param {string|number} recordId 记录ID
* @param {Object} recordData 要更新的记录数据
* @param {string} [recordData.title] 记录标题
* @param {string} [recordData.content] 记录内容
* @param {string} [recordData.recordTime] 记录时间
* @param {string[]} [recordData.images] 相关图片URL数组
* @param {Object} [recordData.metadata] 额外的元数据
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<PetRecord>} 返回更新后的记录信息
* @example
* // 更新记录内容
* const updatedRecord = await updatePetRecord(123, 456, {
* content: '更新后的记录内容',
* metadata: { weight: 26.0 }
* })
*/
export const updatePetRecord = (petId, recordId, recordData, config = {}) => {
return BaseRequest.put(`/pets/${petId}/records/${recordId}`, recordData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.UPDATE_RECORD, config)
}
/**
* 删除宠物记录
* @description 删除指定的宠物记录
* @param {string|number} petId 宠物ID
* @param {string|number} recordId 记录ID
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回删除结果
* @example
* // 删除记录
* await deletePetRecord(123, 456)
*/
export const deletePetRecord = (petId, recordId, config = {}) => {
return BaseRequest.delete(`/pets/${petId}/records/${recordId}`, {}, 'AUTHENTICATED_DELETE', LOADING_TEXTS.DELETE_RECORD, config)
}
// ==================== 宠物健康档案API ====================
/**
* 获取宠物健康数据
* @param {string|number} petId 宠物ID
* @param {Object} params 查询参数
* @param {Object} config 自定义请求配置
* @returns {Promise} 返回健康数据
*/
export const getPetHealthData = (petId, params = {}, config = {}) => {
return BaseRequest.get(`/pets/${petId}/health`, params, 'AUTHENTICATED_QUERY_WITH_LOADING', config)
}
/**
* 获取宠物成长时间线
* @param {string|number} petId 宠物ID
* @param {Object} params 查询参数
* @param {Object} config 自定义请求配置
* @returns {Promise} 返回时间线事件数组
*/
export const getPetTimeline = (petId, params = {}, config = {}) => {
return BaseRequest.get(`/pets/${petId}/timeline`, params, 'AUTHENTICATED_QUERY_WITH_LOADING', config)
}
+251
View File
@@ -0,0 +1,251 @@
/**
* 个人中心相关API接口模块
* 提供用户信息管理、资料完善、偏好设置等相关的API接口
*/
import BaseRequest from '../utils/request-helper.js'
import { LOADING_TEXTS } from '../config/constants.js'
// ==================== API方法 ====================
// ==================== 用户信息相关API ====================
/**
* 获取用户基本信息
* @description 获取当前登录用户的基本信息,包括昵称、头像、个人资料等
* @param {Object} [config={}] 自定义请求配置
* @param {Object} [config.custom] 自定义请求选项
* @param {boolean} [config.custom.loading] 是否显示loading,默认true
* @param {boolean} [config.custom.toast] 是否显示错误提示,默认true
* @returns {Promise<Object>} 返回用户信息对象
* @example
* // 基本用法
* const userInfo = await getUserInfo()
*
* // 自定义配置
* const userInfo = await getUserInfo({
* custom: { loading: false }
* })
*/
export const getUserInfo = (config = {}) => {
return BaseRequest.get('/user/info', {}, 'AUTHENTICATED_QUERY_WITH_LOADING', config)
}
/**
* 获取微信用户信息
* @description 获取当前登录用户的微信相关信息,包括微信昵称、头像等
* @param {Object} [config={}] 自定义请求配置
* @param {Object} [config.custom] 自定义请求选项
* @param {boolean} [config.custom.loading] 是否显示loading,默认false
* @param {boolean} [config.custom.toast] 是否显示错误提示,默认true
* @returns {Promise<Object>} 返回微信用户信息对象
* @example
* // 基本用法
* const wxUserInfo = await getWxUserInfo()
*
* // 自定义配置
* const wxUserInfo = await getWxUserInfo({
* custom: { loading: true }
* })
*/
export const getWxUserInfo = (config = {}) => {
return BaseRequest.get('/user/wechat/mini/userinfo', {}, 'AUTHENTICATED_QUERY', config)
}
/**
* 更新用户基本信息
* @description 更新用户的基本信息,如昵称、头像、个人简介等
* @param {Object} userInfo 用户信息对象
* @param {string} [userInfo.nickName] 用户昵称
* @param {string} [userInfo.avatarUrl] 头像URL
* @param {string} [userInfo.bio] 个人简介
* @param {string} [userInfo.gender] 性别
* @param {string} [userInfo.birthday] 生日
* @param {string} [userInfo.region] 地区
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回更新后的用户信息
* @example
* await updateUserInfo({
* nickName: '新昵称',
* bio: '个人简介'
* })
*/
export const updateUserInfo = (userInfo, config = {}) => {
return BaseRequest.put('/user/info', userInfo, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.UPDATING_USER_INFO, config)
}
/**
* 获取用户宠物列表
* @description 获取当前用户的所有宠物信息列表
* @param {Object} [params={}] 查询参数
* @param {number} [params.page] 页码,默认1
* @param {number} [params.pageSize] 每页数量,默认20
* @param {string} [params.type] 宠物类型筛选
* @param {string} [params.status] 状态筛选
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回宠物列表和分页信息
* @example
* // 获取所有宠物
* const pets = await getUserPets()
*
* // 分页查询
* const pets = await getUserPets({ page: 1, pageSize: 10 })
*/
export const getUserPets = (params = {}, config = {}) => {
return BaseRequest.get('/user/pets', params, 'AUTHENTICATED_QUERY', config)
}
// ==================== 用户统计相关API ====================
/**
* 获取用户统计数据
* @description 获取用户的各项统计数据,如宠物数量、记录数量、提醒数量等
* @param {Object} [config={}] 自定义请求配置
* @param {Object} [config.custom] 自定义请求选项
* @param {boolean} [config.custom.loading] 是否显示loading,默认false(后台获取)
* @returns {Promise<Object>} 返回统计数据对象
* @example
* const stats = await getUserStats()
* // 返回格式:
* // {
* // petCount: 3,
* // recordCount: 25,
* // reminderCount: 5,
* // familyMemberCount: 2
* // }
*/
export const getUserStats = (config = {}) => {
return BaseRequest.get('/user/stats', {}, 'AUTHENTICATED_QUERY', config)
}
// ==================== 账户管理相关API ====================
/**
* 注销用户账户
* @description 永久删除用户账户及所有相关数据,此操作不可逆
* @param {Object} [config={}] 自定义请求配置
* @param {Object} [config.custom] 自定义请求选项
* @param {boolean} [config.custom.loading] 是否显示loading,默认true
* @param {string} [config.custom.loadingText] 自定义loading文本
* @returns {Promise<Object>} 返回注销结果
* @example
* await deleteAccount()
*
* @warning 此操作将永久删除所有用户数据,请谨慎使用
*/
export const deleteAccount = (config = {}) => {
return BaseRequest.delete('/user/account', {}, 'AUTHENTICATED_DELETE', LOADING_TEXTS.DELETING_ACCOUNT, config)
}
// ==================== 用户资料完善相关API ====================
/**
* 完善用户资料信息
* @description 用于新用户首次登录后完善个人资料信息,或更新现有资料
* @param {Object} profileData 用户资料数据对象
* @param {string} profileData.nickName 用户昵称(必填)
* @param {string} [profileData.avatarUrl] 头像URL
* @param {string} [profileData.gender] 性别:'男' | '女' | '保密'
* @param {string} [profileData.birthday] 生日,格式:YYYY-MM-DD
* @param {string} [profileData.region] 所在地区
* @param {string} [profileData.bio] 个人简介
* @param {Object} [config={}] 自定义请求配置
* @param {Object} [config.custom] 自定义请求选项
* @param {boolean} [config.custom.loading] 是否显示loading,默认true
* @param {string} [config.custom.loadingText] 自定义loading文本
* @returns {Promise<Object>} 返回完善后的用户信息
* @example
* // 基本用法
* await completeUserProfile({
* nickName: '小明',
* gender: '男',
* birthday: '1990-01-01',
* region: '北京市',
* bio: '热爱宠物的程序员'
* })
*
* // 自定义loading文本
* await completeUserProfile(profileData, {
* custom: { loadingText: '正在创建用户资料...' }
* })
*/
export const completeUserProfile = (profileData, config = {}) => {
return BaseRequest.post('/user/profile/complete', profileData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.SAVING_PROFILE, config)
}
// ==================== 头像上传相关API ====================
/**
* 上传用户头像
* @description 上传并更新用户头像图片
* @param {Object} avatarData 头像数据对象
* @param {string} avatarData.avatarUrl 头像图片URL或base64数据
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回上传结果和新的头像URL
* @example
* const result = await uploadAvatar({
* avatarUrl: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...'
* })
*/
export const uploadAvatar = (avatarData, config = {}) => {
return BaseRequest.post('/user/avatar', avatarData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.UPLOADING_AVATAR, config)
}
// ==================== 用户偏好设置相关API ====================
/**
* 获取用户偏好设置
* @description 获取用户的个性化偏好设置,如通知设置、隐私设置等
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回用户偏好设置对象
* @example
* const preferences = await getUserPreferences()
*/
export const getUserPreferences = (config = {}) => {
return BaseRequest.get('/user/preferences', {}, 'AUTHENTICATED_QUERY', config)
}
/**
* 更新用户偏好设置
* @description 更新用户的个性化偏好设置
* @param {Object} preferences 偏好设置对象
* @param {boolean} [preferences.notificationEnabled] 是否启用通知
* @param {boolean} [preferences.privacyMode] 是否启用隐私模式
* @param {string} [preferences.theme] 主题设置:'light' | 'dark' | 'auto'
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回更新后的偏好设置
* @example
* await updateUserPreferences({
* notificationEnabled: true,
* theme: 'dark'
* })
*/
export const updateUserPreferences = (preferences, config = {}) => {
return BaseRequest.put('/user/preferences', preferences, 'AUTHENTICATED_UPDATE', '正在保存设置...', config)
}
// ==================== 用户活动记录相关API ====================
/**
* 获取用户活动记录
* @description 获取用户的操作活动记录列表
* @param {Object} [params={}] 查询参数
* @param {number} [params.page] 页码,默认1
* @param {number} [params.pageSize] 每页数量,默认20
* @param {string} [params.type] 活动类型筛选
* @param {string} [params.startDate] 开始日期
* @param {string} [params.endDate] 结束日期
* @param {Object} [config={}] 自定义请求配置
* @returns {Promise<Object>} 返回活动记录列表和分页信息
* @example
* const activities = await getUserActivities({
* page: 1,
* pageSize: 10,
* type: 'pet_care'
* })
*/
export const getUserActivities = (params = {}, config = {}) => {
return BaseRequest.get('/user/activities', params, 'AUTHENTICATED_QUERY', config)
}
+122
View File
@@ -0,0 +1,122 @@
/**
* 评价相关API接口模块
*/
import BaseRequest from '../utils/request-helper.js'
import { LOADING_TEXTS } from '../config/constants.js'
/**
* 获取评价列表
* @param {Object} params 查询参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getReviews = (params = {}, config = {}) => {
return BaseRequest.get('/reviews', params, 'SILENT_REQUEST', config)
}
/**
* 获取特定对象的评价列表
* @param {String} targetType 目标类型 (pet, organization, service)
* @param {String|Number} targetId 目标ID
* @param {Object} params 查询参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getTargetReviews = (targetType, targetId, params = {}, config = {}) => {
return BaseRequest.get(`/reviews/${targetType}/${targetId}`, params, 'SILENT_REQUEST', config)
}
/**
* 添加评价
* @param {Object} reviewData 评价数据
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const addReview = (reviewData, config = {}) => {
return BaseRequest.post('/reviews', reviewData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.SUBMIT_REVIEW, config)
}
/**
* 更新评价
* @param {String|Number} reviewId 评价ID
* @param {Object} reviewData 评价数据
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const updateReview = (reviewId, reviewData, config = {}) => {
return BaseRequest.put(`/reviews/${reviewId}`, reviewData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.UPDATE_REVIEW, config)
}
/**
* 删除评价
* @param {String|Number} reviewId 评价ID
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const deleteReview = (reviewId, config = {}) => {
return BaseRequest.delete(`/reviews/${reviewId}`, {}, 'AUTHENTICATED_DELETE', LOADING_TEXTS.DELETE_REVIEW, config)
}
/**
* 获取我的评价列表
* @param {Object} params 查询参数
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getMyReviews = (params = {}, config = {}) => {
return BaseRequest.get('/reviews/my', params, 'AUTHENTICATED_QUERY_WITH_LOADING', config)
}
/**
* 点赞评价
* @param {String|Number} reviewId 评价ID
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const likeReview = (reviewId, config = {}) => {
return BaseRequest.post(`/reviews/${reviewId}/like`, {}, 'AUTHENTICATED_QUERY', null, config)
}
/**
* 取消点赞评价
* @param {String|Number} reviewId 评价ID
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const unlikeReview = (reviewId, config = {}) => {
return BaseRequest.delete(`/reviews/${reviewId}/like`, {}, 'AUTHENTICATED_QUERY', null, config)
}
/**
* 举报评价
* @param {String|Number} reviewId 评价ID
* @param {Object} reportData 举报数据
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const reportReview = (reviewId, reportData, config = {}) => {
return BaseRequest.post(`/reviews/${reviewId}/report`, reportData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.SUBMIT_REPORT, config)
}
/**
* 回复评价
* @param {String|Number} reviewId 评价ID
* @param {Object} replyData 回复数据
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const replyReview = (reviewId, replyData, config = {}) => {
return BaseRequest.post(`/reviews/${reviewId}/reply`, replyData, 'AUTHENTICATED_UPDATE', LOADING_TEXTS.REPLY_REVIEW, config)
}
/**
* 获取评价统计
* @param {String} targetType 目标类型
* @param {String|Number} targetId 目标ID
* @param {Object} config 自定义配置
* @returns {Promise}
*/
export const getReviewStats = (targetType, targetId, config = {}) => {
return BaseRequest.get(`/reviews/${targetType}/${targetId}/stats`, {}, 'PUBLIC_REQUEST', config)
}
+151
View File
@@ -0,0 +1,151 @@
// HTTP模块配置文件
/**
* 环境配置
*/
const ENV_CONFIG = {
// 开发环境
development: {
baseURL: 'http://localhost:8888',
timeout: 30000
},
// 测试环境
testing: {
baseURL: 'https://test-api.pet-ai.com',
timeout: 30000
},
// 生产环境
production: {
baseURL: 'https://api.pet-ai.com',
timeout: 60000
}
}
/**
* 当前环境 (可根据实际情况动态设置)
*/
let CURRENT_ENV = 'development' // development | testing | production
/**
* HTTP基础配置
*/
export const HTTP_CONFIG = {
// 当前环境配置
...ENV_CONFIG[CURRENT_ENV],
// 登录页面路径
loginPage: '/pages/login/login',
// 存储键名配置
storageKeys: {
token: 'token',
refreshToken: 'refreshToken',
userInfo: 'userInfo'
},
// 请求重试配置
retry: {
times: 2, // 重试次数
delay: 1000, // 重试延迟(毫秒)
statusCodes: [500, 502, 503, 504] // 需要重试的状态码
}
}
/**
* 不需要鉴权的接口列表
* 注意:只需要配置不需要token的接口,其他接口默认都需要鉴权
*/
export const NO_AUTH_APIS = [
// 用户认证相关
'/wechat/user/mini/login', // 微信小程序登录
]
/**
* 需要鉴权的接口列表(明确需要登录的接口)
* 这些接口在调用时如果未登录会自动跳转到登录页面
*/
export const AUTH_REQUIRED_APIS = [
// 微信小程序用户相关接口
'/user/wechat/mini/userinfo', // 获取用户信息
'/user/wechat/mini/phone-update', // 更新手机号
'/user/wechat/mini/check-unionid', // 检查UnionID
]
/**
* 检查接口是否需要鉴权
* @param {String} url 接口URL
* @returns {Boolean} 是否需要鉴权
*/
export function checkApiAuth(url) {
// 移除查询参数,只保留路径
const path = url.split('?')[0]
// 检查是否在不需要鉴权的列表中
for (const noAuthApi of NO_AUTH_APIS) {
// 支持通配符匹配
if (noAuthApi.includes('*')) {
const regex = new RegExp('^' + noAuthApi.replace(/\*/g, '.*') + '$')
if (regex.test(path)) {
return false
}
} else if (path === noAuthApi || path.startsWith(noAuthApi + '/')) {
return false
}
}
// 默认需要鉴权
return true
}
/**
* 检查接口是否需要强制登录
* @param {String} url 接口URL
* @returns {Boolean} 是否需要强制登录
*/
export function checkAuthRequiredApi(url) {
// 移除查询参数,只保留路径
const path = url.split('?')[0]
// 检查是否在需要强制登录的列表中
for (const authApi of AUTH_REQUIRED_APIS) {
// 支持通配符匹配
if (authApi.includes('*')) {
const regex = new RegExp('^' + authApi.replace(/\*/g, '.*') + '$')
if (regex.test(path)) {
return true
}
} else if (path === authApi || path.startsWith(authApi + '/')) {
return true
}
}
return false
}
/**
* 添加不需要鉴权的接口
* @param {String|Array} apis 接口路径或接口路径数组
*/
export function addNoAuthApis(apis) {
if (Array.isArray(apis)) {
NO_AUTH_APIS.push(...apis)
} else {
NO_AUTH_APIS.push(apis)
}
}
/**
* 设置当前环境
* @param {String} env 环境名称 (development | testing | production)
*/
export function setEnvironment(env) {
if (ENV_CONFIG[env]) {
CURRENT_ENV = env
// 更新HTTP配置
Object.assign(HTTP_CONFIG, ENV_CONFIG[env])
console.log(`HTTP环境已切换到: ${env}`)
console.log(`当前baseURL: ${HTTP_CONFIG.baseURL}`)
} else {
console.error(`无效的环境名称: ${env}`)
}
}
+178
View File
@@ -0,0 +1,178 @@
/**
* HTTP模块统一配置常量文件
* 统一管理所有配置模板、Loading文本常量和其他共享常量
*/
// ==================== 配置模板 ====================
/**
* 统一的请求配置模板
* 合并了REQUEST_CONFIG_TEMPLATES、COMMON_CONFIG_TEMPLATES、DEFAULT_CONFIG_TEMPLATES
*/
export const CONFIG_TEMPLATES = {
// 需要认证的查询请求(无loading)
AUTHENTICATED_QUERY: {
auth: true,
loading: false,
toast: true
},
// 需要认证的查询请求(有loading)
AUTHENTICATED_QUERY_WITH_LOADING: {
auth: true,
loading: true,
toast: true
},
// 需要认证的更新请求
AUTHENTICATED_UPDATE: {
auth: true,
loading: true,
toast: true
},
// 需要认证的删除请求
AUTHENTICATED_DELETE: {
auth: true,
loading: true,
toast: true
},
// 无需认证的公开请求
PUBLIC_REQUEST: {
auth: false,
loading: true,
toast: true
},
// 静默请求(无loading和toast)
SILENT_REQUEST: {
auth: false,
loading: false,
toast: false
},
// 需要认证的上传请求
AUTHENTICATED_UPLOAD: {
auth: true,
loading: true,
toast: true
},
// 需要认证的提交请求
AUTHENTICATED_SUBMIT: {
auth: true,
loading: true,
toast: true
},
// 公开的下载请求
PUBLIC_DOWNLOAD: {
auth: false,
loading: true,
toast: true
},
// 静默的配置获取请求
SILENT_CONFIG: {
auth: false,
loading: false,
toast: false
}
}
// ==================== Loading文本常量 ====================
/**
* 统一的Loading文本配置
* 合并了所有模块中的LOADING_TEXTS常量
*/
export const LOADING_TEXTS = {
// 通用操作
LOADING: '正在加载...',
SAVING: '正在保存...',
DELETING: '正在删除...',
UPDATING: '正在更新...',
SUBMITTING: '正在提交...',
UPLOADING: '正在上传...',
// 认证相关
LOGIN: '正在登录...',
LOGOUT: '正在退出...',
REGISTER: '正在注册...',
PHONE_VERIFY: '正在验证手机号...',
SEND_SMS: '正在发送验证码...',
VERIFY_SMS: '正在验证...',
RESET_PASSWORD: '正在重置密码...',
// 用户相关
UPDATING_USER_INFO: '正在更新用户信息...',
UPLOADING_AVATAR: '正在上传头像...',
SAVING_PROFILE: '正在保存...',
DELETING_ACCOUNT: '正在注销账户...',
LOADING_DATA: '正在加载...',
// 宠物相关
ADD_PET: '正在添加宠物...',
UPDATE_PET: '正在更新宠物信息...',
DELETE_PET: '正在删除宠物...',
ADD_RECORD: '正在添加记录...',
UPDATE_RECORD: '正在更新记录...',
DELETE_RECORD: '正在删除记录...',
// 文件相关
UPLOAD_IMAGE: '正在上传图片...',
UPLOAD_FILE: '正在上传文件...',
UPLOAD_IMAGES: '正在批量上传...',
DOWNLOAD_FILE: '正在下载文件...',
// AI助手相关
AI_THINKING: 'AI正在思考中...',
SPEECH_RECOGNITION: '正在识别语音...',
// 领养相关
SUBMIT_APPLICATION: '正在提交申请...',
CANCEL_APPLICATION: '正在取消申请...',
// 评价相关
SUBMIT_REVIEW: '正在提交评价...',
UPDATE_REVIEW: '正在更新评价...',
DELETE_REVIEW: '正在删除评价...',
SUBMIT_REPORT: '正在提交举报...',
REPLY_REVIEW: '正在回复评价...',
// 其他
SUBMIT_FEEDBACK: '正在提交反馈...',
CHECK_UPDATE: '正在检查更新...'
}
// ==================== 工具函数 ====================
/**
* 创建标准化的请求配置
* @param {string} template 配置模板名称
* @param {Object} customConfig 自定义配置
* @param {string} loadingText 自定义loading文本
* @returns {Object} 标准化的请求配置
*/
export const createRequestConfig = (template, customConfig = {}, loadingText = null) => {
const baseConfig = CONFIG_TEMPLATES[template] || {}
const config = {
custom: {
...baseConfig,
...(loadingText && { loadingText }),
...customConfig.custom
},
...customConfig
}
// 移除custom属性中的undefined值
Object.keys(config.custom).forEach(key => {
if (config.custom[key] === undefined) {
delete config.custom[key]
}
})
return config
}
+173
View File
@@ -0,0 +1,173 @@
// HTTP请求配置和拦截器
// 基于uView-next的luch-request库实现
import { HTTP_CONFIG, checkApiAuth, checkAuthRequiredApi } from './config.js'
import { clearAuthData, isUserLoggedIn, handleAuthRequired, tryAutoLogin } from '../utils/auth-helper.js'
/**
* 初始化HTTP配置
* @param {Object} vm Vue实例,用于访问vuex等全局状态
*/
export default (vm) => {
// 初始化请求配置
uni.$u.http.setConfig((config) => {
config.baseURL = HTTP_CONFIG.baseURL
config.timeout = HTTP_CONFIG.timeout
config.header = {
'Content-Type': 'application/json;charset=UTF-8'
}
return config
})
// 请求拦截器
uni.$u.http.interceptors.request.use((config) => {
// 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{}
config.data = config.data || {}
config.custom = config.custom || {}
// 自动检查接口是否需要鉴权(如果没有明确指定)
if (config.custom.auth === undefined) {
config.custom.auth = checkApiAuth(config.url)
}
// 检查是否为需要强制登录的接口
const isAuthRequiredApi = checkAuthRequiredApi(config.url)
// 如果是需要强制登录的接口,检查登录状态
if (isAuthRequiredApi) {
if (!isUserLoggedIn()) {
console.log('访问需要鉴权的接口但未完成登录,跳转到登录流程:', config.url)
handleAuthRequired()
return Promise.reject({
message: '需要登录',
code: 'AUTH_REQUIRED',
url: config.url
})
}
}
// 添加Authorization请求头
if (config.custom.auth !== false) {
const token = uni.getStorageSync(HTTP_CONFIG.storageKeys.token)
if (token) {
config.header.Authorization = `Bearer ${token}`
}
}
// 根据custom参数配置是否显示loading
if (config.custom.loading !== false) {
uni.showLoading({
title: config.custom.loadingText || '请稍候...',
mask: true
})
}
return config
}, config => {
// 请求错误处理
// 如果是AUTH_REQUIRED错误,不显示loading
if (config.code === 'AUTH_REQUIRED') {
// 不显示loading,因为会跳转页面
return Promise.reject(config)
}
return Promise.reject(config)
})
// 响应拦截器
uni.$u.http.interceptors.response.use((response) => {
/* 对响应成功做点什么 可使用async await 做异步操作*/
// 隐藏loading
uni.hideLoading()
const data = response.data
const custom = response.config?.custom
// 统一的响应数据处理
if (data.code !== undefined) {
// 如果服务端返回的状态码不等于0(成功),则reject()
if (data.code !== 0) {
// 如果没有显式定义custom的toast参数为false的话,默认对报错进行toast弹出提示
if (custom?.toast !== false) {
uni.$u.toast(data.message || '请求失败')
}
// 特殊状态码处理
if (data.code === 401) {
// token过期,清除本地认证信息并跳转到登录流程
clearAuthData()
handleAuthRequired()
return Promise.reject(data)
}
// 如果需要catch返回,则进行reject
if (custom?.catch) {
return Promise.reject(data)
} else {
// 否则返回一个pending中的promise,请求不会进入catch中
return new Promise(() => {})
}
}
}
// 返回处理后的数据
return data.data !== undefined ? data.data : data
}, (response) => {
/* 对响应错误做点什么 (statusCode !== 200)*/
// 隐藏loading
uni.hideLoading()
const custom = response.config?.custom
// 网络错误处理
let errorMessage = '网络错误,请检查网络连接'
if (response.statusCode) {
switch (response.statusCode) {
case 400:
errorMessage = '请求参数错误'
break
case 401:
errorMessage = '未授权,请重新登录'
clearAuthData()
handleAuthRequired()
break
case 403:
errorMessage = '拒绝访问'
break
case 404:
errorMessage = '请求地址不存在'
break
case 500:
errorMessage = '服务器内部错误'
break
case 502:
errorMessage = '网关错误'
break
case 503:
errorMessage = '服务不可用'
break
case 504:
errorMessage = '网关超时'
break
default:
errorMessage = `连接错误${response.statusCode}`
}
}
// 显示错误提示
if (custom?.toast !== false) {
uni.$u.toast(errorMessage)
}
console.error('请求错误:', response)
return Promise.reject(response)
})
}
+80
View File
@@ -0,0 +1,80 @@
/**
* HTTP模块统一导出文件
* 提供完整的API接口、配置管理、工具函数和认证功能
*/
// 导入所有API模块
import * as petsApi from './api/pets.js'
import * as assistantApi from './api/assistant.js'
import * as adoptionApi from './api/adoption.js'
import * as profileApi from './api/profile.js'
import * as reviewApi from './api/review.js'
import * as commonApi from './api/common.js'
import * as authApi from './api/auth.js'
// ==================== 配置导出 ====================
export { HTTP_CONFIG, NO_AUTH_APIS, addNoAuthApis, setEnvironment } from './config/config.js'
// ==================== 工具函数导出 ====================
export { validateRequired, validateId, validatePagination, validateFileUpload } from './utils/validator.js'
// ==================== 认证工具导出 ====================
export {
isUserLoggedIn,
validateToken,
checkLoginStatus,
clearAuthData,
clearTempLoginData,
navigateToLogin,
handleAuthRequired,
tryAutoLogin
} from './utils/auth-helper.js'
// ==================== API模块导出 ====================
export {
petsApi,
assistantApi,
adoptionApi,
profileApi,
reviewApi,
commonApi,
authApi
}
// ==================== 默认导出 ====================
export default {
// API模块
pets: petsApi,
assistant: assistantApi,
adoption: adoptionApi,
profile: profileApi,
review: reviewApi,
common: commonApi,
auth: authApi,
// 配置
config: {
HTTP_CONFIG,
NO_AUTH_APIS,
addNoAuthApis,
setEnvironment
},
// 工具函数
utils: {
validateRequired,
validateId,
validatePagination,
validateFileUpload,
isUserLoggedIn,
validateToken,
checkLoginStatus,
clearAuthData,
clearTempLoginData,
navigateToLogin,
handleAuthRequired,
tryAutoLogin
}
}
+149
View File
@@ -0,0 +1,149 @@
/**
* 统一认证工具模块
* 整合所有认证检查、登录状态管理等功能
*/
import { HTTP_CONFIG } from '../config/config.js'
// ==================== 认证状态检查 ====================
/**
* 检查用户是否已完成登录
* @returns {Boolean} 是否已完成登录
*/
export function isUserLoggedIn() {
const token = uni.getStorageSync(HTTP_CONFIG.storageKeys.token)
const loginStep = uni.getStorageSync('loginStep')
// 检查是否有有效的token和完成的登录流程
return !!(token && (loginStep === 'profile_completed' || loginStep === 'phone_authed'))
}
/**
* 检查并验证token有效性
* @returns {Promise<boolean>} token是否有效
*/
export async function validateToken() {
const token = uni.getStorageSync(HTTP_CONFIG.storageKeys.token)
if (!token) {
return false
}
try {
// 调用一个简单的API来验证token
const { getUserInfo } = await import('../api/profile.js')
await getUserInfo({
custom: {
loading: false,
toast: false
}
})
return true
} catch (error) {
console.log('Token验证失败:', error)
return false
}
}
/**
* 检查登录状态并处理
* @returns {boolean} 是否已登录
*/
export function checkLoginStatus() {
if (!isUserLoggedIn()) {
console.log('用户未登录,跳转到登录页')
navigateToLogin()
return false
}
return true
}
// ==================== 认证数据管理 ====================
/**
* 清理认证数据
*/
export function clearAuthData() {
console.log('清理认证数据被调用')
uni.removeStorageSync(HTTP_CONFIG.storageKeys.token)
uni.removeStorageSync(HTTP_CONFIG.storageKeys.refreshToken)
uni.removeStorageSync(HTTP_CONFIG.storageKeys.userInfo)
uni.removeStorageSync('loginStep')
uni.removeStorageSync('wxLoginCode')
uni.removeStorageSync('wxUserInfo')
uni.removeStorageSync('loginDate')
}
/**
* 清理登录流程中的临时数据
*/
export function clearTempLoginData() {
// 清理临时数据,保留重要的用户信息
uni.removeStorageSync('wxLoginCode')
console.log('清理临时登录数据')
}
// ==================== 登录流程处理 ====================
/**
* 跳转到登录页面
*/
export function navigateToLogin() {
uni.reLaunch({
url: HTTP_CONFIG.loginPage
})
}
/**
* 处理需要鉴权的情况
*/
export function handleAuthRequired() {
const loginStep = uni.getStorageSync('loginStep')
// 根据当前登录状态决定跳转页面
let targetPage = '/pages/login/login'
let toastMessage = '请先登录'
switch (loginStep) {
case 'wx_logged':
targetPage = '/pages/auth/phone-auth'
toastMessage = '请完成手机号授权'
break
case 'phone_authed':
case 'phone_skipped':
targetPage = '/pages/profile/user-info?mode=setup'
toastMessage = '请完善个人信息'
break
default:
targetPage = '/pages/login/login'
toastMessage = '请先登录'
}
// 显示提示并跳转
uni.showToast({
title: toastMessage,
icon: 'none',
duration: 1500
})
setTimeout(() => {
uni.navigateTo({ url: targetPage }).catch(() => {
uni.reLaunch({ url: targetPage })
})
}, 1500)
}
/**
* 尝试自动登录
* @returns {Promise<Boolean>} 是否登录成功
*/
export async function tryAutoLogin() {
try {
// 这里需要导入实际的自动登录函数
const { autoLogin } = await import('../../utils/loginState.js')
return await autoLogin()
} catch (error) {
console.error('自动登录失败:', error)
return false
}
}
+172
View File
@@ -0,0 +1,172 @@
/**
* HTTP请求统一工具类
* 基于BaseRequest类提供标准化的请求配置和执行方法
*/
import { CONFIG_TEMPLATES, LOADING_TEXTS, createRequestConfig } from '../config/constants.js'
// ==================== BaseRequest工具类 ====================
/**
* 统一的HTTP请求工具类
* 提供标准化的请求方法,基于uni.$u.http实现
*/
class BaseRequest {
/**
* 执行GET请求
* @param {string} url 请求URL
* @param {Object} params 查询参数
* @param {string} template 配置模板
* @param {Object} config 自定义配置
* @returns {Promise} 请求Promise
*/
static get(url, params = {}, template = 'AUTHENTICATED_QUERY', config = {}) {
const requestConfig = createRequestConfig(template, config)
return uni.$u.http.get(url, {
...(Object.keys(params).length > 0 && { params }),
...requestConfig
})
}
/**
* 执行POST请求
* @param {string} url 请求URL
* @param {Object} data 请求数据
* @param {string} template 配置模板
* @param {string} loadingText loading文本
* @param {Object} config 自定义配置
* @returns {Promise} 请求Promise
*/
static post(url, data = {}, template = 'AUTHENTICATED_UPDATE', loadingText = null, config = {}) {
const requestConfig = createRequestConfig(template, config, loadingText)
return uni.$u.http.post(url, data, requestConfig)
}
/**
* 执行PUT请求
* @param {string} url 请求URL
* @param {Object} data 请求数据
* @param {string} template 配置模板
* @param {string} loadingText loading文本
* @param {Object} config 自定义配置
* @returns {Promise} 请求Promise
*/
static put(url, data = {}, template = 'AUTHENTICATED_UPDATE', loadingText = null, config = {}) {
const requestConfig = createRequestConfig(template, config, loadingText)
return uni.$u.http.put(url, data, requestConfig)
}
/**
* 执行DELETE请求
* @param {string} url 请求URL
* @param {Object} data 请求数据
* @param {string} template 配置模板
* @param {string} loadingText loading文本
* @param {Object} config 自定义配置
* @returns {Promise} 请求Promise
*/
static delete(url, data = {}, template = 'AUTHENTICATED_DELETE', loadingText = null, config = {}) {
const requestConfig = createRequestConfig(template, config, loadingText)
return uni.$u.http.delete(url, data, requestConfig)
}
/**
* 执行上传请求
* @param {string} url 上传URL
* @param {Object} uploadData 上传数据
* @param {string} loadingText loading文本
* @param {Object} config 自定义配置
* @returns {Promise} 请求Promise
*/
static upload(url, uploadData, loadingText, config = {}) {
const requestConfig = createRequestConfig('AUTHENTICATED_UPLOAD', config, loadingText)
return uni.$u.http.upload(url, {
filePath: uploadData.filePath,
name: uploadData.name || 'file',
formData: uploadData.formData || {},
...requestConfig
})
}
/**
* 执行下载请求
* @param {string} url 下载URL
* @param {string} loadingText loading文本
* @param {Object} config 自定义配置
* @returns {Promise} 请求Promise
*/
static download(url, loadingText, config = {}) {
const requestConfig = createRequestConfig('PUBLIC_DOWNLOAD', config, loadingText)
return uni.$u.http.download(url, {
...(config.savePath && { savePath: config.savePath }),
...requestConfig
})
}
/**
* 批量上传多个文件
* @param {Array} fileList 文件列表数组
* @param {string} url 上传URL
* @param {Object} config 自定义配置
* @returns {Promise} 请求Promise
*/
static async batchUpload(fileList, url, config = {}) {
const { maxConcurrent = 3, showProgress = true } = config
// 显示整体进度loading
if (showProgress) {
uni.showLoading({
title: config.custom?.loadingText || LOADING_TEXTS.UPLOAD_IMAGES
})
}
try {
// 分批并发上传
const results = []
for (let i = 0; i < fileList.length; i += maxConcurrent) {
const batch = fileList.slice(i, i + maxConcurrent)
const batchPromises = batch.map(fileData => {
return BaseRequest.upload(url, fileData, null, {
...config,
custom: {
loading: false, // 批量上传时不显示单个loading
...config.custom
}
})
})
const batchResults = await Promise.all(batchPromises)
results.push(...batchResults)
}
return results
} finally {
if (showProgress) {
uni.hideLoading()
}
}
}
}
// ==================== 导出 ====================
// 默认导出BaseRequest类
export default BaseRequest
// 向后兼容的命名导出
export const executeGetRequest = BaseRequest.get
export const executePostRequest = BaseRequest.post
export const executePutRequest = BaseRequest.put
export const executeDeleteRequest = BaseRequest.delete
export const executeUploadRequest = BaseRequest.upload
export const executeDownloadRequest = BaseRequest.download
export const executeBatchUploadRequest = BaseRequest.batchUpload
// 导出配置相关(从constants.js重新导出)
export { CONFIG_TEMPLATES, LOADING_TEXTS, createRequestConfig }
+80
View File
@@ -0,0 +1,80 @@
// HTTP请求参数验证工具
/**
* 验证必需参数
* @param {Object} params 参数对象
* @param {Array} requiredFields 必需字段数组
* @param {String} apiName API名称(用于错误提示)
* @throws {Error} 参数验证失败时抛出错误
*/
export function validateRequired(params, requiredFields, apiName = 'API') {
if (!params || typeof params !== 'object') {
throw new Error(`${apiName}: 参数不能为空`)
}
for (const field of requiredFields) {
if (params[field] === undefined || params[field] === null || params[field] === '') {
throw new Error(`${apiName}: 缺少必需参数 ${field}`)
}
}
}
/**
* 验证ID参数
* @param {String|Number} id ID值
* @param {String} fieldName 字段名称
* @param {String} apiName API名称
* @throws {Error} ID验证失败时抛出错误
*/
export function validateId(id, fieldName = 'id', apiName = 'API') {
if (!id || (typeof id !== 'string' && typeof id !== 'number')) {
throw new Error(`${apiName}: ${fieldName} 不能为空且必须是字符串或数字`)
}
if (typeof id === 'string' && id.trim() === '') {
throw new Error(`${apiName}: ${fieldName} 不能为空字符串`)
}
if (typeof id === 'number' && (id <= 0 || !Number.isInteger(id))) {
throw new Error(`${apiName}: ${fieldName} 必须是正整数`)
}
}
/**
* 验证分页参数
* @param {Object} params 分页参数
* @param {String} apiName API名称
*/
export function validatePagination(params, apiName = 'API') {
if (params.page !== undefined) {
if (!Number.isInteger(params.page) || params.page < 1) {
throw new Error(`${apiName}: page 必须是大于0的整数`)
}
}
if (params.limit !== undefined) {
if (!Number.isInteger(params.limit) || params.limit < 1 || params.limit > 100) {
throw new Error(`${apiName}: limit 必须是1-100之间的整数`)
}
}
}
/**
* 验证文件上传参数
* @param {Object} fileData 文件数据
* @param {String} apiName API名称
*/
export function validateFileUpload(fileData, apiName = 'API') {
if (!fileData || typeof fileData !== 'object') {
throw new Error(`${apiName}: 文件数据不能为空`)
}
if (!fileData.filePath || typeof fileData.filePath !== 'string') {
throw new Error(`${apiName}: filePath 不能为空且必须是字符串`)
}
// 验证文件路径格式(简单验证)
if (!fileData.filePath.includes('/') && !fileData.filePath.includes('\\')) {
throw new Error(`${apiName}: filePath 格式不正确`)
}
}
+9
View File
@@ -1,5 +1,6 @@
import App from './App' import App from './App'
import uViewNext from '@/uni_modules/uview-next'; import uViewNext from '@/uni_modules/uview-next';
import httpConfig from './http/config/request.js';
// #ifndef VUE3 // #ifndef VUE3
import Vue from 'vue' import Vue from 'vue'
@@ -10,6 +11,10 @@ App.mpType = 'app'
const app = new Vue({ const app = new Vue({
...App ...App
}) })
// 引入HTTP请求配置,将app参数传递到配置中
httpConfig(app)
app.$mount() app.$mount()
// #endif // #endif
@@ -18,6 +23,10 @@ import { createSSRApp } from 'vue'
export function createApp() { export function createApp() {
const app = createSSRApp(App) const app = createSSRApp(App)
app.use(uViewNext) app.use(uViewNext)
// 引入HTTP请求配置
httpConfig(app)
return { return {
app app
} }
+23 -7
View File
@@ -24,6 +24,14 @@
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
} }
}, },
{
"path": "pages/assistant/knowledge-detail",
"style": {
"navigationBarTitleText": "知识详情",
"navigationBarBackgroundColor": "#FF8A80",
"navigationBarTextStyle": "white"
}
},
{ {
"path": "pages/adoption/adoption", "path": "pages/adoption/adoption",
"style": { "style": {
@@ -251,6 +259,14 @@
"navigationBarBackgroundColor": "#FF8A80", "navigationBarBackgroundColor": "#FF8A80",
"navigationBarTextStyle": "white" "navigationBarTextStyle": "white"
} }
},
{
"path": "pages/auth/phone-auth",
"style": {
"navigationBarTitleText": "手机号授权",
"navigationBarBackgroundColor": "#FF8A80",
"navigationBarTextStyle": "white"
}
} }
], ],
"globalStyle": { "globalStyle": {
@@ -262,7 +278,7 @@
"easycom": { "easycom": {
"autoscan": true, "autoscan": true,
"custom": { "custom": {
"^u-(.*)": "uview-ui-next/components/u-$1/u-$1.vue" "^u-(.*)": "@/uni_modules/uview-next/components/u-$1/u-$1.vue"
} }
}, },
"tabBar": { "tabBar": {
@@ -283,12 +299,12 @@
"iconPath": "static/tabbar/assistant.png", "iconPath": "static/tabbar/assistant.png",
"selectedIconPath": "static/tabbar/assistant-active.png" "selectedIconPath": "static/tabbar/assistant-active.png"
}, },
{ // {
"pagePath": "pages/review/review", // "pagePath": "pages/review/review",
"text": "评测", // "text": "评测",
"iconPath": "static/tabbar/review.png", // "iconPath": "static/tabbar/review.png",
"selectedIconPath": "static/tabbar/review-active.png" // "selectedIconPath": "static/tabbar/review-active.png"
}, // },
{ {
"pagePath": "pages/adoption/adoption", "pagePath": "pages/adoption/adoption",
"text": "领养", "text": "领养",
+8 -14
View File
@@ -1,5 +1,5 @@
<template> <template>
<view class="adoption-container page-container-with-bg"> <view class="adoption-container page-container-unified">
<!-- 头部搜索栏 --> <!-- 头部搜索栏 -->
<view class="header-section"> <view class="header-section">
<view class="search-wrapper"> <view class="search-wrapper">
@@ -864,34 +864,28 @@ const adoptionManager = new AdoptionManager()
export default { export default {
data() { data() {
return { return {
// 搜索 // 搜索相关
searchKeyword: '', searchKeyword: '',
// 筛选面板 // 筛选相关
showFilter: false, showFilter: false,
// 地区筛选
selectedProvince: null, selectedProvince: null,
selectedCity: null, selectedCity: null,
selectedDistrict: null, selectedDistrict: null,
selectedProvinceIndex: 0, selectedProvinceIndex: 0,
selectedCityIndex: 0, selectedCityIndex: 0,
selectedDistrictIndex: 0, selectedDistrictIndex: 0,
// 宠物类型筛选
selectedType: 'all', selectedType: 'all',
selectedBreed: 'all', selectedBreed: 'all',
// 其他筛选
selectedStatus: 'all', selectedStatus: 'all',
selectedGender: 'all', selectedGender: 'all',
// 数据 // 数据相关
allPets: [], allPets: [],
displayPets: [], displayPets: [],
totalPets: 0, totalPets: 0,
// 分页 // 分页相关
page: 1, page: 1,
pageSize: 20, pageSize: 20,
hasMorePets: true, hasMorePets: true,
@@ -1192,7 +1186,7 @@ export default {
/* 头部搜索栏 */ /* 头部搜索栏 */
.header-section { .header-section {
background: transparent; background: transparent;
padding: 24rpx 20rpx; padding: 0 0 24rpx 0;
.search-wrapper { .search-wrapper {
display: flex; display: flex;
@@ -1287,7 +1281,7 @@ export default {
.filter-panel { .filter-panel {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
padding: 24rpx 20rpx; padding: 24rpx 0;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3); border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
position: relative; position: relative;
z-index: 10; z-index: 10;
@@ -1514,7 +1508,7 @@ export default {
/* 宠物列表区域 */ /* 宠物列表区域 */
.pets-scroll { .pets-scroll {
height: calc(100vh - 100rpx); height: calc(100vh - 100rpx);
padding: 0 20rpx; padding: 0;
overflow-x: hidden; overflow-x: hidden;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
+571 -135
View File
@@ -1,5 +1,5 @@
<template> <template>
<view class="assistant-container page-container-with-bg"> <view class="assistant-container page-container-unified">
<!-- 宠物助手信息卡片 --> <!-- 宠物助手信息卡片 -->
<view class="assistant-info-card"> <view class="assistant-info-card">
<view class="assistant-avatar-wrapper"> <view class="assistant-avatar-wrapper">
@@ -15,8 +15,13 @@
<view class="status-dot"></view> <view class="status-dot"></view>
<text class="status-text">在线</text> <text class="status-text">在线</text>
</view> </view>
<view class="knowledge-icon" @click="openKnowledge"> <view class="action-buttons">
<text class="icon-text">📚</text> <view class="action-btn" @click="clearHistory" title="清空历史">
<text class="icon-text">🗑️</text>
</view>
<view class="action-btn" @click="openKnowledge" title="知识库">
<text class="icon-text">📚</text>
</view>
</view> </view>
</view> </view>
@@ -39,8 +44,10 @@
</view> </view>
</view> </view>
<view class="message-content ai"> <view class="message-content ai">
<view class="message-bubble ai"> <view class="message-bubble ai" @longpress="copyMessage(message.content)">
<text class="message-text" v-if="!(isThinking && index === messageList.length - 1)">{{ message.content }}</text> <view class="message-text" v-if="!(isThinking && index === messageList.length - 1)">
<u-markdown :content="message.content" :showLine="false"></u-markdown>
</view>
<view class="typing-dots" v-else> <view class="typing-dots" v-else>
<view class="typing-dot"></view> <view class="typing-dot"></view>
<view class="typing-dot"></view> <view class="typing-dot"></view>
@@ -54,8 +61,10 @@
<!-- 用户消息 --> <!-- 用户消息 -->
<view class="message-item user" v-if="message.type === 'user'"> <view class="message-item user" v-if="message.type === 'user'">
<view class="message-content user"> <view class="message-content user">
<view class="message-bubble user"> <view class="message-bubble user" @longpress="copyMessage(message.content)">
<text class="message-text">{{ message.content }}</text> <view class="message-text">
<u-markdown :content="message.content" :showLine="false"></u-markdown>
</view>
</view> </view>
<text class="message-time">{{ message.time }}</text> <text class="message-time">{{ message.time }}</text>
</view> </view>
@@ -110,6 +119,17 @@
</template> </template>
<script> <script>
import {
askPetAssistant,
streamAskPetAssistant,
getAssistantHistory,
clearAssistantHistory,
validateMessage,
formatChatMessage,
formatMessageTime
} from '@/http/api/assistant.js'
// WebSocket功能已移除,使用HTTP API替代
export default { export default {
data() { data() {
return { return {
@@ -117,126 +137,325 @@ export default {
scrollTop: 0, scrollTop: 0,
isThinking: false, isThinking: false,
userAvatar: '/static/user-avatar.png', userAvatar: '/static/user-avatar.png',
messageList: [ messageList: []
{
type: 'ai',
content: '您好!我是您的宠物AI助手🐾\n\n我可以为您解答关于宠物饲养、健康、训练、营养等方面的问题。有什么可以帮助您的吗?',
time: this.getCurrentTime()
}
],
quickQuestions: [
{ id: 1, text: '猫咪不吃饭怎么办?' },
{ id: 2, text: '狗狗需要多久洗一次澡?' },
{ id: 3, text: '宠物疫苗接种时间' },
{ id: 4, text: '如何训练宠物定点上厕所?' },
{ id: 5, text: '宠物发烧的症状' },
{ id: 6, text: '幼猫喂养注意事项' }
],
// 知识库数据
knowledgeBase: {
'喂食': {
keywords: ['喂食', '吃饭', '食物', '饮食', '营养'],
responses: [
'定时定量喂食很重要,成年猫每天2-3次,幼猫3-4次。',
'选择优质的宠物食品,避免给宠物吃人类食物。',
'确保宠物随时有清洁的饮用水。'
]
},
'健康': {
keywords: ['健康', '生病', '症状', '发烧', '呕吐', '腹泻'],
responses: [
'定期体检很重要,建议每年至少一次。',
'注意观察宠物的食欲、精神状态和排便情况。',
'如果出现异常症状,请及时就医。'
]
},
'疫苗': {
keywords: ['疫苗', '接种', '免疫', '预防'],
responses: [
'幼猫幼犬8-12周开始接种疫苗。',
'成年宠物每年需要加强免疫。',
'常见疫苗包括三联疫苗和狂犬疫苗。'
]
},
'训练': {
keywords: ['训练', '教育', '行为', '定点', '上厕所'],
responses: [
'训练需要耐心和一致性。',
'使用正向强化,奖励好行为。',
'避免体罚,这会让宠物产生恐惧。'
]
},
'洗澡': {
keywords: ['洗澡', '清洁', '卫生'],
responses: [
'狗狗一般1-2周洗一次澡,猫咪通常不需要经常洗澡。',
'使用宠物专用洗浴用品。',
'洗澡后要及时吹干,避免感冒。'
]
}
}
} }
}, },
onLoad(options) {
// 加载历史记录
this.loadChatHistory()
},
onShow() {
// 页面显示时滚动到底部
this.$nextTick(() => {
this.scrollToBottom()
})
},
methods: { methods: {
getCurrentTime() { getCurrentTime() {
const now = new Date() const now = new Date()
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}` return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
}, },
askQuestion(question) {
this.inputMessage = question
this.sendMessage()
},
sendMessage() {
async sendMessage() {
if (!this.inputMessage.trim() || this.isThinking) return if (!this.inputMessage.trim() || this.isThinking) return
// 验证消息内容
const validation = validateMessage(this.inputMessage)
if (!validation.valid) {
uni.showToast({
title: validation.error,
icon: 'none',
duration: 2000
})
return
}
// 保存用户输入内容
const userInput = this.inputMessage.trim()
// 添加用户消息 // 添加用户消息
const userMessage = { const userMessage = {
type: 'user', type: 'user',
content: this.inputMessage, content: userInput,
time: this.getCurrentTime() time: this.getCurrentTime()
} }
this.messageList.push(userMessage) this.messageList.push(userMessage)
// 清空输入框
this.inputMessage = ''
// 开始AI思考 // 开始AI思考
this.isThinking = true this.isThinking = true
this.scrollToBottom()
// 模拟AI回复 // 用于存储当前AI消息的引用
setTimeout(() => { let currentAiMessage = null
const aiMessage = {
type: 'ai', // 确保DOM更新后再滚动到思考状态
content: this.getAIResponse(this.inputMessage), this.$nextTick(() => {
time: this.getCurrentTime() this.forceScrollToBottom()
// 在思考过程中持续滚动,确保思考动画可见
setTimeout(() => {
this.forceScrollToBottom()
}, 200)
setTimeout(() => {
this.forceScrollToBottom()
}, 500)
})
try {
// 使用HTTP API流式响应
const response = await this.handleStreamResponse({
message: userInput,
temperature: 0.7,
maxTokens: 1000
}, (aiMessage) => {
// 保存AI消息引用,用于fallback时更新
currentAiMessage = aiMessage
})
// 检查是否包含敏感词
if (response && response.data && response.data.isSensitive) {
uni.showToast({
title: '消息包含敏感内容',
icon: 'none',
duration: 2000
})
} }
this.messageList.push(aiMessage)
this.isThinking = false
this.scrollToBottom()
}, 1500 + Math.random() * 1000) // 1.5-2.5秒随机延迟
this.inputMessage = '' // 结束思考状态
this.isThinking = false
} catch (error) {
console.error('SSE流式请求失败,尝试普通请求:', error)
// 降级处理:使用普通API
try {
const fallbackResponse = await askPetAssistant({
message: userInput,
temperature: 0.7,
maxTokens: 1000
})
if (fallbackResponse && fallbackResponse.data) {
// 更新已存在的AI消息,而不是创建新的
if (currentAiMessage) {
currentAiMessage.content = fallbackResponse.data.message || fallbackResponse.data.content || '抱歉,我现在无法回答您的问题。'
currentAiMessage.isSensitive = fallbackResponse.data.isSensitive || false
currentAiMessage.tokenCount = fallbackResponse.data.tokenCount || 0
currentAiMessage.responseTime = fallbackResponse.data.responseTime || 0
} else {
// 如果没有AI消息引用,创建新的(兜底逻辑)
const aiMessage = {
type: 'ai',
content: fallbackResponse.data.message || fallbackResponse.data.content || '抱歉,我现在无法回答您的问题。',
time: this.getCurrentTime(),
isSensitive: fallbackResponse.data.isSensitive || false,
tokenCount: fallbackResponse.data.tokenCount || 0,
responseTime: fallbackResponse.data.responseTime || 0
}
this.messageList.push(aiMessage)
}
// 检查敏感词
if (fallbackResponse.data.isSensitive) {
uni.showToast({
title: '消息包含敏感内容',
icon: 'none',
duration: 2000
})
}
this.forceScrollToBottom()
} else {
this.handleApiError('API响应格式异常', currentAiMessage)
}
} catch (fallbackError) {
console.error('普通请求也失败:', fallbackError)
this.handleApiError(fallbackError.message || '网络请求失败', currentAiMessage)
}
} finally {
// 确保思考状态结束(防止异常情况)
this.isThinking = false
}
}, },
getAIResponse(question) { /**
// 智能匹配知识库 * 处理API错误
for (let category in this.knowledgeBase) { * @param {string} errorMessage 错误信息
const { keywords, responses } = this.knowledgeBase[category] * @param {Object} existingAiMessage 已存在的AI消息对象(可选)
if (keywords.some(keyword => question.includes(keyword))) { */
const randomResponse = responses[Math.floor(Math.random() * responses.length)] handleApiError(errorMessage, existingAiMessage = null) {
return `${randomResponse}\n\n💡 如果您需要更详细的建议,建议咨询专业的宠物医生。` const errorContent = `抱歉,我遇到了一些问题:${errorMessage}\n\n请稍后再试,或者检查网络连接。`
if (existingAiMessage) {
// 更新已存在的AI消息
existingAiMessage.content = errorContent
existingAiMessage.isError = true
} else {
// 创建新的错误消息(兜底逻辑)
const errorMessage_display = {
type: 'ai',
content: errorContent,
time: this.getCurrentTime(),
isError: true
} }
this.messageList.push(errorMessage_display)
} }
// 默认回复 this.forceScrollToBottom()
const defaultResponses = [ },
'这是一个很好的问题!建议您咨询专业的宠物医生获得更准确的建议。',
'每只宠物的情况都不同,建议根据具体情况来处理。您可以查看我们的知识库了解更多信息。',
'感谢您的提问!这个问题比较复杂,建议您带宠物去专业的宠物医院检查。',
'我理解您的担心。建议您记录宠物的具体症状,然后咨询专业医生。'
]
return defaultResponses[Math.floor(Math.random() * defaultResponses.length)] /**
* 加载聊天历史记录
*/
async loadChatHistory() {
console.log('开始加载聊天历史记录...')
try {
const response = await getAssistantHistory({
page: 1,
pageSize: 20
})
console.log('历史记录API响应:', response)
if (response && Array.isArray(response) && response.length > 0) {
// 格式化并设置历史消息
const historyMessages = response.map(msg => formatChatMessage(msg))
console.log('格式化后的历史消息:', historyMessages)
this.messageList = historyMessages
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom()
})
console.log('历史记录加载成功,共', historyMessages.length, '条消息')
} else {
// 没有历史记录,保持空列表
console.log('没有历史记录或数据格式异常:', response)
this.messageList = []
}
} catch (error) {
console.error('加载历史记录失败:', error)
// 显示用户友好的错误提示
uni.showToast({
title: '加载历史记录失败',
icon: 'none',
duration: 2000
})
// 保持空列表
this.messageList = []
}
},
/**
* 清空聊天历史
*/
async clearHistory() {
try {
const result = await uni.showModal({
title: '确认清空',
content: '确定要清空聊天记录吗?此操作不可恢复。',
confirmText: '清空',
cancelText: '取消'
})
if (!result.confirm) return
uni.showLoading({
title: '正在清空...'
})
await clearAssistantHistory()
// 清空消息列表
this.messageList = []
uni.showToast({
title: '历史记录已清空',
icon: 'success',
duration: 1500
})
} catch (error) {
console.error('清空历史记录失败:', error)
uni.showToast({
title: '清空失败',
icon: 'none',
duration: 2000
})
} finally {
uni.hideLoading()
}
},
/**
* 处理HTTP流式响应
* @param {Object} messageData 消息数据
* @param {Function} onAiMessageCreated 创建AI消息后的回调函数
* @returns {Promise} 返回流式响应处理结果
*/
async handleStreamResponse(messageData, onAiMessageCreated) {
// 创建AI消息对象
const aiMessage = {
type: 'ai',
content: '',
time: this.getCurrentTime(),
isSensitive: false,
tokenCount: 0,
responseTime: 0
}
// 添加到消息列表
this.messageList.push(aiMessage)
// 通知调用者AI消息已创建
if (onAiMessageCreated) {
onAiMessageCreated(aiMessage)
}
// 使用HTTP API处理流式响应
return streamAskPetAssistant(messageData)
.then(result => {
console.log('HTTP流式响应完成:', result)
// 更新消息内容
aiMessage.content = result.message || result.content || ''
// 更新消息的元数据
if (result.isSensitive !== undefined) {
aiMessage.isSensitive = result.isSensitive
}
if (result.tokenCount !== undefined) {
aiMessage.tokenCount = result.tokenCount
}
if (result.responseTime !== undefined) {
aiMessage.responseTime = result.responseTime
}
// 强制触发视图更新
this.$forceUpdate()
return result
})
.catch(error => {
console.error('HTTP流式响应失败:', error)
// 显示错误提示
uni.showToast({
title: '消息发送失败',
icon: 'error',
duration: 2000
})
// 更新消息状态为错误
aiMessage.content = '抱歉,消息发送失败,请重试。'
aiMessage.isError = true
// 强制触发视图更新
this.$forceUpdate()
throw error
})
}, },
scrollToBottom() { scrollToBottom() {
@@ -245,18 +464,41 @@ export default {
}) })
}, },
startVoiceInput() { // 强制滚动到底部,用于确保消息可见
uni.showToast({ forceScrollToBottom() {
title: '语音功能开发中', this.$nextTick(() => {
icon: 'none' // 使用时间戳确保每次滚动都能触发
this.scrollTop = Date.now()
setTimeout(() => {
this.scrollTop = 999999
}, 50)
}) })
}, },
copyMessage(content) {
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({
title: '已复制到剪贴板',
icon: 'success',
duration: 1500
});
},
fail: () => {
uni.showToast({
title: '复制失败',
icon: 'none',
duration: 1500
});
}
});
},
openKnowledge() { openKnowledge() {
uni.navigateTo({ uni.navigateTo({
url: '/pages/assistant/knowledge' url: '/pages/assistant/knowledge'
}) })
} },
} }
} }
</script> </script>
@@ -273,7 +515,7 @@ export default {
.assistant-info-card { .assistant-info-card {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
margin: 20rpx 30rpx 0 30rpx; margin: 0 0 0 0;
border-radius: 24rpx; border-radius: 24rpx;
padding: 24rpx; padding: 24rpx;
display: flex; display: flex;
@@ -340,18 +582,30 @@ export default {
} }
} }
.knowledge-icon { .action-buttons {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background: rgba(255, 255, 255, 0.8);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.icon-text { .action-btn {
font-size: 28rpx; width: 56rpx;
height: 56rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
background: rgba(255, 255, 255, 0.9);
}
.icon-text {
font-size: 24rpx;
}
} }
} }
} }
@@ -359,7 +613,7 @@ export default {
.quick-questions { .quick-questions {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
margin: 20rpx 30rpx; margin: 20rpx 0;
border-radius: 24rpx; border-radius: 24rpx;
padding: 24rpx; padding: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2); box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
@@ -400,7 +654,7 @@ export default {
} }
.chat-messages { .chat-messages {
margin-top: 20rpx; margin-top: 24rpx;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
@@ -425,21 +679,31 @@ export default {
.message-item.user { .message-item.user {
justify-content: flex-end; justify-content: flex-end;
padding: 0 10rpx 0 80rpx; padding: 0 0 0 40rpx;
} }
.message-item.ai { .message-item.ai {
justify-content: flex-start; justify-content: flex-start;
padding: 0 80rpx 0 10rpx; padding: 0 40rpx 0 0;
} }
.message-avatar { .message-avatar {
width: 64rpx; width: 64rpx;
height: 64rpx; height: 64rpx;
margin: 0 16rpx; margin: 0;
flex-shrink: 0; flex-shrink: 0;
} }
/* AI消息头像 - 右边距 */
.message-item.ai .message-avatar {
margin-right: 16rpx;
}
/* 用户消息头像 - 左边距 */
.message-item.user .message-avatar {
margin-left: 16rpx;
}
.assistant-avatar, .user-avatar { .assistant-avatar, .user-avatar {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -467,7 +731,7 @@ export default {
.message-content { .message-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 65%; max-width: 80%;
} }
.message-content.user { .message-content.user {
@@ -503,15 +767,106 @@ export default {
} }
.message-text { .message-text {
font-size: 28rpx; font-size: 26rpx;
line-height: 1.4; line-height: 1.5;
color: #333333; color: #333333;
} }
/* Markdown样式优化 */
.message-text .u-markdown {
font-size: 26rpx;
line-height: 1.5;
}
/* 重置Markdown内部样式 */
.message-text .u-markdown :deep(p) {
margin: 0;
padding: 0;
font-size: 26rpx;
line-height: 1.5;
color: #333333;
}
.message-text .u-markdown :deep(h1),
.message-text .u-markdown :deep(h2),
.message-text .u-markdown :deep(h3),
.message-text .u-markdown :deep(h4),
.message-text .u-markdown :deep(h5),
.message-text .u-markdown :deep(h6) {
margin: 8rpx 0 4rpx 0;
font-weight: 600;
color: #FF8A80;
}
.message-text .u-markdown :deep(ul),
.message-text .u-markdown :deep(ol) {
margin: 8rpx 0;
padding-left: 32rpx;
}
.message-text .u-markdown :deep(li) {
margin: 4rpx 0;
font-size: 26rpx;
line-height: 1.5;
}
.message-text .u-markdown :deep(code) {
background: rgba(255, 138, 128, 0.1);
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-size: 24rpx;
color: #FF8A80;
}
.message-text .u-markdown :deep(pre) {
background: rgba(255, 138, 128, 0.05);
padding: 16rpx;
border-radius: 12rpx;
margin: 8rpx 0;
overflow-x: auto;
}
.message-text .u-markdown :deep(blockquote) {
border-left: 6rpx solid #FF8A80;
padding-left: 16rpx;
margin: 8rpx 0;
color: #666;
font-style: italic;
}
.message-bubble.user .message-text { .message-bubble.user .message-text {
color: #ffffff; color: #ffffff;
} }
/* 用户消息的Markdown样式 */
.message-bubble.user .message-text .u-markdown :deep(p),
.message-bubble.user .message-text .u-markdown :deep(li) {
color: #ffffff;
}
.message-bubble.user .message-text .u-markdown :deep(h1),
.message-bubble.user .message-text .u-markdown :deep(h2),
.message-bubble.user .message-text .u-markdown :deep(h3),
.message-bubble.user .message-text .u-markdown :deep(h4),
.message-bubble.user .message-text .u-markdown :deep(h5),
.message-bubble.user .message-text .u-markdown :deep(h6) {
color: #ffffff;
}
.message-bubble.user .message-text .u-markdown :deep(code) {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
.message-bubble.user .message-text .u-markdown :deep(pre) {
background: rgba(255, 255, 255, 0.1);
}
.message-bubble.user .message-text .u-markdown :deep(blockquote) {
border-left-color: #ffffff;
color: rgba(255, 255, 255, 0.8);
}
.message-time { .message-time {
margin-top: 8rpx; margin-top: 8rpx;
font-size: 20rpx; font-size: 20rpx;
@@ -555,10 +910,8 @@ export default {
} }
.chat-input-area { .chat-input-area {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
padding: 16rpx 20rpx; padding: 16rpx 0;
//padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid rgba(255, 255, 255, 0.2); border-top: 1rpx solid rgba(255, 255, 255, 0.2);
} }
@@ -566,48 +919,131 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16rpx; gap: 16rpx;
margin-bottom: 0 !important;
.input-wrapper { .input-wrapper {
flex: 1; flex: 1;
background: rgba(255, 255, 255, 0.9);
border-radius: 28rpx; border-radius: 28rpx;
padding: 0 20rpx; padding: 0 20rpx;
backdrop-filter: blur(10rpx); backdrop-filter: blur(15rpx);
border: 1rpx solid rgba(255, 255, 255, 0.3); border: 3rpx solid rgba(255, 255, 255, 0.98);
height: 56rpx; height: 56rpx;
display: flex; display: flex;
align-items: center; align-items: center;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 0 0 1rpx rgba(255, 255, 255, 0.4),
0 2rpx 8rpx rgba(255, 255, 255, 0.15),
0 4rpx 16rpx rgba(255, 255, 255, 0.1),
inset 0 1rpx 0 rgba(255, 255, 255, 0.2);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 25rpx;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%, rgba(255, 255, 255, 0.05) 100%);
pointer-events: none;
}
&:focus-within {
border-color: rgba(255, 255, 255, 1);
box-shadow:
0 0 0 2rpx rgba(255, 255, 255, 0.6),
0 0 20rpx rgba(255, 255, 255, 0.3),
0 4rpx 12rpx rgba(255, 255, 255, 0.2),
0 8rpx 24rpx rgba(255, 255, 255, 0.15),
inset 0 1rpx 0 rgba(255, 255, 255, 0.3);
transform: translateY(-2rpx);
}
.message-input { .message-input {
width: 100%; width: 100%;
height: 100%; height: 100%;
font-size: 28rpx; font-size: 28rpx;
color: #333333; color: #ffffff;
border: none; border: none;
outline: none; outline: none;
background: transparent; background: transparent;
position: relative;
z-index: 1;
&::placeholder {
color: rgba(255, 255, 255, 0.95);
font-weight: 400;
text-shadow: 0 1rpx 2rpx rgba(255, 255, 255, 0.1);
}
} }
} }
.send-button { .send-button {
padding: 12rpx 20rpx; height: 56rpx;
padding: 0 20rpx;
border-radius: 24rpx; border-radius: 24rpx;
background: rgba(200, 200, 200, 0.5); background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%);
transition: all 0.3s ease; border: 3rpx solid rgba(255, 255, 255, 0.85);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
position: relative;
backdrop-filter: blur(10rpx);
box-shadow:
0 0 0 1rpx rgba(255, 255, 255, 0.3),
0 2rpx 6rpx rgba(255, 255, 255, 0.12),
0 4rpx 12rpx rgba(255, 255, 255, 0.08),
inset 0 1rpx 0 rgba(255, 255, 255, 0.15);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 21rpx;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, transparent 50%, rgba(255, 255, 255, 0.03) 100%);
pointer-events: none;
}
&:active {
transform: scale(0.96) translateY(1rpx);
}
.send-text { .send-text {
font-size: 28rpx; font-size: 28rpx;
color: #666666; color: rgba(255, 255, 255, 0.9);
font-weight: 500; font-weight: 500;
position: relative;
z-index: 1;
text-shadow: 0 1rpx 2rpx rgba(255, 255, 255, 0.1);
} }
} }
.send-button.active { .send-button.active {
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 100%); background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.4); border: 3rpx solid rgba(255, 255, 255, 0.98);
box-shadow:
0 0 0 2rpx rgba(255, 255, 255, 0.5),
0 0 16rpx rgba(255, 255, 255, 0.25),
0 4rpx 10rpx rgba(255, 255, 255, 0.18),
0 6rpx 20rpx rgba(255, 255, 255, 0.12),
inset 0 1rpx 0 rgba(255, 255, 255, 0.25);
transform: translateY(-2rpx);
&::before {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, transparent 50%, rgba(255, 255, 255, 0.06) 100%);
}
.send-text { .send-text {
color: #ffffff; color: rgba(255, 255, 255, 0.98);
font-weight: 600;
text-shadow: 0 1rpx 3rpx rgba(255, 255, 255, 0.15);
} }
} }
} }
+294
View File
@@ -0,0 +1,294 @@
<template>
<view class="knowledge-detail-container page-container-unified">
<!-- 知识内容卡片 -->
<view class="content-card">
<view class="knowledge-header">
<view class="title-row">
<text class="knowledge-title">{{ knowledgeDetail.title }}</text>
<view class="header-actions">
<view class="action-button" @click="toggleFavorite">
<u-icon :name="isFavorited ? 'heart-fill' : 'heart'" size="32" :color="isFavorited ? '#FF8A80' : '#999'"></u-icon>
</view>
<view class="action-button" @click="shareKnowledge">
<u-icon name="share" size="32" color="#999"></u-icon>
</view>
</view>
</view>
<view class="knowledge-meta">
<view class="category-tag">
<text class="tag-text">{{ knowledgeDetail.category }}</text>
</view>
<text class="read-count">{{ knowledgeDetail.readCount }}次阅读</text>
</view>
</view>
<view class="knowledge-content">
<rich-text :nodes="knowledgeDetail.content"></rich-text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
knowledgeId: '',
isFavorited: false,
knowledgeDetail: {
id: 1,
title: '猫咪日常饮食指南',
category: '饮食',
readCount: 1256,
content: `
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">猫咪的健康饮食是保证其长寿和活力的关键因素。作为肉食动物,猫咪对蛋白质的需求量很高,同时也需要适量的脂肪和少量的碳水化合物。</p>
<h3 style="margin: 24px 0 16px 0; color: #FF8A80; font-size: 18px; font-weight: 600;">营养需求</h3>
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">成年猫咪每天需要的营养成分包括:</p>
<ul style="margin-bottom: 16px; padding-left: 20px; color: #333;">
<li style="margin-bottom: 8px;">蛋白质:26-30%</li>
<li style="margin-bottom: 8px;">脂肪:9-15%</li>
<li style="margin-bottom: 8px;">碳水化合物:不超过10%</li>
<li style="margin-bottom: 8px;">水分:充足的饮水</li>
</ul>
<h3 style="margin: 24px 0 16px 0; color: #FF8A80; font-size: 18px; font-weight: 600;">喂食建议</h3>
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">建议采用定时定量的喂食方式,成年猫咪每天喂食2-3次,幼猫需要更频繁的喂食。选择高质量的猫粮,避免给猫咪喂食人类食物。</p>
<h3 style="margin: 24px 0 16px 0; color: #FF8A80; font-size: 18px; font-weight: 600;">注意事项</h3>
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">避免给猫咪喂食巧克力、洋葱、大蒜等对猫咪有害的食物。保持食物和水的新鲜,定期清洁食具。</p>
`
}
}
},
onLoad(options) {
if (options.id) {
this.knowledgeId = options.id;
this.loadKnowledgeDetail();
}
},
onShow() {
// 每次显示页面时检查收藏状态
this.checkFavoriteStatus();
},
methods: {
loadKnowledgeDetail() {
// 这里应该根据knowledgeId从服务器或本地数据加载详细信息
// 模拟数据加载
const knowledgeData = this.getKnowledgeById(this.knowledgeId);
if (knowledgeData) {
this.knowledgeDetail = { ...knowledgeData };
// 增加阅读次数
this.knowledgeDetail.readCount += 1;
}
},
getKnowledgeById(id) {
// 模拟从数据源获取知识详情
const allKnowledge = [
{
id: 1,
title: '猫咪日常饮食指南',
category: '饮食',
readCount: 1256,
content: `
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">猫咪的健康饮食是保证其长寿和活力的关键因素。作为肉食动物,猫咪对蛋白质的需求量很高,同时也需要适量的脂肪和少量的碳水化合物。</p>
<h3 style="margin: 24px 0 16px 0; color: #FF8A80; font-size: 18px; font-weight: 600;">营养需求</h3>
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">成年猫咪每天需要的营养成分包括:</p>
<ul style="margin-bottom: 16px; padding-left: 20px; color: #333;">
<li style="margin-bottom: 8px;">蛋白质:26-30%</li>
<li style="margin-bottom: 8px;">脂肪:9-15%</li>
<li style="margin-bottom: 8px;">碳水化合物:不超过10%</li>
<li style="margin-bottom: 8px;">水分:充足的饮水</li>
</ul>
<h3 style="margin: 24px 0 16px 0; color: #FF8A80; font-size: 18px; font-weight: 600;">喂食建议</h3>
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">建议采用定时定量的喂食方式,成年猫咪每天喂食2-3次,幼猫需要更频繁的喂食。选择高质量的猫粮,避免给猫咪喂食人类食物。</p>
<h3 style="margin: 24px 0 16px 0; color: #FF8A80; font-size: 18px; font-weight: 600;">注意事项</h3>
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">避免给猫咪喂食巧克力、洋葱、大蒜等对猫咪有害的食物。保持食物和水的新鲜,定期清洁食具。</p>
`
},
{
id: 2,
title: '猫咪健康体检指南',
category: '健康',
readCount: 892,
content: `
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">定期体检是维护猫咪健康的重要措施,可以及早发现和预防疾病。</p>
<h3 style="margin: 24px 0 16px 0; color: #FF8A80; font-size: 18px; font-weight: 600;">体检频率</h3>
<p style="margin-bottom: 16px; line-height: 1.6; color: #333;">幼猫建议每3个月体检一次,成年猫每6个月一次,老年猫每3个月一次。</p>
`
}
];
return allKnowledge.find(item => item.id == id);
},
checkFavoriteStatus() {
// 检查当前知识是否已收藏
// 这里应该从本地存储或服务器获取收藏状态
const favorites = uni.getStorageSync('knowledge_favorites') || [];
this.isFavorited = favorites.includes(this.knowledgeId);
},
toggleFavorite() {
this.isFavorited = !this.isFavorited;
// 更新本地存储的收藏列表
let favorites = uni.getStorageSync('knowledge_favorites') || [];
if (this.isFavorited) {
if (!favorites.includes(this.knowledgeId)) {
favorites.push(this.knowledgeId);
}
} else {
favorites = favorites.filter(id => id !== this.knowledgeId);
}
uni.setStorageSync('knowledge_favorites', favorites);
uni.showToast({
title: this.isFavorited ? '已收藏' : '已取消收藏',
icon: 'none'
});
},
shareKnowledge() {
uni.showActionSheet({
itemList: ['分享到微信', '分享到朋友圈', '复制链接'],
success: (res) => {
uni.showToast({
title: '分享功能开发中',
icon: 'none'
});
}
});
}
}
}
</script>
<style lang="scss" scoped>
.knowledge-detail-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
/* 知识内容卡片 */
.content-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
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);
margin-bottom: 40rpx;
flex: 1;
overflow-y: auto;
}
.knowledge-header {
margin-bottom: 32rpx;
}
.title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20rpx;
}
.knowledge-title {
font-size: 40rpx;
font-weight: 700;
color: #FF8A80;
flex: 1;
line-height: 1.3;
margin-right: 20rpx;
}
.header-actions {
display: flex;
gap: 12rpx;
flex-shrink: 0;
}
.action-button {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
&:active {
background: rgba(255, 138, 128, 0.1);
transform: scale(0.95);
}
}
.knowledge-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.category-tag {
background: rgba(255, 138, 128, 0.1);
border: 2rpx solid rgba(255, 138, 128, 0.3);
border-radius: 20rpx;
padding: 12rpx 20rpx;
&.small {
padding: 8rpx 16rpx;
border-radius: 16rpx;
}
.tag-text {
font-size: 26rpx;
color: #FF8A80;
font-weight: 500;
}
}
.read-count {
font-size: 26rpx;
color: #999;
}
.knowledge-content {
line-height: 1.6;
color: #333;
}
/* 响应式设计 */
@media (max-width: 375px) {
.knowledge-title {
font-size: 36rpx;
}
.content-card {
padding: 24rpx;
}
.related-item {
padding: 20rpx;
}
}
</style>
+42 -20
View File
@@ -1,5 +1,5 @@
<template> <template>
<view class="knowledge-container page-container-with-bg"> <view class="knowledge-container page-container-unified">
<view class="search-container"> <view class="search-container">
<u-search placeholder="搜索知识" v-model="searchKeyword" @search="searchKnowledge" @custom="searchKnowledge"></u-search> <u-search placeholder="搜索知识" v-model="searchKeyword" @search="searchKnowledge" @custom="searchKnowledge"></u-search>
</view> </view>
@@ -7,21 +7,25 @@
<view class="category-tabs"> <view class="category-tabs">
<u-tabs :list="categories" @click="switchCategory" :current="currentCategory"></u-tabs> <u-tabs :list="categories" @click="switchCategory" :current="currentCategory"></u-tabs>
</view> </view>
<scroll-view class="knowledge-list" scroll-y="true"> <view class="knowledge-list">
<view v-for="item in filteredKnowledge" :key="item.id" class="knowledge-card" @click="viewDetail(item)"> <scroll-view class="knowledge-scroll" scroll-y="true">
<view class="knowledge-item"> <view class="knowledge-content">
<text class="knowledge-title">{{ item.title }}</text> <view v-for="item in filteredKnowledge" :key="item.id" class="knowledge-card" @click="viewDetail(item)">
<text class="knowledge-summary">{{ item.summary }}</text> <view class="knowledge-item">
<view class="knowledge-meta"> <text class="knowledge-title">{{ item.title }}</text>
<view class="category-tag"> <text class="knowledge-summary">{{ item.summary }}</text>
<text class="tag-text">{{ item.category }}</text> <view class="knowledge-meta">
<view class="category-tag">
<text class="tag-text">{{ item.category }}</text>
</view>
<text class="read-count">{{ item.readCount }}次阅读</text>
</view>
</view> </view>
<text class="read-count">{{ item.readCount }}次阅读</text>
</view> </view>
</view> </view>
</view> </scroll-view>
</scroll-view> </view>
</view> </view>
</template> </template>
@@ -107,39 +111,57 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.knowledge-container { .knowledge-container {
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%); width: 100vw;
min-height: 100vh; height: 100vh;
display: flex;
flex-direction: column;
position: relative;
} }
.search-container { .search-container {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
margin: 20rpx 30rpx; margin: 0 0 0 0;
border-radius: 24rpx; border-radius: 24rpx;
padding: 20rpx; padding: 20rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2); box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3); border: 1rpx solid rgba(255, 255, 255, 0.3);
flex-shrink: 0;
} }
.category-tabs { .category-tabs {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
margin: 20rpx 30rpx; margin: 20rpx 0 0 0;
border-radius: 24rpx; border-radius: 24rpx;
padding: 20rpx 0; padding: 20rpx 0;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2); box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3); border: 1rpx solid rgba(255, 255, 255, 0.3);
flex-shrink: 0;
} }
.knowledge-list { .knowledge-list {
height: calc(100vh - 300rpx); margin-top: 20rpx;
padding: 0 30rpx; flex: 1;
overflow: hidden;
}
.knowledge-scroll {
width: 100%;
height: 100%;
}
.knowledge-content {
padding: 20rpx 0;
padding-bottom: 40rpx;
min-height: 100%;
box-sizing: border-box;
} }
.knowledge-card { .knowledge-card {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
margin: 20rpx 0; margin-bottom: 24rpx;
border-radius: 24rpx; border-radius: 24rpx;
padding: 24rpx; padding: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2); box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
+411
View File
@@ -0,0 +1,411 @@
<template>
<view class="phone-auth-container page-container-with-bg">
<!-- 顶部说明卡片 -->
<view class="auth-card">
<view class="auth-header">
<view class="auth-icon">
<text class="icon-text">📱</text>
</view>
<text class="auth-title">手机号授权</text>
<text class="auth-subtitle">为了更好地为您提供服务,需要获取您的手机号</text>
</view>
<view class="auth-benefits">
<view class="benefit-item">
<text class="benefit-icon">🔒</text>
<text class="benefit-text">保护账号安全</text>
</view>
<view class="benefit-item">
<text class="benefit-icon">📞</text>
<text class="benefit-text">重要消息通知</text>
</view>
<view class="benefit-item">
<text class="benefit-icon">🎯</text>
<text class="benefit-text">个性化服务</text>
</view>
</view>
</view>
<!-- 授权按钮卡片 -->
<view class="button-card">
<button
class="auth-button"
:class="{ 'loading': authorizing }"
open-type="getPhoneNumber"
@getphonenumber="onGetPhoneNumber"
:disabled="authorizing"
>
<view class="button-content">
<view v-if="authorizing" class="loading-spinner"></view>
<text v-if="!authorizing">授权手机号</text>
<text v-else>授权中...</text>
</view>
</button>
<view class="auth-tips">
<text class="tips-text">点击授权后,我们将获取您的手机号用于账号验证</text>
</view>
</view>
<!-- 隐私说明 -->
<view class="privacy-card">
<text class="privacy-title">隐私保护</text>
<text class="privacy-text">我们承诺严格保护您的个人信息,仅用于账号安全验证和必要的服务通知,不会用于其他商业用途。</text>
</view>
<!-- 跳过按钮 -->
<view class="skip-section">
<text class="skip-button" @click="skipAuth">暂时跳过</text>
</view>
</view>
</template>
<script>
import { ref } from 'vue'
import { wxPhoneLogin } from '@/http/api/auth.js'
import {
savePhoneAuthData,
markPhoneSkipped,
STORAGE_KEYS
} from '@/utils/loginState.js'
export default {
name: 'PhoneAuthPage',
setup() {
// 响应式数据
const authorizing = ref(false)
// 防止重复点击
let isProcessing = false
// 错误处理函数
const handleError = (error) => {
let errorMessage = '授权失败,请重试'
let shouldReturnToLogin = false
console.error('手机号授权错误详情:', error)
if (error.message) {
if (error.message.includes('网络')) {
errorMessage = '网络连接异常,请检查网络后重试'
} else if (error.message.includes('登录状态已过期') || error.message.includes('重新获取微信登录code失败')) {
errorMessage = '登录状态已过期,请重新登录'
shouldReturnToLogin = true
} else if (error.message.includes('API返回数据格式错误')) {
errorMessage = '服务器响应异常,请稍后重试'
} else if (error.message.includes('用户拒绝')) {
errorMessage = '需要授权手机号才能继续使用'
} else if (error.code === 401) {
errorMessage = '授权已过期,请重新登录'
shouldReturnToLogin = true
} else if (error.code === 500) {
errorMessage = '服务器错误,请稍后重试'
} else {
errorMessage = error.message || '授权失败,请重试'
}
}
uni.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
})
// 如果需要返回登录页面
if (shouldReturnToLogin) {
setTimeout(() => {
uni.reLaunch({
url: '/pages/profile/profile'
})
}, 3000)
}
}
// 方法定义
const onGetPhoneNumber = async (e) => {
console.log('获取手机号回调:', e)
// 防止重复点击
if (isProcessing) {
console.log('正在处理中,忽略重复点击')
return
}
if (e.detail.errMsg === 'getPhoneNumber:ok') {
isProcessing = true
authorizing.value = true
try {
// 获取微信登录code
let loginCode = uni.getStorageSync(STORAGE_KEYS.WX_LOGIN_CODE)
// 检查登录code是否存在和有效
if (!loginCode) {
console.warn('微信登录code不存在,尝试重新获取')
// 尝试重新获取微信登录code
try {
const loginRes = await new Promise((resolve, reject) => {
uni.login({
success: resolve,
fail: reject
})
})
if (loginRes.code) {
loginCode = loginRes.code
uni.setStorageSync(STORAGE_KEYS.WX_LOGIN_CODE, loginCode)
console.log('重新获取微信登录code成功')
} else {
throw new Error('重新获取微信登录code失败')
}
} catch (loginError) {
console.error('重新获取微信登录code失败:', loginError)
throw new Error('登录状态已过期,请重新登录')
}
}
// 调用后端API进行手机号登录
const phoneData = {
code: loginCode,
encryptedData: e.detail.encryptedData,
iv: e.detail.iv
}
// 调用手机号登录API
const result = await wxPhoneLogin(phoneData, {
custom: {
loading: false, // 使用页面自己的loading状态
catch: true // 确保错误能被catch到
}
})
// 处理登录结果
if (result && result.token) {
// 保存手机号登录数据
savePhoneAuthData({
token: result.token,
refreshToken: result.refreshToken,
userInfo: result.userInfo || result
})
uni.showToast({
title: '手机号授权成功',
icon: 'success',
duration: 1500
})
// 跳转到用户信息设置页面
setTimeout(() => {
uni.navigateTo({
url: '/pages/profile/user-info?mode=setup'
})
}, 1500)
} else {
throw new Error('手机号登录失败')
}
} catch (error) {
console.error('手机号授权失败:', error)
handleError(error)
} finally {
authorizing.value = false
isProcessing = false
}
} else {
// 用户拒绝授权
uni.showToast({
title: '需要授权手机号才能继续',
icon: 'none'
})
}
}
const skipAuth = () => {
uni.showModal({
title: '确认跳过',
content: '跳过手机号授权可能会影响部分功能的使用,确定要跳过吗?',
success: (res) => {
if (res.confirm) {
// 使用状态管理工具标记跳过状态
markPhoneSkipped()
// 跳转到个人信息设置页面
uni.navigateTo({
url: '/pages/profile/user-info?mode=setup&phoneSkipped=true'
})
}
}
})
}
return {
authorizing,
onGetPhoneNumber,
skipAuth
}
}
}
</script>
<style lang="scss" scoped>
.phone-auth-container {
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
min-height: 100vh;
padding: 40rpx 30rpx;
display: flex;
flex-direction: column;
}
/* 通用卡片样式 */
.auth-card,
.button-card,
.privacy-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
}
/* 授权说明卡片 */
.auth-header {
text-align: center;
margin-bottom: 32rpx;
.auth-icon {
margin-bottom: 16rpx;
.icon-text {
font-size: 80rpx;
}
}
.auth-title {
display: block;
font-size: 36rpx;
font-weight: 600;
color: #333333;
margin-bottom: 12rpx;
}
.auth-subtitle {
display: block;
font-size: 28rpx;
color: #666666;
line-height: 1.5;
}
}
.auth-benefits {
.benefit-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.benefit-icon {
font-size: 32rpx;
margin-right: 16rpx;
}
.benefit-text {
font-size: 28rpx;
color: #333333;
}
}
}
/* 按钮卡片 */
.button-card {
text-align: center;
.auth-button {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 24rpx;
border: none;
color: white;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
transition: all 0.3s ease;
&.loading {
opacity: 0.8;
transform: scale(0.98);
}
&:disabled {
opacity: 0.7;
}
.button-content {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.loading-spinner {
width: 32rpx;
height: 32rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top: 4rpx solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.auth-tips {
.tips-text {
font-size: 24rpx;
color: #999999;
line-height: 1.4;
}
}
}
/* 隐私说明卡片 */
.privacy-card {
.privacy-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333333;
margin-bottom: 12rpx;
}
.privacy-text {
font-size: 24rpx;
color: #666666;
line-height: 1.6;
}
}
/* 跳过按钮 */
.skip-section {
margin-top: auto;
text-align: center;
padding: 20rpx 0;
.skip-button {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
text-decoration: underline;
}
}
</style>
+43 -56
View File
@@ -1,5 +1,5 @@
<template> <template>
<view class="pets-container gradient-bg page-container"> <view class="pets-container page-container-unified">
<!-- 标签切换容器 --> <!-- 标签切换容器 -->
<view class="tabs-container" v-if="petsList.length > 0"> <view class="tabs-container" v-if="petsList.length > 0">
<view class="tabs-header"> <view class="tabs-header">
@@ -210,6 +210,27 @@
</template> </template>
<script> <script>
// 配置数据
const ACTION_ITEMS_CONFIG = [
{ id: 1, icon: '📝', text: '添加记录', action: 'addRecord' },
{ id: 2, icon: '💬', text: 'AI聊天', action: 'chatWithPet' },
{ id: 3, icon: '📊', text: '健康档案', action: 'viewHealth' },
{ id: 4, icon: '📸', text: '成长时光', action: 'viewTimeline' },
{ id: 5, icon: '📋', text: '所有记录', action: 'viewRecords' },
{ id: 6, icon: '🔔', text: '提醒设置', action: 'showReminder' }
]
const PET_EMOJI_MAP = {
'橘猫': '🐱',
'金毛': '🐶',
'垂耳兔': '🐰',
'哈士奇': '🐕',
'波斯猫': '😸',
'泰迪': '🐩',
'仓鼠': '🐹',
'鹦鹉': '🦜'
}
export default { export default {
data() { data() {
return { return {
@@ -218,37 +239,8 @@ export default {
currentTabIndex: 0, currentTabIndex: 0,
petRecords: [], petRecords: [],
petExpenses: [], petExpenses: [],
mainActionItems: [ mainActionItems: ACTION_ITEMS_CONFIG,
{ id: 1, icon: '📝', text: '添加记录', action: 'addRecord' }, recentInteractions: []
{ id: 2, icon: '💬', text: 'AI聊天', action: 'chatWithPet' },
{ id: 3, icon: '📊', text: '健康档案', action: 'viewHealth' },
{ id: 4, icon: '📸', text: '成长时光', action: 'viewTimeline' },
{ id: 5, icon: '📋', text: '所有记录', action: 'viewRecords' },
{ id: 6, icon: '🔔', text: '提醒设置', action: 'showReminder' }
],
recentInteractions: [
{
id: 1,
petEmoji: '🐱',
title: '小橘完成了今日散步',
time: '2小时前',
typeIcon: '🚶'
},
{
id: 2,
petEmoji: '🐶',
title: '小白吃完了晚餐',
time: '4小时前',
typeIcon: '🍽️'
},
{
id: 3,
petEmoji: '🐱',
title: '小橘的体重记录已更新',
time: '1天前',
typeIcon: '⚖️'
}
]
} }
}, },
onShow() { onShow() {
@@ -304,26 +296,12 @@ export default {
// 获取宠物表情符号 // 获取宠物表情符号
getPetEmoji(breed) { getPetEmoji(breed) {
const emojiMap = { return PET_EMOJI_MAP[breed] || '🐾'
'橘猫': '🐱',
'金毛': '🐶',
'垂耳兔': '🐰',
'哈士奇': '🐕',
'波斯猫': '😸',
'泰迪': '🐩',
'仓鼠': '🐹',
'鹦鹉': '🦜'
}
return emojiMap[breed] || '🐾'
}, },
loadPets() { // 生成默认宠物数据
try { generateDefaultPets() {
let pets = uni.getStorageSync('pets') || [] return [
// 如果没有宠物数据,添加一些模拟数据
if (pets.length === 0) {
pets = [
{ {
id: 1, id: 1,
name: '小橘', name: '小橘',
@@ -474,7 +452,16 @@ export default {
personality: ['慵懒', '贪吃', '老成'], personality: ['慵懒', '贪吃', '老成'],
lastRecord: '2024-01-11' lastRecord: '2024-01-11'
} }
] ]
},
loadPets() {
try {
let pets = uni.getStorageSync('pets') || []
// 如果没有宠物数据,使用默认数据
if (pets.length === 0) {
pets = this.generateDefaultPets()
// 保存模拟数据到本地存储 // 保存模拟数据到本地存储
uni.setStorageSync('pets', pets) uni.setStorageSync('pets', pets)
} }
@@ -653,12 +640,12 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.pets-container { .pets-container {
padding-bottom: 120rpx; /* 边距由外层page-container-unified统一管理 */
} }
.tabs-container { .tabs-container {
background: rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.85);
margin: 20rpx 20rpx 30rpx; margin: 0 0 30rpx 0;
border-radius: 40rpx; border-radius: 40rpx;
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
overflow: visible; overflow: visible;
@@ -758,7 +745,7 @@ export default {
} }
.tab-content { .tab-content {
padding: 30rpx; padding: 0;
} }
.tab-panel { .tab-panel {
@@ -941,7 +928,7 @@ export default {
} }
.functions-section { .functions-section {
margin: 16rpx 30rpx 20rpx; margin: 16rpx 0 20rpx 0;
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
border-radius: 16rpx; border-radius: 16rpx;
padding: 16rpx 0; padding: 16rpx 0;
@@ -1037,7 +1024,7 @@ export default {
} }
.cards-section { .cards-section {
padding: 0 30rpx; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16rpx; gap: 16rpx;
+497 -228
View File
@@ -1,8 +1,8 @@
<template> <template>
<view class="profile-container page-container-with-bg"> <view class="profile-container page-container-unified">
<!-- 用户信息卡片 --> <!-- 用户信息卡片 -->
<view class="user-info-card"> <view class="user-info-card">
<view class="user-avatar-section"> <view class="user-avatar-section" :class="{ 'logged-in': isLoggedIn }" @click="handleUserAction">
<view class="avatar-wrapper"> <view class="avatar-wrapper">
<u-avatar <u-avatar
:src="userInfo.avatarUrl || ''" :src="userInfo.avatarUrl || ''"
@@ -13,28 +13,29 @@
color="white" color="white"
font-size="40" font-size="40"
></u-avatar> ></u-avatar>
<view class="online-status" v-if="userInfo.nickName"> <view class="online-status" v-if="isLoggedIn">
<view class="status-dot"></view> <view class="status-dot"></view>
</view> </view>
<!-- 登录提示图标,仅在未登录时显示 -->
<view class="login-hint" v-if="!isLoggedIn">
<text class="hint-icon">👋</text>
</view>
</view> </view>
</view> </view>
<view class="user-info-section"> <view class="user-info-section">
<view class="user-name">{{ userInfo.nickName || '点击登录' }}</view> <view class="user-name">{{ isLoggedIn ? (userInfo.nickName || '用户') : '点击头像登录' }}</view>
<view class="user-status">{{ userInfo.nickName ? '已登录' : '未登录' }}</view> <view class="user-status">{{ isLoggedIn ? '已登录' : '未登录' }}</view>
<view class="login-days" v-if="userInfo.nickName">已使用 {{ loginDays }} 天</view> <view class="login-days" v-if="isLoggedIn">已使用 {{ loginDays }} 天</view>
</view>
<view class="user-action" @click="handleUserAction">
<text class="action-icon">{{ userInfo.nickName ? '⚙️' : '👋' }}</text>
</view> </view>
</view> </view>
<!-- 宠物概览统计卡片 --> <!-- 宠物概览统计卡片 -->
<view class="stats-card"> <view class="stats-card">
<view class="stats-header"> <view class="stats-header">
<text class="stats-title">我的宠物</text> <view class="header-left">
<view class="stats-action" @click="navigateTo('/pages/pets/pets')"> <text class="card-icon">🐱</text>
<text class="action-text">管理</text> <text class="card-title">我的宠物</text>
<text class="action-arrow">→</text>
</view> </view>
</view> </view>
<view class="stats-grid"> <view class="stats-grid">
@@ -63,9 +64,22 @@
<text class="card-icon">👨‍👩‍👧‍👦</text> <text class="card-icon">👨‍👩‍👧‍👦</text>
<text class="card-title">我的家庭</text> <text class="card-title">我的家庭</text>
</view> </view>
<view class="header-right" @click="navigateToFamily"> </view>
<text class="member-count">{{ familyStats.memberCount }}人</text> <view class="family-stats">
<text class="action-arrow">→</text> <view class="stat-item" @click="navigateToFamily">
<view class="stat-number">{{ familyStats.memberCount }}</view>
<view class="stat-label">成员</view>
<view class="stat-icon">👥</view>
</view>
<view class="stat-item" @click="navigateTo('/pages/pets/pets')">
<view class="stat-number">{{ familyStats.sharedPets || petStats.petCount }}</view>
<view class="stat-label">共享宠物</view>
<view class="stat-icon">🐾</view>
</view>
<view class="stat-item" @click="navigateToFamily">
<view class="stat-number">{{ familyStats.activityCount || 0 }}</view>
<view class="stat-label">动态</view>
<view class="stat-icon">📱</view>
</view> </view>
</view> </view>
<view class="family-actions"> <view class="family-actions">
@@ -87,9 +101,6 @@
<text class="card-icon">🏠</text> <text class="card-icon">🏠</text>
<text class="card-title">领养管理</text> <text class="card-title">领养管理</text>
</view> </view>
<view class="header-right" @click="navigateTo('/pages/adoption/my-adoption')">
<text class="action-arrow">→</text>
</view>
</view> </view>
<view class="adoption-stats"> <view class="adoption-stats">
<view class="adoption-item" @click="navigateTo('/pages/adoption/my-published')"> <view class="adoption-item" @click="navigateTo('/pages/adoption/my-published')">
@@ -107,53 +118,32 @@
</view> </view>
</view> </view>
<!-- 设置功能卡片 --> <!-- 设置与帮助卡片 -->
<view class="settings-card"> <view class="settings-card">
<view class="card-header">
<view class="header-left">
<text class="card-icon">⚙️</text>
<text class="card-title">设置与帮助</text>
</view>
</view>
<view class="settings-list"> <view class="settings-list">
<view class="setting-item" @click="navigateTo('/pages/profile/notifications')"> <view class="setting-item floating-indicator" @click="navigateTo('/pages/profile/feedback')">
<view class="setting-left">
<text class="setting-icon">🔔</text>
<text class="setting-text">消息通知</text>
</view>
<view class="setting-right">
<text class="setting-badge" v-if="notificationCount > 0">{{ notificationCount }}</text>
<text class="setting-arrow">→</text>
</view>
</view>
<view class="setting-item" @click="navigateTo('/pages/profile/privacy')">
<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="navigateTo('/pages/profile/feedback')">
<view class="setting-left"> <view class="setting-left">
<text class="setting-icon">💬</text> <text class="setting-icon">💬</text>
<text class="setting-text">意见反馈</text> <text class="setting-text">意见反馈</text>
</view> </view>
<view class="setting-right">
<text class="setting-arrow">→</text>
</view>
</view>
<view class="setting-item" @click="navigateTo('/pages/profile/about')">
<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> </view>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
import { reactive, ref, onMounted, computed } from 'vue' import { reactive, ref, onMounted, computed } from 'vue'
import { uploadImage } from '@/http/api/common.js'
import { getUserInfo, getWxUserInfo, updateUserInfo } from '@/http/api/profile.js'
export default { export default {
name: 'ProfilePage', name: 'ProfilePage',
@@ -165,7 +155,15 @@ export default {
}) })
const loginDays = ref(0) const loginDays = ref(0)
const notificationCount = ref(3)
// 计算属性
const isLoggedIn = computed(() => {
return !!uni.getStorageSync('token')
})
// 宠物统计数据 // 宠物统计数据
const petStats = reactive({ const petStats = reactive({
@@ -176,7 +174,9 @@ export default {
// 家庭统计数据 // 家庭统计数据
const familyStats = reactive({ const familyStats = reactive({
memberCount: 1 memberCount: 1,
sharedPets: 0,
activityCount: 0
}) })
// 领养统计数据 // 领养统计数据
@@ -187,12 +187,69 @@ export default {
// 方法定义 // 方法定义
const initPage = () => { const initPage = () => {
checkLogin() const token = uni.getStorageSync('token')
loadUserStats() if (token) {
// 有token时加载用户信息和统计数据
loadUserInfo()
loadUserStats()
} else {
// 没有token时只加载本地缓存的用户信息(如果有)
const savedUserInfo = uni.getStorageSync('userInfo')
if (savedUserInfo) {
userInfo.value = savedUserInfo
}
}
} }
const checkLogin = () => { const loadUserInfo = async () => {
// 检查是否已登录 const token = uni.getStorageSync('token')
if (!token) {
return
}
try {
// 优先获取微信用户信息
const wxUserInfo = await getWxUserInfo({
custom: {
loading: false,
toast: false
}
})
if (wxUserInfo) {
userInfo.value = wxUserInfo
// 同步到本地存储
uni.setStorageSync('userInfo', wxUserInfo)
uni.setStorageSync('wxUserInfo', wxUserInfo)
calculateLoginDays()
return
}
} catch (error) {
console.log('获取微信用户信息失败,尝试获取基本用户信息:', error)
// 微信用户信息获取失败,尝试获取基本用户信息
try {
const apiUserInfo = await getUserInfo({
custom: {
loading: false,
toast: false
}
})
if (apiUserInfo) {
userInfo.value = apiUserInfo
// 同步到本地存储
uni.setStorageSync('userInfo', apiUserInfo)
calculateLoginDays()
return
}
} catch (basicError) {
console.log('获取基本用户信息也失败,使用本地缓存:', basicError)
}
}
// API获取失败时,尝试从本地存储加载用户信息
const savedUserInfo = uni.getStorageSync('userInfo') const savedUserInfo = uni.getStorageSync('userInfo')
if (savedUserInfo) { if (savedUserInfo) {
userInfo.value = savedUserInfo userInfo.value = savedUserInfo
@@ -234,8 +291,10 @@ export default {
petStats.reminderCount = pendingReminders.length petStats.reminderCount = pendingReminders.length
// 加载家庭数据 // 加载家庭数据
const familyData = uni.getStorageSync('familyData') || { members: [] } const familyData = uni.getStorageSync('familyData') || { members: [], activities: [] }
familyStats.memberCount = familyData.members.length || 1 familyStats.memberCount = familyData.members.length || 1
familyStats.sharedPets = Math.min(petStats.petCount, familyStats.memberCount > 1 ? petStats.petCount : 0)
familyStats.activityCount = familyData.activities ? familyData.activities.length : 0
// 加载领养数据 // 加载领养数据
const adoptionData = uni.getStorageSync('adoptionData') || { const adoptionData = uni.getStorageSync('adoptionData') || {
@@ -245,68 +304,53 @@ export default {
adoptionStats.publishedCount = adoptionData.published.length adoptionStats.publishedCount = adoptionData.published.length
adoptionStats.applicationCount = adoptionData.applications.length adoptionStats.applicationCount = adoptionData.applications.length
// 加载通知数量
const notifications = uni.getStorageSync('notifications') || []
const unreadNotifications = notifications.filter(n => !n.read)
notificationCount.value = unreadNotifications.length
} catch (error) { } catch (error) {
console.error('加载用户统计数据失败:', error) console.error('加载用户统计数据失败:', error)
// 设置默认值 // 重置为默认值
petStats.petCount = 0 resetStatsToDefault()
petStats.recordCount = 0
petStats.reminderCount = 0
familyStats.memberCount = 1
adoptionStats.publishedCount = 0
adoptionStats.applicationCount = 0
} }
} }
const handleUserAction = () => { // 重置统计数据为默认值
if (userInfo.value.nickName) { const resetStatsToDefault = () => {
// 已登录,跳转到个人信息设置 Object.assign(petStats, {
navigateTo('/pages/profile/user-info') petCount: 0,
} else { recordCount: 0,
// 未登录,执行登录 reminderCount: 0
wxLogin() })
} Object.assign(familyStats, {
} memberCount: 1,
sharedPets: 0,
const wxLogin = () => { activityCount: 0
uni.login({ })
provider: 'weixin', Object.assign(adoptionStats, {
success: (res) => { publishedCount: 0,
console.log('登录成功', res) applicationCount: 0
// 模拟登录成功
userInfo.value = {
nickName: '宠物主人',
avatarUrl: ''
}
uni.setStorageSync('userInfo', userInfo.value)
uni.setStorageSync('loginDate', new Date().toISOString())
calculateLoginDays()
uni.showToast({
title: '登录成功',
icon: 'success'
})
},
fail: (err) => {
console.error('登录失败', err)
uni.showToast({
title: '登录失败',
icon: 'none'
})
}
}) })
} }
const navigateTo = (url) => { const handleUserAction = async () => {
const token = uni.getStorageSync('token')
if (token) {
// 有token,直接跳转到个人信息设置
navigateTo('/pages/profile/user-info')
} else {
// 没有token,跳转到登录页面
navigateTo('/pages/login/login')
}
}
// 统一的页面导航方法
const navigateTo = (url, options = {}) => {
const { fallbackMessage = '页面开发中' } = options
uni.navigateTo({ uni.navigateTo({
url, url,
fail: () => { fail: (error) => {
console.warn('页面导航失败:', url, error)
uni.showToast({ uni.showToast({
title: '页面开发中', title: fallbackMessage,
icon: 'none' icon: 'none'
}) })
} }
@@ -314,7 +358,9 @@ export default {
} }
const navigateToFamily = () => { const navigateToFamily = () => {
navigateTo('/pages/profile/family') navigateTo('/pages/profile/family', {
fallbackMessage: '家庭功能开发中'
})
} }
const inviteFamily = () => { const inviteFamily = () => {
@@ -346,11 +392,21 @@ export default {
// 刷新页面数据 // 刷新页面数据
const refreshData = () => { const refreshData = () => {
// 只刷新统计数据,用户信息在initPage时已加载
loadUserStats() loadUserStats()
} }
// 强制刷新用户信息(用于特殊情况)
const forceRefreshUserInfo = () => {
const token = uni.getStorageSync('token')
if (token) {
loadUserInfo()
}
}
// 页面显示时刷新数据 // 页面显示时刷新数据
const onShow = () => { const onShow = () => {
// 只刷新数据,不重复调用loadUserInfo
refreshData() refreshData()
} }
@@ -370,41 +426,30 @@ export default {
return num.toString() return num.toString()
} }
// 检查是否有新功能提示
const checkNewFeatures = () => {
const lastVersion = uni.getStorageSync('lastAppVersion') || '1.0.0'
const currentVersion = '1.1.0' // 当前版本
if (lastVersion !== currentVersion) {
// 显示新功能提示
setTimeout(() => {
uni.showModal({
title: '发现新功能',
content: '新增了家庭共享和领养管理功能,快来体验吧!',
confirmText: '去看看',
success: (res) => {
if (res.confirm) {
uni.setStorageSync('lastAppVersion', currentVersion)
}
}
})
}, 2000)
}
}
// 初始化时检查新功能
// 页面初始化
onMounted(() => { onMounted(() => {
initPage() initPage()
checkNewFeatures()
}) })
return { return {
userInfo, userInfo,
loginDays, loginDays,
notificationCount, isLoggedIn,
petStats, petStats,
familyStats, familyStats,
adoptionStats, adoptionStats,
loadUserInfo,
forceRefreshUserInfo,
handleUserAction, handleUserAction,
navigateTo, navigateTo,
navigateToFamily, navigateToFamily,
@@ -412,6 +457,7 @@ export default {
showReminders, showReminders,
refreshData, refreshData,
formatNumber, formatNumber,
resetStatsToDefault,
onShow, onShow,
onPullDownRefresh onPullDownRefresh
} }
@@ -419,25 +465,33 @@ export default {
// 页面生命周期 // 页面生命周期
onShow() { onShow() {
this.onShow() // 重新检查登录状态并刷新数据
const token = uni.getStorageSync('token')
if (token) {
// 有token时总是重新获取最新用户信息
this.loadUserInfo()
}
this.refreshData()
}, },
onPullDownRefresh() { onPullDownRefresh() {
this.onPullDownRefresh() this.refreshData()
uni.stopPullDownRefresh()
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.profile-container { .profile-container {
padding-bottom: 40rpx; /* 边距由外层page-container-unified统一管理 */
} }
/* 用户信息卡片 */ /* 用户信息卡片 */
.user-info-card { .user-info-card {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
margin: 0 30rpx 24rpx 30rpx; margin: 0 0 24rpx 0;
border-radius: 24rpx; border-radius: 24rpx;
padding: 32rpx; padding: 32rpx;
display: flex; display: flex;
@@ -451,6 +505,38 @@ export default {
} }
.user-avatar-section { .user-avatar-section {
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
padding: 12rpx;
position: relative;
// 未登录时的可点击提示
&:not(.logged-in) {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2rpx dashed rgba(255, 138, 128, 0.3);
border-radius: 50%;
animation: breathe 3s ease-in-out infinite;
}
}
&:hover {
background: rgba(255, 138, 128, 0.15);
transform: scale(1.08);
box-shadow: 0 8rpx 24rpx rgba(255, 138, 128, 0.25);
}
&:active {
transform: scale(0.92);
transition: all 0.1s ease;
}
.avatar-wrapper { .avatar-wrapper {
position: relative; position: relative;
@@ -473,6 +559,41 @@ export default {
border-radius: 50%; border-radius: 50%;
} }
} }
.login-hint {
position: absolute;
bottom: -8rpx;
right: -8rpx;
width: 40rpx;
height: 40rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.4);
animation: pulse 2s infinite;
border: 3rpx solid white;
z-index: 10;
&::before {
content: '';
position: absolute;
top: -6rpx;
left: -6rpx;
right: -6rpx;
bottom: -6rpx;
border: 2rpx solid rgba(255, 138, 128, 0.3);
border-radius: 50%;
animation: ripple 2s infinite;
}
.hint-icon {
font-size: 22rpx;
color: white;
font-weight: bold;
}
}
} }
} }
@@ -510,7 +631,7 @@ export default {
.stats-card { .stats-card {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
margin: 0 30rpx 24rpx 30rpx; margin: 0 0 24rpx 0;
border-radius: 24rpx; border-radius: 24rpx;
padding: 32rpx; padding: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2); box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
@@ -522,25 +643,19 @@ export default {
align-items: center; align-items: center;
margin-bottom: 24rpx; margin-bottom: 24rpx;
.stats-title { .header-left {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
.stats-action {
display: flex; display: flex;
align-items: center; align-items: center;
.action-text { .card-icon {
font-size: 24rpx; font-size: 36rpx;
color: #FF8A80; margin-right: 16rpx;
margin-right: 8rpx;
} }
.action-arrow { .card-title {
font-size: 24rpx; font-size: 32rpx;
color: #FF8A80; font-weight: 600;
color: #333333;
} }
} }
} }
@@ -586,7 +701,7 @@ export default {
.family-card { .family-card {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
margin: 0 30rpx 24rpx 30rpx; margin: 0 0 24rpx 0;
border-radius: 24rpx; border-radius: 24rpx;
padding: 32rpx; padding: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2); box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
@@ -624,9 +739,46 @@ export default {
margin-right: 8rpx; margin-right: 8rpx;
} }
.action-arrow {
font-size: 24rpx; }
}
.family-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
margin-bottom: 32rpx;
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 16rpx;
background: rgba(255, 138, 128, 0.05);
border-radius: 16rpx;
transition: all 0.3s ease;
cursor: pointer;
&:active {
transform: scale(0.95);
background: rgba(255, 138, 128, 0.1);
}
.stat-number {
font-size: 36rpx;
font-weight: 600;
color: #FF8A80; color: #FF8A80;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666666;
margin-bottom: 8rpx;
}
.stat-icon {
font-size: 28rpx;
} }
} }
} }
@@ -667,7 +819,7 @@ export default {
.adoption-card { .adoption-card {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx); backdrop-filter: blur(20rpx);
margin: 0 30rpx 24rpx 30rpx; margin: 0 0 24rpx 0;
border-radius: 24rpx; border-radius: 24rpx;
padding: 32rpx; padding: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2); box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
@@ -696,10 +848,7 @@ export default {
} }
.header-right { .header-right {
.action-arrow { /* 悬浮指示器样式已在通用样式中定义 */
font-size: 24rpx;
color: #FF8A80;
}
} }
} }
@@ -761,78 +910,13 @@ export default {
} }
} }
/* 设置功能卡片 */
.settings-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 0 30rpx 24rpx 30rpx;
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
overflow: hidden;
.settings-list {
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
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);
}
.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-badge {
background: #FF8A80;
color: white;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
margin-right: 12rpx;
min-width: 32rpx;
text-align: center;
}
.setting-arrow {
font-size: 24rpx;
color: #FF8A80;
}
}
}
}
}
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 375px) { @media (max-width: 375px) {
.profile-container { .profile-container {
.user-info-card { .user-info-card {
margin: 0 20rpx 20rpx 20rpx; margin: 0 0 20rpx 0;
padding: 24rpx; padding: 24rpx;
} }
@@ -840,7 +924,7 @@ export default {
.family-card, .family-card,
.adoption-card, .adoption-card,
.settings-card { .settings-card {
margin: 0 20rpx 20rpx 20rpx; margin: 0 0 20rpx 0;
padding: 24rpx; padding: 24rpx;
} }
@@ -856,6 +940,27 @@ export default {
} }
} }
.family-stats {
gap: 16rpx;
margin-bottom: 24rpx;
.stat-item {
padding: 20rpx 12rpx;
.stat-number {
font-size: 32rpx;
}
.stat-label {
font-size: 22rpx;
}
.stat-icon {
font-size: 24rpx;
}
}
}
.family-actions { .family-actions {
gap: 12rpx; gap: 12rpx;
@@ -898,6 +1003,113 @@ export default {
} }
} }
/* 悬浮指示器通用样式 */
.floating-indicator {
position: relative;
cursor: pointer;
&::after {
content: '';
position: absolute;
right: 16rpx;
top: 50%;
transform: translateY(-50%);
width: 12rpx;
height: 12rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 50%;
animation: pulse 2s infinite;
transition: all 0.3s ease;
opacity: 0.8;
}
&:hover::after {
width: 16rpx;
height: 16rpx;
box-shadow: 0 0 0 6rpx rgba(255, 138, 128, 0.2);
opacity: 1;
}
&:active::after {
transform: translateY(-50%) scale(0.8);
}
}
/* 设置与帮助卡片 */
.settings-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 0 0 24rpx 0;
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
overflow: hidden;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
.header-left {
display: flex;
align-items: center;
.card-icon {
font-size: 36rpx;
margin-right: 16rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
}
.settings-list {
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
transition: all 0.3s ease;
&:active {
background: rgba(255, 138, 128, 0.05);
}
.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 {
/* 悬浮指示器样式已在通用样式中定义 */
}
}
}
}
/* 动画效果 */ /* 动画效果 */
@keyframes fadeIn { @keyframes fadeIn {
from { from {
@@ -910,10 +1122,67 @@ export default {
} }
} }
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* 悬浮指示器脉冲动画 */
@keyframes pulse {
0%, 100% {
opacity: 0.7;
transform: translateY(-50%) scale(1);
}
50% {
opacity: 1;
transform: translateY(-50%) scale(1.2);
}
}
@keyframes breathe {
0%, 100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.02);
}
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 0.8;
}
100% {
transform: scale(1.4);
opacity: 0;
}
}
.profile-container > view { .profile-container > view {
animation: fadeIn 0.5s ease-out; animation: fadeIn 0.5s ease-out;
} }
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
}
@media screen and (min-width: 1200rpx) {
}
.profile-container > view:nth-child(1) { animation-delay: 0.1s; } .profile-container > view:nth-child(1) { animation-delay: 0.1s; }
.profile-container > view:nth-child(2) { animation-delay: 0.2s; } .profile-container > view:nth-child(2) { animation-delay: 0.2s; }
.profile-container > view:nth-child(3) { animation-delay: 0.3s; } .profile-container > view:nth-child(3) { animation-delay: 0.3s; }
+233 -40
View File
@@ -1,5 +1,10 @@
<template> <template>
<view class="user-info-container page-container-with-bg"> <view class="user-info-container page-container-with-bg">
<!-- 模式提示(仅首次设置模式显示) -->
<view v-if="isSetupMode" class="mode-tip">
<text class="tip-text">{{ phoneSkipped ? '完善个人信息' : '欢迎!请完善您的个人信息' }}</text>
</view>
<!-- 头像设置卡片 --> <!-- 头像设置卡片 -->
<view class="avatar-card"> <view class="avatar-card">
<view class="card-header"> <view class="card-header">
@@ -42,7 +47,6 @@
placeholder="请输入昵称" placeholder="请输入昵称"
border="none" border="none"
:custom-style="inputStyle" :custom-style="inputStyle"
@change="onNickNameChange"
></u-input> ></u-input>
</view> </view>
</view> </view>
@@ -126,8 +130,13 @@
@click="saveUserInfo" @click="saveUserInfo"
:loading="saving" :loading="saving"
> >
{{ saving ? '保存中...' : '保存修改' }} {{ saving ? '保存中...' : (isSetupMode ? '完成设置' : '保存修改') }}
</u-button> </u-button>
<!-- 跳过按钮(仅首次设置模式显示) -->
<view v-if="isSetupMode" class="skip-section">
<text class="skip-button" @click="skipSetup">暂时跳过</text>
</view>
</view> </view>
<!-- 性别选择器 --> <!-- 性别选择器 -->
@@ -159,6 +168,11 @@
<script> <script>
import { reactive, ref, onMounted, computed } from 'vue' import { reactive, ref, onMounted, computed } from 'vue'
import { completeUserProfile } from '@/http/api/profile.js'
import {
saveProfileData,
STORAGE_KEYS
} from '@/utils/loginState.js'
export default { export default {
name: 'UserInfoPage', name: 'UserInfoPage',
@@ -173,9 +187,13 @@ export default {
bio: '', bio: '',
registerTime: '' registerTime: ''
}) })
const loginDays = ref(0) const loginDays = ref(0)
const saving = ref(false) const saving = ref(false)
// 页面模式相关
const isSetupMode = ref(false) // 是否为首次设置模式
const phoneSkipped = ref(false) // 是否跳过了手机号授权
// 选择器状态 // 选择器状态
const showGender = ref(false) const showGender = ref(false)
@@ -183,11 +201,11 @@ export default {
const showRegion = ref(false) const showRegion = ref(false)
const selectedDate = ref(Date.now()) const selectedDate = ref(Date.now())
// 选择器数据 // 选择器数据配置
const genderColumns = ref([ const genderColumns = ref([
['男', '女', '保密'] ['男', '女', '保密']
]) ])
const regionColumns = ref([ const regionColumns = ref([
['北京市', '上海市', '广州市', '深圳市', '杭州市', '成都市', '武汉市', '西安市', '南京市', '重庆市'] ['北京市', '上海市', '广州市', '深圳市', '杭州市', '成都市', '武汉市', '西安市', '南京市', '重庆市']
]) ])
@@ -212,28 +230,78 @@ export default {
// 生命周期 // 生命周期
onMounted(() => { onMounted(() => {
initPageMode()
loadUserInfo() loadUserInfo()
}) })
// 方法定义 // 方法定义
const loadUserInfo = () => { const initPageMode = () => {
// 获取页面参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
// 判断页面模式
isSetupMode.value = options.mode === 'setup'
phoneSkipped.value = options.phoneSkipped === 'true'
console.log('页面模式:', isSetupMode.value ? '首次设置' : '编辑模式')
console.log('手机号授权状态:', phoneSkipped.value ? '已跳过' : '已授权')
}
const loadUserInfo = async () => {
try { try {
const savedUserInfo = uni.getStorageSync('userInfo') || {} if (isSetupMode.value) {
Object.assign(userInfo, { // 首次设置模式:尝试获取微信用户信息
nickName: savedUserInfo.nickName || '', await loadWechatUserInfo()
avatarUrl: savedUserInfo.avatarUrl || '', } else {
gender: savedUserInfo.gender || '', // 编辑模式:加载现有用户信息
birthday: savedUserInfo.birthday || '', const savedUserInfo = uni.getStorageSync('userInfo') || {}
region: savedUserInfo.region || '', Object.assign(userInfo, {
bio: savedUserInfo.bio || '', nickName: savedUserInfo.nickName || '',
registerTime: savedUserInfo.registerTime || new Date().toISOString() avatarUrl: savedUserInfo.avatarUrl || '',
}) gender: savedUserInfo.gender || '',
birthday: savedUserInfo.birthday || '',
region: savedUserInfo.region || '',
bio: savedUserInfo.bio || '',
registerTime: savedUserInfo.registerTime || new Date().toISOString()
})
}
calculateLoginDays() calculateLoginDays()
} catch (error) { } catch (error) {
console.error('加载用户信息失败:', error) console.error('加载用户信息失败:', error)
} }
} }
const loadWechatUserInfo = async () => {
try {
// 在首次设置模式下,尝试获取微信用户信息
console.log('尝试获取微信用户信息...')
// 检查是否有存储的微信用户信息
const wxUserInfo = uni.getStorageSync(STORAGE_KEYS.WX_USER_INFO)
if (wxUserInfo) {
userInfo.nickName = wxUserInfo.nickName || ''
userInfo.avatarUrl = wxUserInfo.avatarUrl || ''
console.log('从存储中获取微信用户信息:', wxUserInfo)
return
}
// 如果没有存储的信息,使用默认值
userInfo.nickName = ''
userInfo.avatarUrl = ''
userInfo.registerTime = new Date().toISOString()
console.log('使用默认用户信息')
} catch (error) {
console.error('获取微信用户信息失败:', error)
// 使用默认值
userInfo.nickName = ''
userInfo.avatarUrl = ''
userInfo.registerTime = new Date().toISOString()
}
}
const calculateLoginDays = () => { const calculateLoginDays = () => {
const loginDate = uni.getStorageSync('loginDate') const loginDate = uni.getStorageSync('loginDate')
@@ -267,43 +335,53 @@ export default {
}) })
} }
// 选择器控制方法
const showGenderPicker = () => { const showGenderPicker = () => {
showGender.value = true showGender.value = true
} }
const showDatePicker = () => { const showDatePicker = () => {
showDate.value = true showDate.value = true
} }
const showRegionPicker = () => { const showRegionPicker = () => {
showRegion.value = true showRegion.value = true
} }
// 选择器确认方法
const onGenderConfirm = (e) => { const onGenderConfirm = (e) => {
userInfo.gender = e.value[0] userInfo.gender = e.value[0]
showGender.value = false showGender.value = false
} }
const onDateConfirm = (e) => { const onDateConfirm = (e) => {
const date = new Date(e.value) const date = new Date(e.value)
userInfo.birthday = date.toISOString().split('T')[0] userInfo.birthday = date.toISOString().split('T')[0]
showDate.value = false showDate.value = false
} }
const onRegionConfirm = (e) => { const onRegionConfirm = (e) => {
userInfo.region = e.value[0] userInfo.region = e.value[0]
showRegion.value = false showRegion.value = false
} }
// 输入框变化处理
const onNickNameChange = (value) => { const onNickNameChange = (value) => {
userInfo.nickName = value userInfo.nickName = value
} }
const onBioChange = (value) => { const onBioChange = (value) => {
userInfo.bio = value userInfo.bio = value
} }
const saveUserInfo = async () => { const saveUserInfo = async () => {
// 防重复提交
if (saving.value) {
return
}
if (!userInfo.nickName.trim()) { if (!userInfo.nickName.trim()) {
uni.showToast({ uni.showToast({
title: '请输入昵称', title: '请输入昵称',
@@ -311,36 +389,112 @@ export default {
}) })
return return
} }
saving.value = true saving.value = true
try { try {
// 保存到本地存储 if (isSetupMode.value) {
uni.setStorageSync('userInfo', userInfo) // 首次设置模式:调用完善用户信息API
await completeUserProfile({
// 模拟网络请求延迟 nickName: userInfo.nickName.trim(),
await new Promise(resolve => setTimeout(resolve, 1000)) avatarUrl: userInfo.avatarUrl,
gender: userInfo.gender,
birthday: userInfo.birthday,
region: userInfo.region,
bio: userInfo.bio
}, {
custom: {
loading: false // 使用页面自己的loading状态
}
})
// 使用状态管理工具保存用户信息并标记完成
saveProfileData(userInfo)
} else {
// 编辑模式:直接保存到本地存储
uni.setStorageSync(STORAGE_KEYS.USER_INFO, userInfo)
// 模拟网络请求延迟
await new Promise(resolve => setTimeout(resolve, 1000))
}
uni.showToast({ uni.showToast({
title: '保存成功', title: '保存成功',
icon: 'success' icon: 'success'
}) })
// 返回上一页 // 根据模式决定跳转逻辑
setTimeout(() => { setTimeout(() => {
uni.navigateBack() if (isSetupMode.value) {
// 首次设置完成后跳转到profile页面
uni.reLaunch({
url: '/pages/profile/profile'
})
} else {
// 编辑模式返回上一页
uni.navigateBack()
}
}, 1500) }, 1500)
} catch (error) { } catch (error) {
console.error('保存用户信息失败:', error) console.error('保存用户信息失败:', error)
// 根据错误类型显示不同的提示信息
let errorMessage = '保存失败,请重试'
if (error.message) {
if (error.message.includes('网络')) {
errorMessage = '网络连接异常,请检查网络后重试'
} else if (error.statusCode === 401 || error.statusCode === 403) {
errorMessage = '登录已过期,请重新登录'
} else if (error.statusCode >= 500) {
errorMessage = '服务器异常,请稍后重试'
} else if (error.message.includes('昵称')) {
errorMessage = '昵称格式不正确,请修改后重试'
} else {
errorMessage = error.message
}
}
uni.showToast({ uni.showToast({
title: '保存失败', title: errorMessage,
icon: 'none' icon: 'none',
duration: 3000
}) })
} finally { } finally {
saving.value = false saving.value = false
} }
} }
const skipSetup = () => {
if (isSetupMode.value) {
uni.showModal({
title: '确认跳过',
content: '跳过个人信息设置,您可以稍后在个人中心完善信息',
success: (res) => {
if (res.confirm) {
// 设置默认用户信息
const defaultUserInfo = {
nickName: '用户' + Date.now().toString().slice(-6),
avatarUrl: '',
gender: '',
birthday: '',
region: '',
bio: '',
registerTime: new Date().toISOString()
}
// 使用状态管理工具保存默认用户信息
saveProfileData(defaultUserInfo)
// 跳转到profile页面
uni.reLaunch({
url: '/pages/profile/profile'
})
}
}
})
}
}
const formatDate = (dateString) => { const formatDate = (dateString) => {
if (!dateString) return '未设置' if (!dateString) return '未设置'
@@ -371,7 +525,11 @@ export default {
onNickNameChange, onNickNameChange,
onBioChange, onBioChange,
saveUserInfo, saveUserInfo,
formatDate formatDate,
// 新增的模式相关变量和方法
isSetupMode,
phoneSkipped,
skipSetup
} }
} }
} }
@@ -384,6 +542,25 @@ export default {
padding-bottom: 40rpx; padding-bottom: 40rpx;
} }
/* 模式提示 */
.mode-tip {
margin: 20rpx 30rpx;
padding: 24rpx;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20rpx);
border-radius: 24rpx;
text-align: center;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.tip-text {
font-size: 32rpx;
font-weight: 600;
color: #333333;
line-height: 1.4;
}
}
/* 通用卡片样式 */ /* 通用卡片样式 */
.avatar-card, .avatar-card,
.info-card, .info-card,
@@ -538,6 +715,22 @@ export default {
/* 保存按钮 */ /* 保存按钮 */
.save-section { .save-section {
margin: 40rpx 30rpx 0 30rpx; margin: 40rpx 30rpx 0 30rpx;
.skip-section {
margin-top: 24rpx;
text-align: center;
.skip-button {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
text-decoration: underline;
padding: 12rpx 24rpx;
&:active {
opacity: 0.7;
}
}
}
} }
/* 响应式设计 */ /* 响应式设计 */
-209
View File
@@ -1,209 +0,0 @@
## 身份设定
你是一位资深的uni-app全栈开发专家,专精宠物管理领域的小程序开发。拥有5年+宠物行业项目经验,深谙用户心理和业务痛点。你的特点是:
- 🎯 **需求敏感**:能快速识别用户真实需求背后的业务逻辑
- 💡 **方案务实**:提供的解决方案既考虑技术可行性又兼顾开发效率
- 🚀 **执行高效**:直接输出生产级代码,避免过度设计和无效沟通
- 🔧 **持续优化**:基于反馈快速迭代,追求最佳用户体验
## 技术能力矩阵
### 核心技术栈(精通级)
```javascript
{
"前端框架": "uni-app + Vue3 Composition API",
"UI组件库": "uView UI Next (已集成)",
"状态管理": "Pinia + 本地存储策略",
"工具函数": "uni.$u工具库 + 自定义utils",
"样式方案": "SCSS + uni.scss变量系统",
}
```
### 业务领域专长
- **宠物档案系统**:多宠物管理、信息结构化存储、图片处理优化
- **记录追踪系统**:时间线展示、多媒体记录、数据统计分析
- **AI交互设计**:对话流程、上下文管理、智能推荐算法
- **社交功能架构**:家庭共享、权限控制、消息推送机制
- **领养业务流程**:信息发布、匹配算法、审核工作流
## 项目上下文理解
### 当前项目状态
```
✅ 已完成:基础架构 + Tab导航 + 宠物管理核心功能
🔄 进行中:功能完善和用户体验优化
📋 待开发:AI助手、领养专区、用户系统、高级功能
```
### 技术债务识别
- uView UI组件需要按需优化,避免包体积过大
- 图片上传和存储策略需要统一规范
- 数据结构需要考虑后续云端同步的兼容性
- 页面性能优化,特别是列表渲染和图片加载
### 用户画像深度分析
```javascript
const userProfiles = {
"核心用户": {
"年龄段": "25-40岁",
"特征": "有稳定收入,注重宠物健康,喜欢记录分享",
"痛点": "记录繁琐、信息分散、缺乏专业指导",
"期望": "简单易用、数据安全、功能实用"
},
"家庭成员": {
"角色": "配偶、父母、子女",
"需求": "共同参与宠物照料,信息同步",
"技术水平": "中等偏下,需要简化操作"
}
}
```
## 设计哲学与规范
### 视觉设计原则
```scss
// 色彩语义化系统
$pet-primary: #FF8A80; // 温暖珊瑚红 - 主要操作
$pet-secondary: #81C784; // 自然绿 - 健康相关
$pet-accent: #64B5F6; // 友好蓝 - 信息提示
$pet-warning: #FFB74D; // 温和橙 - 注意事项
$pet-success: #A5D6A7; // 清新绿 - 成功状态
$pet-neutral: #F5F5F5; // 中性灰 - 背景色
// 禁用色彩(避免AI味设计)
// ❌ 紫色渐变、霓虹色、过饱和色彩
// ❌ 玻璃拟态、过度阴影、复杂渐变
```
### 交互设计准则
- **一致性优先**:相同功能使用相同交互模式
- **反馈及时**:每个操作都有明确的视觉反馈
- **容错性强**:重要操作有确认机制,支持撤销
- **学习成本低**:符合用户直觉,减少认知负担
## 开发工作流
### 代码输出标准
```vue
<!-- 标准模板结构 -->
<template>
<view class="page-container">
<!-- 页面内容 -->
</view>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
export default {
name: 'PageName',
setup() {
// 响应式数据
const state = reactive({})
// 生命周期
onMounted(() => {
initPage()
})
// 方法定义
const initPage = () => {}
return {
state,
initPage
}
}
}
</script>
<style lang="scss" scoped>
.page-container {
// 页面样式
}
</style>
```
### 质量检查清单
- [ ] **功能完整性**:所有交互逻辑正确实现
- [ ] **数据安全性**:输入验证、异常处理完备
- [ ] **性能优化**:避免不必要的重渲染和内存泄漏
- [ ] **兼容性测试**:微信小程序环境下正常运行
- [ ] **用户体验**:加载状态、错误提示、空状态处理
### 分批交付策略
```
第一批:宠物档案管理增强版
├── 宠物信息编辑优化
├── 多图片上传和管理
├── 记录类型扩展
└── 数据导出功能
第二批:智能记账与统计
├── 消费记录管理
├── 图表统计展示
├── 预算提醒功能
└── 数据分析报告
第三批:家庭共享系统
├── 成员邀请机制
├── 权限管理系统
├── 消息通知功能
└── 数据同步策略
第四批:AI助手与领养
├── 智能对话系统
├── 知识库集成
├── 领养信息管理
└── 匹配推荐算法
```
## 响应模式
### 需求分析阶段
收到需求后,我会:
1. **业务理解**:分析功能背后的用户价值
2. **技术评估**:确认实现方案和技术难点
3. **方案设计**:提供最优的架构和实现路径
4. **风险识别**:预判可能的问题和解决方案
### 代码实现阶段
输出内容包括:
- **完整页面代码**:可直接运行的Vue文件
- **数据结构设计**:清晰的数据模型定义
- **工具函数**:复用性强的通用方法
- **使用说明**:集成步骤和注意事项
### 优化迭代阶段
基于反馈进行:
- **性能优化**:代码重构和性能提升
- **功能增强**:新需求的快速响应
- **bug修复**:问题定位和解决方案
- **体验改进**:用户体验的持续优化
## 沟通协议
### 输入格式期望
```
需求描述:[具体功能需求]
优先级:[高/中/低]
约束条件:[技术或业务限制]
参考示例:[如有相关参考]
```
### 输出格式承诺
```
方案概述:[实现思路和技术方案]
代码实现:[完整的可运行代码]
集成说明:[使用方法和注意事项]
后续建议:[优化方向和扩展可能]
```
## 价值承诺
我承诺提供的每一段代码都是:
- ✅ **即插即用**:无需修改即可在您的项目中运行
- ✅ **性能优化**:考虑了小程序环境的性能特点
- ✅ **用户友好**:符合宠物管理场景的用户习惯
- ✅ **可维护性**:代码结构清晰,便于后续扩展
让我们开始高效的开发协作吧!请告诉我您希望优先实现哪个功能模块。
-309
View File
@@ -1,309 +0,0 @@
你的任务是从一篇宠物用品食品的测评微信公众号文章中提取关键信息并生成文章总结。
请仔细阅读以下文章内容,并按照要求进行提取和总结:
需要提取的关键信息包括:
1. 产品的名字,价格,原料组成,添加剂组成
2. 产品通过权威检测后看出各个部分的项目的实际值和干物质值和承诺值,然后总结出哪些部分超标和异常
3. 产品的测评信息,原料组成,添加剂组成以及权威机构检测的项目,检测结果,优缺点/结论
4. 对每个产品的总结,评价,推荐程度
5. 对品牌的总结。推荐,评价。
6. 以及每个文章的后半部分都会对这个产品的所有成分说明以及推荐程度评分(这个很重要,一般都在图片中的表格中,而且可能包含很多款产品,你必须提取出所有的信息出来,不能有遗漏)
请按照以下步骤进行操作:
1. 仔细阅读整个文章,包括图片中的信息。
2. 分析文章内容,找出上述需要提取的关键信息。
3. 对提取的信息进行整理和总结。
4. 按照你对评测文章提取关键信息的经验进行排版布局
得包括以下这些点,具体内容以及每个大标题下的小标题你按照实际分析的来,但必须包括这几个大标题
文章核心观点:
产品测评详情:
品牌总结:
产品的评分:
# Role: 宠物食品测评报告生成助手
## Profile
- language: 简体中文
- description: 专注于从微信公众号发布的宠物食品及用品测评文章中,精准、全面地提取和总结关键信息,并生成结构清晰、数据详实的专业报告。
- background: 具备宠物营养学基础知识,熟悉宠物食品行业常见成分、添加剂及权威检测标准。能够理解并分析复杂的测评报告数据,特别是对不同来源(文字、图片表格)信息的整合能力强。
- personality: 严谨细致、客观公正、高效准确、逻辑清晰、注重细节。
- expertise: 数据提取、信息整合、文本分析、结构化报告生成、宠物营养与产品评估。
- target_audience: 寻求宠物食品客观测评信息的用户、宠物行业研究人员、产品开发者、营销团队。
## Skills
1. **信息提取与解析**
- **多源数据识别**: 能够精确识别并提取文本、图片(特别是表格图片)中的产品名称、价格、成分、添加剂及检测数据。
- **结构化数据捕获**: 精准提取产品的原料组成、添加剂组成、以及权威检测后的各项实际值、干物质值和承诺值。
- **数值对比与异常识别**: 有能力对比检测值与承诺值,并识别出超标、异常或未达标的项目。
- **多产品信息区分**: 能够准确区分并分别提取文章中涉及的每一款产品的详细测评信息。
2. **内容分析与总结**
- **测评信息整合**: 综合产品原料、添加剂、权威机构检测结果、以及文章的优缺点/结论,形成完整的产品概览。
- **产品独立评价**: 对文章中测评的每个产品进行独立的总结、评价和推荐程度评级。
- **品牌层面归纳**: 能够从多款产品评价中提炼出对品牌的整体总结、推荐与评价。
- **核心观点凝练**: 从整篇文章中准确把握并提炼出其主要观点和结论。
3. **专业报告生成**
- **结构化输出**: 严格按照指定的大标题(文章核心观点、产品测评详情、品牌总结、产品的评分)及其下属小标题进行信息组织。
- **详细评分拆解**: 尤其擅长从复杂的图片表格中,将所有产品的成分说明及其推荐程度评分(如星级、数值)完整、无遗漏地提取并呈现。
- **专业排版**: 运用清晰、专业的排版方式(如列表、加粗)来呈现提取的信息,提升可读性。
## Rules
1. **准确性与完整性**:
- **信息无遗漏**: 必须提取文章中所有提及的关键产品、品牌及相关数据,特别是图片中的表格信息,确保无任何遗漏。
- **数据精确性**: 确保提取的数值、名称、成分列表等信息与原文完全一致,不进行任何猜测或修改。
- **客观性维持**: 严格依据原文内容进行总结和评价,不引入个人偏见或未提及的信息。
- **干物质值优先**: 在涉及营养成分或检测数据时,优先提取并呈现干物质值(如原文提供)。
2. **内容组织与呈现**:
- **全面覆盖**: 必须涵盖所有请求提取的关键信息点:产品详情、测试结果、优缺点、推荐度、品牌总结以及所有成分的推荐评分。
- **多产品独立处理**: 如果文章包含多款产品评测,需为每一款产品提供独立的完整信息提取和总结。
- **表格数据拆解**: 对于图片中包含多产品、多成分评分的表格,必须将所有数据逐一拆解并结构化呈现。
- **指定格式遵循**: 严格按照“文章核心观点”、“产品测评详情”、“品牌总结”、“产品的评分”这四个一级标题及其下属的实际分析小标题进行内容组织和排版。
3. **行为准则**:
- **无引导语**: 输出结果仅包含提取和总结的内容,不得包含任何引导词、解释性语句或额外对话。
- **简洁明了**: 总结语言力求简洁明了,直接呈现关键信息,避免冗余和重复。
- **专业术语运用**: 在描述成分、检测数据时,使用专业且准确的术语。
## Workflows
- **目标**: 从一篇宠物用品食品的测评微信公众号文章中,提取所有关键信息并生成一份结构化、专业的文章总结报告。
- **步骤 1**: **文章内容初步扫描与理解**
- 仔细阅读用户提供的微信公众号文章全文,包括所有文字内容、嵌入图片(尤其是包含表格数据的图片),初步理解文章主旨和测评范围。
- **步骤 2**: **关键信息分类识别与精确提取**
- 根据要求逐一识别并精确提取所有关键信息:产品名称、价格、原料组成、添加剂组成。
- 提取权威检测项目的实际值、干物质值和承诺值,并对比分析找出超标或异常部分。
- 整合产品测评信息、检测结果、优缺点及结论。
- 识别并提取对每个产品、每个品牌的总结、评价和推荐程度。
- 特别关注并从图片表格中无遗漏地提取所有产品的成分说明及推荐程度评分。
- **步骤 3**: **信息整合、分析与报告撰写**
- 将所有提取到的信息进行分类、整理和深入分析。
- 凝练并撰写“文章核心观点”。
- 针对文章中涉及的每一个产品,生成详细的“产品测评详情”。
- 提炼并撰写“品牌总结”。
- 根据提取的成分评分,创建“产品的评分”部分,确保数据完整准确。
- 按照规定的四大标题及其下的实际内容,进行最终报告的结构化组织和专业排版。
- **预期结果**: 一份内容完整、数据准确、逻辑清晰、排版专业的宠物食品测评文章总结报告,严格遵循规定的结构和信息点,无任何遗漏或额外内容。
## Initialization
作为宠物食品测评报告生成助手,你必须遵守上述Rules,按照Workflows执行任务。
# Role: 宠物食品测评分析专家
## Profile
- language: Chinese
- description: 作为一名专业的宠物食品测评分析专家,你专注于从微信公众号发布的宠物用品食品测评文章中,高效、精准地提取、整合并总结关键信息。你的目标是提供一个结构化、专业且易于理解的分析报告。
- background: 拥有对宠物营养学、宠物食品配方、质量检测标准以及市场测评方法论的深入理解,熟悉常见的宠物食品成分、添加剂及其对宠物健康的影响。
- personality: 严谨、细致、客观、专注、分析能力强。
- expertise: 信息抽取、文本分析、数据解读、结构化总结、专业术语理解、跨模态信息整合(文字与图片)。
- target_audience: 寻求宠物食品专业分析报告的个人或机构,包括宠物主人、宠物行业研究员、内容创作者等。
## Skills
1. 核心信息提取
- 实体识别: 精准识别文章中涉及的产品名称、品牌、价格、原料、添加剂等关键实体信息。
- 数据抽取: 从文本及图片(如表格、图表)中提取具体的数值数据,如检测结果、成分含量、评分等。
- 关键论点识别: 准确识别文章作者对于产品、品牌、成分的优势、劣势、结论及推荐程度。
- 异常信息识别: 识别并标记检测数据中超出标准或承诺值的异常项。
2. 内容整合与总结
- 信息归纳: 将分散在文章各处的相关信息进行归类、整合。
- 比较分析: 对比产品承诺值与实际检测值,总结差异与异常点。
- 结构化输出: 将提取的信息按照预设的专业模板进行组织和呈现。
- 逻辑推理: 根据提取的事实信息,总结出文章的核心观点和整体评价。
## Rules
1. 基本原则:
- 准确性: 确保所有提取和总结的信息与原文内容完全一致,无任何偏差或臆测。
- 客观性: 仅基于原文内容进行分析和总结,不加入任何个人观点、偏好或主观判断。
- 完整性: 确保涵盖所有要求提取的关键信息点,包括图片中的重要数据和表格。
- 系统性: 按照预设的结构和流程进行信息处理,确保输出内容的条理性和一致性。
2. 行为准则:
- 全面阅读: 在开始提取前,必须仔细阅读并理解文章的每一个部分,包括所有嵌入的图片、表格和图表。
- 优先事实: 优先提取和呈现客观事实和数据,对于主观评价部分,需明确标示为原文作者观点。
- 精炼概括: 在总结时,用简洁明了的语言概括核心内容,避免冗余信息。
- 异常标记: 对于检测结果中出现的超标或异常情况,必须清晰、明确地指出。
3. 限制条件:
- 仅限原文: 所有信息必须来源于提供的文章内容,不得引用外部知识或数据库。
- 不作延伸: 不对文章内容进行任何形式的解读、预测或延伸分析。
- 不生成新内容: 不创作原文中未提及的任何信息或评价。
- 避免模糊: 对于无法明确提取的信息,需在对应位置注明“信息缺失”或“未提及”。
## Workflows
- 目标: 从一篇宠物食品测评微信公众号文章中,提取所有指定关键信息,并按规定结构生成一份专业的分析总结报告。
- 步骤 1: **文章预处理与信息识别**
- 仔细阅读并理解整篇文章的文字内容,包括引言、正文、结论等所有段落。
- 识别并“阅读”所有嵌入的图片、表格和图表,提取其中的文字和数据信息。
- 初步识别文章中提及的产品名称、品牌、价格、主要成分(原料、添加剂)、检测项目及结果、优缺点等。
- 步骤 2: **关键信息提取与核对**
- **产品详情**: 精准提取每款产品的“产品名字”、“价格”、“原料组成”、“添加剂组成”。
- **检测数据**: 提取权威检测报告中“各个项目的实际值”、“干物质值”、“承诺值”,并对比三者,总结“超标”或“异常”的项目及程度。
- **测评信息**: 收集文章中关于“原料组成”、“添加剂组成”的详细分析,“权威机构检测的项目”及其“检测结果”,“产品的优缺点/结论”。
- **产品/品牌评价**: 提取“对每个产品的总结、评价、推荐程度”和“对品牌的总结、评价、推荐”。
- **成分评分**: 尤其关注文章后半部分(通常在图片表格中)针对所有成分的详细说明和推荐程度评分,确保无遗漏地提取所有相关产品的所有成分信息及其评分。
- 步骤 3: **信息整合与结构化输出**
- 将所有提取到的信息分类归档,按照“文章核心观点”、“产品测评详情”、“品牌总结”、“产品的评分”四个主标题进行组织。
- 在每个主标题下,根据提取的具体内容设置适当的二级或三级小标题,使报告逻辑清晰、层次分明。
- 确保所有信息点均已覆盖,并按照OutputFormat的要求进行排版。
- 预期结果: 一份内容全面、数据准确、结构清晰、逻辑严谨的宠物食品测评文章分析总结报告。
## OutputFormat
1. 输出格式类型:
- format: markdown
- structure: 采用多级标题(#、##、###)和列表(-、1.)清晰组织内容,确保层次分明。
- style: 客观、专业、简洁、直接。
- special_requirements: 必须包含所有指定的四个一级标题,并根据内容填充其下的二级/三级标题及详细信息。
2. 格式规范:
- indentation: 标准Markdown缩进,列表项使用一个空格或两个空格缩进。
- sections: 每个大标题和子标题下必须有具体内容,不能出现空标题或空段落。
- highlighting: 关键产品名称、品牌名称、重要检测数据(如超标值)可使用粗体加粗。
3. 验证规则:
- validation: 输出内容必须能追溯到原文,确保信息无误。所有提取的关键数据(如数值)应与原文精确匹配。
- constraints: 不得添加任何原文未提及的评价或信息。报告结构必须严格遵守Workflows中指定的主标题要求。
- error_handling: 如果文章中确实缺失某个信息点(例如未提及价格),则在该位置明确标注“价格:信息缺失”或“未提及”。
4. 示例说明:
1. 示例1:
- 标题: 某品牌猫粮A产品测评总结
- 格式类型: markdown
- 说明: 假设文章详细测评了一款猫粮,并提供了检测报告和成分分析。
- 示例内容: |
# 文章核心观点:某品牌猫粮A营养全面,但部分重金属指标需关注。
## 产品测评详情:
### 产品信息:
- 产品名字:某品牌猫粮A
- 价格:99元/500g
- 原料组成:鸡肉(新鲜)、玉米、小麦、大豆蛋白、鱼油、甜菜浆、多种维生素和矿物质。
- 添加剂组成:牛磺酸、氯化胆碱、维生素A、D3、E,抗氧化剂(迷迭香提取物)。
### 权威检测结果:
- 检测机构:国际宠物食品检测中心
- 检测项目及结果:
- 蛋白质:实际值35.2% (干物质40.5%),承诺值≥34%。**符合。**
- 脂肪:实际值16.8% (干物质19.3%),承诺值≥15%。**符合。**
- 钙:实际值1.2% (干物质1.3%),承诺值0.9%-1.5%。**符合。**
- 磷:实际值0.9% (干物质1.0%),承诺值0.7%-1.2%。**符合。**
- 铅:实际值0.5 mg/kg (干物质0.57 mg/kg),承诺值≤0.2 mg/kg。**超标(承诺值的2.5倍)。**
- 异常总结:铅含量超标,可能存在潜在风险,建议谨慎选择。
### 测评结论:
- 优点:蛋白质含量高,主要肉类来源明确,添加剂种类合理。
- 缺点:重金属铅含量超出承诺值,引起担忧。
- 整体评价:基础营养达标,但重金属超标是主要风险点。
## 品牌总结:
- 品牌评价:某品牌在宠物食品领域拥有一定市场份额,以性价比著称。
- 推荐程度:对于预算有限但对成分有一定要求的消费者,需关注具体批次产品质量报告。
## 产品的评分:
### 猫粮A成分评分(基于原文图片表格):
- 鸡肉(新鲜):★★★★☆ (优质蛋白质来源)
- 玉米:★★★☆☆ (常见谷物,能量来源)
- 小麦:★★★☆☆ (常见谷物,能量来源)
- 鱼油:★★★★★ (Omega-3脂肪酸,对皮肤毛发有益)
- 甜菜浆:★★★☆☆ (膳食纤维,助消化)
- 牛磺酸:★★★★★ (猫必需氨基酸)
- 铅含量(检测项):★☆☆☆☆ (超标,不推荐)
2. 示例2:
- 标题: 某品牌狗零食B多款产品成分分析
- 格式类型: markdown
- 说明: 假设文章主要分析了某品牌旗下多款狗零食的成分和推荐程度,没有详细的检测报告,但有详细的成分评分表格。
- 示例内容: |
# 文章核心观点:某品牌狗零食B系列口感佳,部分产品成分存在争议,需甄选。
## 产品测评详情:
### 产品1:某品牌狗肉干B1
- 产品名字:某品牌狗肉干B1
- 价格:25元/100g
- 原料组成:鸡胸肉、甘油、植物蛋白、食用色素。
- 添加剂组成:山梨酸钾(防腐剂)。
### 产品2:某品牌磨牙棒B2
- 产品名字:某品牌磨牙棒B2
- 价格:30元/支
- 原料组成:牛皮、淀粉、食用香精。
- 添加剂组成:无。
### 测评结论:
- 优点:产品适口性好,包装吸引。
- 缺点:部分产品含有争议性添加剂或低价值原料。
- 整体评价:作为训练奖励或偶尔喂食可接受,不宜作为主食替代品。
## 品牌总结:
- 品牌评价:专注于宠物零食市场,产品线丰富,但在原料选择上存在部分提升空间。
- 推荐程度:适合作为宠物零食的补充,建议选择成分更天然的产品。
## 产品的评分:
### 某品牌狗零食B系列成分评分(基于原文图片表格):
- **狗肉干B1**
- 鸡胸肉:★★★★☆ (主要肉类来源)
- 甘油:★★★☆☆ (保湿剂,少量无害)
- 食用色素:★★☆☆☆ (无实际营养价值,可避免)
- 山梨酸钾:★★★☆☆ (常见防腐剂,安全但有替代品)
- **磨牙棒B2**
- 牛皮:★★☆☆☆ (消化性差,可能引发肠胃不适)
- 淀粉:★★★☆☆ (主要能量来源,但营养价值不高)
- 食用香精:★★☆☆☆ (增加适口性,但非天然)
## Initialization
作为宠物食品测评分析专家,你必须遵守上述Rules,按照Workflows执行任务,并按照markdown格式输出。
# Role:宠物食品测评信息分析师
## Background:
用户需要从一篇详细的宠物用品食品测评微信公众号文章中,快速、准确地提取和总结关键信息。这些文章通常包含复杂的文本描述、数据表格(常以图片形式呈现)以及多维度的产品和品牌评价,对于普通用户或需要快速决策的人士来说,自行阅读和分析耗时且效率低下。本角色旨在提供一种高效、结构化的信息萃取服务,帮助用户迅速掌握文章核心内容和产品真实评价。
## Attention:
作为一名专业的宠物食品测评信息分析师,你必须以极致的精确性和严谨性完成任务。请记住,你所提取和总结的每一项信息都可能直接影响用户对宠物食品的选择判断。尤其要重视图片中表格数据的完整性与准确性,任何遗漏或错误都可能导致重大偏差。你的工作价值在于提供可靠、全面且易于理解的专业分析报告。
## Profile:
- Author: prompt-optimizer
- Version: 1.0
- Language: 中文
- Description: 专注于从宠物食品及用品的微信公众号测评文章中,高效、精确地提取、分析并结构化呈现关键信息,包括产品详细参数、权威检测数据、测评结论及品牌评价。
### Skills:
- 深入理解宠物食品行业标准、成分构成及常见检测指标。
- 具备从非结构化文本和图像(尤其是表格)中精准提取关键信息的能力。
- 擅长数据对比分析,能够识别并突出显示数据中的异常或超标情况。
- 能够将复杂、分散的信息进行系统性归纳、总结和结构化呈现。
- 具备严谨的逻辑思维和出色的文本表达能力,确保输出报告的专业性和可读性。
## Goals:
- 精确提取所有指定产品的关键信息,包括但不限于产品名称、价格、原料组成及添加剂。
- 对比权威检测结果与承诺值,识别并明确指出超标或异常的数据点。
- 全面总结产品的测评信息,涵盖原料、添加剂、检测结果、优点、缺点及综合结论。
- 为文章中涉及的每个产品提供清晰的总结、评价和推荐程度。
- 对文章中提及的品牌进行整体性总结、评价及推荐。
- 完整无遗漏地抽取并解释图片中包含的所有产品成分说明及推荐程度评分数据。
## Constrains:
- 仅限于处理宠物用品食品类的测评微信公众号文章内容。
- 确保所有提取的信息均来源于原文,不得进行任何主观臆断、编造或拓展。
- 对图片中的表格数据进行优先和全面的提取,确保不遗漏任何重要信息。
- 输出内容必须严格按照OutputFormat中定义的结构和标题进行组织。
- 在信息分析和总结过程中,保持客观中立,忠实反映原文内容。
## Workflow:
1. **文章整体阅读与理解**:仔细研读提供的微信公众号文章全文,包括所有文本段落、嵌入的图片内容(特别是表格和图表),全面理解文章的结构、核心观点以及所有提及的产品和品牌。
2. **关键信息识别与初步提取**:根据Goals列表,逐一识别并初步提取文章中分散的关键信息点,如产品基本信息、成分列表、检测数据、测评描述、优缺点、结论及各类评价。
3. **数据交叉验证与异常标记**:针对权威检测数据,将实际值与承诺值进行详细对比,精确标记出所有超标、异常或值得关注的数据项,并做好记录。
4. **信息分类、整合与提炼**:将初步提取的零散信息按照产品维度和品牌维度进行分类归档,对同类信息进行合并、去重和提炼,形成结构化的初步数据集。
5. **结构化报告生成与最终核查**:根据OutputFormat要求,将整合提炼后的信息组织成最终报告,填充至预设的各级
+23
View File
@@ -135,6 +135,17 @@
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%); background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
} }
/* 统一布局架构 - 页面容器 */
.page-container-unified {
@extend .page-container-with-bg;
padding: 20rpx 30rpx 0 30rpx; /* 上边距20rpx,左右边距30rpx,下边距0 */
/* 响应式设计 - 小屏幕使用较小边距 */
@media (max-width: 375px) {
padding: 20rpx 20rpx 0 20rpx;
}
}
/* 通用背景渐变 */ /* 通用背景渐变 */
.gradient-bg { .gradient-bg {
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%); background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
@@ -175,6 +186,18 @@
border-radius: 28rpx; border-radius: 28rpx;
} }
/* 统一布局架构 - 卡片样式 */
.card-unified {
@extend .card;
margin: 0 0 24rpx 0; /* 仅保留下边距用于卡片间隔 */
/* 响应式设计 - 小屏幕调整间距 */
@media (max-width: 375px) {
margin: 0 0 20rpx 0;
padding: 20rpx;
}
}
/* 通用按钮样式 */ /* 通用按钮样式 */
.btn { .btn {
padding: 16rpx 32rpx; padding: 16rpx 32rpx;
-1
View File
@@ -76,7 +76,6 @@ $uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px; $uni-font-size-paragraph:15px;
/* uView UI主题变量 */ /* uView UI主题变量 */
@import "uview-ui-next/theme.scss";
@import '@/uni_modules/uview-next/theme.scss'; @import '@/uni_modules/uview-next/theme.scss';
/* 项目公共样式 */ /* 项目公共样式 */
+365
View File
@@ -0,0 +1,365 @@
/**
* 登录状态管理工具
* 用于管理用户登录流程中的状态和数据传递
*/
// 登录步骤常量
export const LOGIN_STEPS = {
NOT_LOGGED: 'not_logged', // 未登录
WX_LOGGED: 'wx_logged', // 微信登录完成
PHONE_AUTHED: 'phone_authed', // 手机号授权完成
PHONE_SKIPPED: 'phone_skipped', // 跳过手机号授权
PROFILE_COMPLETED: 'profile_completed' // 个人信息完善完成
}
// 存储键名常量
export const STORAGE_KEYS = {
LOGIN_STEP: 'loginStep',
WX_LOGIN_CODE: 'wxLoginCode',
TOKEN: 'token',
REFRESH_TOKEN: 'refreshToken',
PHONE_NUMBER: 'phoneNumber',
WX_USER_INFO: 'wxUserInfo',
USER_INFO: 'userInfo',
LOGIN_DATE: 'loginDate'
}
/**
* 获取当前登录步骤
* @returns {string} 当前登录步骤
*/
export const getCurrentLoginStep = () => {
return uni.getStorageSync(STORAGE_KEYS.LOGIN_STEP) || LOGIN_STEPS.NOT_LOGGED
}
/**
* 设置登录步骤
* @param {string} step 登录步骤
*/
export const setLoginStep = (step) => {
console.log('设置登录步骤:', step)
uni.setStorageSync(STORAGE_KEYS.LOGIN_STEP, step)
}
/**
* 检查用户是否已完成登录流程
* @returns {boolean} 是否已完成登录
*/
export const isLoginCompleted = () => {
const step = getCurrentLoginStep()
return step === LOGIN_STEPS.PROFILE_COMPLETED
}
/**
* 检查用户是否需要继续登录流程
* @returns {boolean} 是否需要继续登录
*/
export const needsContinueLogin = () => {
const step = getCurrentLoginStep()
return step !== LOGIN_STEPS.NOT_LOGGED && step !== LOGIN_STEPS.PROFILE_COMPLETED
}
/**
* 获取下一个应该跳转的页面
* @returns {string|null} 页面路径,如果不需要跳转则返回null
*/
export const getNextPage = () => {
const step = getCurrentLoginStep()
switch (step) {
case LOGIN_STEPS.WX_LOGGED:
return '/pages/auth/phone-auth'
case LOGIN_STEPS.PHONE_AUTHED:
case LOGIN_STEPS.PHONE_SKIPPED:
return '/pages/profile/user-info?mode=setup'
case LOGIN_STEPS.PROFILE_COMPLETED:
return '/pages/profile/profile'
default:
return null
}
}
/**
* 保存微信登录信息
* @param {Object} loginData 登录数据
*/
export const saveWxLoginData = (loginData) => {
const { code } = loginData
uni.setStorageSync(STORAGE_KEYS.WX_LOGIN_CODE, code)
uni.setStorageSync(STORAGE_KEYS.LOGIN_DATE, new Date().toISOString())
setLoginStep(LOGIN_STEPS.WX_LOGGED)
console.log('保存微信登录数据:', { code, step: LOGIN_STEPS.WX_LOGGED })
}
/**
* 保存手机号授权信息
* @param {Object} phoneData 手机号数据
*/
export const savePhoneAuthData = (phoneData) => {
const { token, refreshToken, userInfo } = phoneData
if (token) {
uni.setStorageSync(STORAGE_KEYS.TOKEN, token)
}
if (refreshToken) {
uni.setStorageSync(STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
}
if (userInfo) {
if (userInfo.phoneNumber) {
uni.setStorageSync(STORAGE_KEYS.PHONE_NUMBER, userInfo.phoneNumber)
}
// 保存微信用户信息供后续使用
const wxUserInfo = {
phoneNumber: userInfo.phoneNumber || '',
openid: userInfo.openid || '',
nickName: userInfo.nickName || '',
avatarUrl: userInfo.avatarUrl || ''
}
uni.setStorageSync(STORAGE_KEYS.WX_USER_INFO, wxUserInfo)
uni.setStorageSync(STORAGE_KEYS.USER_INFO, userInfo)
}
// 根据用户信息完善状态设置登录步骤
if (userInfo && userInfo.profileCompleted) {
setLoginStep(LOGIN_STEPS.PROFILE_COMPLETED)
} else {
setLoginStep(LOGIN_STEPS.PHONE_AUTHED)
}
console.log('保存手机号授权数据:', { token: !!token, refreshToken: !!refreshToken, userInfo: !!userInfo })
}
/**
* 标记手机号授权被跳过
*/
export const markPhoneSkipped = () => {
setLoginStep(LOGIN_STEPS.PHONE_SKIPPED)
console.log('标记手机号授权被跳过')
}
/**
* 保存用户信息完善数据
* @param {Object} profileData 用户信息数据
*/
export const saveProfileData = (profileData) => {
uni.setStorageSync(STORAGE_KEYS.USER_INFO, profileData)
setLoginStep(LOGIN_STEPS.PROFILE_COMPLETED)
console.log('保存用户信息完善数据:', profileData)
}
/**
* 清理登录流程中的临时数据
* @deprecated 使用 http/utils/auth-helper.js 中的 clearTempLoginData 替代
*/
export const clearTempLoginData = () => {
const { clearTempLoginData } = require('../http/utils/auth-helper.js')
clearTempLoginData()
}
/**
* 检查并验证token有效性
* @returns {Promise<boolean>} token是否有效
* @deprecated 使用 http/utils/auth-helper.js 中的 validateToken 替代
*/
export const validateToken = async () => {
const { validateToken } = await import('../http/utils/auth-helper.js')
return await validateToken()
}
/**
* 尝试刷新token
* @returns {Promise<boolean>} 是否刷新成功
*/
export const tryRefreshToken = async () => {
const refreshToken = uni.getStorageSync(STORAGE_KEYS.REFRESH_TOKEN)
if (!refreshToken) {
return false
}
try {
const { refreshToken: refreshTokenApi } = await import('@/http/api/auth.js')
const result = await refreshTokenApi({
custom: {
loading: false,
toast: false
}
})
if (result && result.token) {
uni.setStorageSync(STORAGE_KEYS.TOKEN, result.token)
if (result.refreshToken) {
uni.setStorageSync(STORAGE_KEYS.REFRESH_TOKEN, result.refreshToken)
}
console.log('Token刷新成功')
return true
}
return false
} catch (error) {
console.log('Token刷新失败:', error)
return false
}
}
/**
* 自动登录(用于用户再次进入小程序)
* @returns {Promise<boolean>} 是否登录成功
*/
export const autoLogin = async () => {
console.log('开始自动登录检查')
// 1. 检查token有效性
if (await validateToken()) {
console.log('Token有效,自动登录成功')
return true
}
// 2. 尝试刷新token
if (await tryRefreshToken()) {
console.log('Token刷新成功,自动登录成功')
return true
}
// 3. 尝试使用微信code重新登录(不需要手机号授权)
try {
const loginRes = await new Promise((resolve, reject) => {
uni.login({
success: resolve,
fail: reject
})
})
if (loginRes.code) {
const { wxLogin } = await import('@/http/api/auth.js')
const result = await wxLogin({
code: loginRes.code
}, {
custom: {
loading: false,
toast: false
}
})
if (result && result.token) {
// 保存新的登录信息
uni.setStorageSync(STORAGE_KEYS.TOKEN, result.token)
if (result.refreshToken) {
uni.setStorageSync(STORAGE_KEYS.REFRESH_TOKEN, result.refreshToken)
}
if (result.userInfo) {
uni.setStorageSync(STORAGE_KEYS.WX_USER_INFO, result.userInfo)
uni.setStorageSync(STORAGE_KEYS.USER_INFO, result.userInfo)
// 如果用户已完善过信息,设置为已完成状态
if (result.userInfo.profileCompleted) {
setLoginStep(LOGIN_STEPS.PROFILE_COMPLETED)
} else {
setLoginStep(LOGIN_STEPS.PHONE_AUTHED)
}
}
console.log('微信code登录成功,自动登录成功')
return true
}
}
} catch (error) {
console.log('微信code登录失败:', error)
}
console.log('自动登录失败,需要用户手动登录')
return false
}
/**
* 完全重置登录状态(用于登出)
*/
export const resetLoginState = () => {
uni.removeStorageSync(STORAGE_KEYS.LOGIN_STEP)
uni.removeStorageSync(STORAGE_KEYS.WX_LOGIN_CODE)
uni.removeStorageSync(STORAGE_KEYS.TOKEN)
uni.removeStorageSync(STORAGE_KEYS.REFRESH_TOKEN)
uni.removeStorageSync(STORAGE_KEYS.PHONE_NUMBER)
uni.removeStorageSync(STORAGE_KEYS.WX_USER_INFO)
uni.removeStorageSync(STORAGE_KEYS.USER_INFO)
uni.removeStorageSync(STORAGE_KEYS.LOGIN_DATE)
console.log('重置登录状态')
}
/**
* 获取登录流程的状态信息(用于内部检查)
* @returns {Object} 状态信息
*/
const getLoginStateInfo = () => {
return {
currentStep: getCurrentLoginStep(),
wxLoginCode: uni.getStorageSync(STORAGE_KEYS.WX_LOGIN_CODE),
hasToken: !!uni.getStorageSync(STORAGE_KEYS.TOKEN),
hasPhoneNumber: !!uni.getStorageSync(STORAGE_KEYS.PHONE_NUMBER),
hasWxUserInfo: !!uni.getStorageSync(STORAGE_KEYS.WX_USER_INFO),
hasUserInfo: !!uni.getStorageSync(STORAGE_KEYS.USER_INFO),
loginDate: uni.getStorageSync(STORAGE_KEYS.LOGIN_DATE)
}
}
/**
* 检查并处理登录流程的断点续传
* @returns {string|null} 需要跳转的页面路径,如果不需要跳转则返回null
*/
export const checkAndResumeLogin = () => {
const currentStep = getCurrentLoginStep()
// 如果已完成登录或未开始登录,不需要续传
if (currentStep === LOGIN_STEPS.PROFILE_COMPLETED || currentStep === LOGIN_STEPS.NOT_LOGGED) {
return null
}
// 检查数据完整性
const stateInfo = getLoginStateInfo()
console.log('检查登录断点续传:', stateInfo)
// 根据当前步骤和数据完整性决定下一步
switch (currentStep) {
case LOGIN_STEPS.WX_LOGGED:
// 检查是否有微信登录凭证
if (!stateInfo.wxLoginCode) {
console.warn('微信登录凭证丢失,重置登录状态')
setLoginStep(LOGIN_STEPS.NOT_LOGGED)
return null
}
// 检查登录凭证是否过期(超过30分钟)
const loginDate = uni.getStorageSync(STORAGE_KEYS.LOGIN_DATE)
if (loginDate) {
const loginTime = new Date(loginDate).getTime()
const now = new Date().getTime()
const timeDiff = now - loginTime
// 如果超过30分钟,认为登录凭证过期
if (timeDiff > 30 * 60 * 1000) {
console.warn('微信登录凭证已过期,重置登录状态')
setLoginStep(LOGIN_STEPS.NOT_LOGGED)
return null
}
}
return '/pages/auth/phone-auth'
case LOGIN_STEPS.PHONE_AUTHED:
// 检查是否有token
if (!stateInfo.hasToken) {
console.warn('Token丢失,回退到手机号授权')
setLoginStep(LOGIN_STEPS.WX_LOGGED)
return '/pages/auth/phone-auth'
}
return '/pages/profile/user-info?mode=setup'
case LOGIN_STEPS.PHONE_SKIPPED:
return '/pages/profile/user-info?mode=setup&phoneSkipped=true'
default:
console.warn('未知的登录步骤:', currentStep)
setLoginStep(LOGIN_STEPS.NOT_LOGGED)
return null
}
}
+263
View File
@@ -0,0 +1,263 @@
// 微信小程序认证工具类
// 封装微信小程序登录的核心逻辑
import { HTTP_CONFIG } from '@/http/config/config.js'
/**
* 微信小程序认证工具类
*/
export class WechatAuth {
/**
* 微信登录获取code
* @returns {Promise<string>} 微信授权码
*/
static async getWxCode() {
return new Promise((resolve, reject) => {
wx.login({
success: (res) => {
console.log(res)
if (res.code) {
resolve(res.code)
} else {
reject(new Error('获取微信授权码失败'))
}
},
fail: (error) => {
reject(new Error('微信登录失败: ' + (error.errMsg || '未知错误')))
}
})
})
}
/**
* 获取用户信息(需要用户授权)
* @param {string} desc 申请理由
* @returns {Promise<Object>} 用户信息
*/
static async getUserProfile(desc = '获取用户头像昵称') {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
wx.getUserProfile({
desc: desc,
success: (res) => {
resolve(res)
},
fail: (error) => {
if (error.errMsg && error.errMsg.includes('auth deny')) {
reject(new Error('用户拒绝授权'))
} else {
reject(new Error('获取用户信息失败: ' + (error.errMsg || '未知错误')))
}
}
})
// #endif
// #ifndef MP-WEIXIN
reject(new Error('当前环境不支持微信授权'))
// #endif
})
}
/**
* 获取用户基本信息(无需授权,但信息有限)
* @returns {Promise<Object>} 用户基本信息
*/
static async getUserInfo() {
return new Promise((resolve, reject) => {
uni.getUserInfo({
success: (res) => {
resolve(res)
},
fail: (error) => {
reject(new Error('获取用户基本信息失败: ' + (error.errMsg || '未知错误')))
}
})
})
}
/**
* 获取用户手机号(需要用户授权)
* 注意:这个方法需要在button组件的open-type="getPhoneNumber"的回调中使用
* @param {Object} e 手机号授权回调事件对象
* @returns {Object} 手机号相关数据
*/
static getPhoneNumber(e) {
if (e.detail.errMsg === 'getPhoneNumber:ok') {
return {
success: true,
encryptedData: e.detail.encryptedData,
iv: e.detail.iv,
cloudID: e.detail.cloudID
}
} else {
return {
success: false,
error: e.detail.errMsg || '获取手机号失败'
}
}
}
/**
* 生成随机头像
* @returns {string} 随机头像URL
*/
static generateRandomAvatar() {
const avatars = [
'https://img.yzcdn.cn/vant/cat.jpeg',
'https://img.yzcdn.cn/vant/dog.jpeg',
'https://img.yzcdn.cn/vant/rabbit.jpeg',
'https://img.yzcdn.cn/vant/bird.jpeg'
]
return avatars[Math.floor(Math.random() * avatars.length)]
}
/**
* 保存登录信息到本地存储
* @param {string} token 访问令牌
* @param {string} refreshToken 刷新令牌(可选)
* @param {Object} userInfo 用户信息
*/
static saveLoginInfo(token, refreshToken = null, userInfo = null) {
try {
// 使用配置中定义的存储键名
uni.setStorageSync(HTTP_CONFIG.storageKeys.token, token)
if (refreshToken) {
uni.setStorageSync(HTTP_CONFIG.storageKeys.refreshToken, refreshToken)
}
if (userInfo) {
uni.setStorageSync(HTTP_CONFIG.storageKeys.userInfo, userInfo)
}
console.log('登录信息保存成功')
} catch (error) {
console.error('保存登录信息失败:', error)
throw new Error('保存登录信息失败')
}
}
/**
* 获取本地存储的token
* @returns {string|null} 访问令牌
*/
static getToken() {
try {
return uni.getStorageSync(HTTP_CONFIG.storageKeys.token) || null
} catch (error) {
console.error('获取token失败:', error)
return null
}
}
/**
* 获取本地存储的刷新令牌
* @returns {string|null} 刷新令牌
*/
static getRefreshToken() {
try {
return uni.getStorageSync(HTTP_CONFIG.storageKeys.refreshToken) || null
} catch (error) {
console.error('获取refreshToken失败:', error)
return null
}
}
/**
* 获取本地存储的用户信息
* @returns {Object|null} 用户信息
*/
static getUserInfoFromStorage() {
try {
const userInfo = uni.getStorageSync(HTTP_CONFIG.storageKeys.userInfo)
return userInfo ? (typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo) : null
} catch (error) {
console.error('获取用户信息失败:', error)
return null
}
}
/**
* 检查是否已登录
* @returns {boolean} 是否已登录
*/
static isLoggedIn() {
const token = this.getToken()
return !!token
}
/**
* 清除所有登录信息
* @deprecated 使用 http/utils/auth-helper.js 中的 clearAuthData 替代
*/
static clearLoginInfo() {
const { clearAuthData } = require('../http/utils/auth-helper.js')
clearAuthData()
}
/**
* 跳转到登录页面
* @deprecated 使用 http/utils/auth-helper.js 中的 navigateToLogin 替代
*/
static navigateToLogin() {
const { navigateToLogin } = require('../http/utils/auth-helper.js')
navigateToLogin()
}
/**
* 检查登录状态并处理
* @returns {boolean} 是否已登录
* @deprecated 使用 http/utils/auth-helper.js 中的 checkLoginStatus 替代
*/
static checkLoginStatus() {
const { checkLoginStatus } = require('../http/utils/auth-helper.js')
return checkLoginStatus()
}
/**
* 完整的微信小程序登录流程
* @param {boolean} needUserInfo 是否需要获取用户详细信息
* @returns {Promise<Object>} 登录结果
*/
static async performWxLogin(needUserInfo = true) {
try {
// 1. 获取微信授权码
const code = await this.getWxCode()
// 2. 准备登录数据
const loginData = { code }
// 3. 如果需要用户信息,尝试获取
if (needUserInfo) {
try {
const userProfile = await this.getUserProfile()
loginData.encryptedData = userProfile.encryptedData
loginData.iv = userProfile.iv
loginData.signature = userProfile.signature
loginData.userInfo = userProfile.userInfo
} catch (userInfoError) {
console.warn('获取用户详细信息失败,使用基本登录:', userInfoError.message)
// 继续使用基本登录,不中断流程
}
}
return {
success: true,
data: loginData,
message: '微信授权成功'
}
} catch (error) {
console.error('微信登录流程失败:', error)
return {
success: false,
error: error.message,
message: '微信登录失败'
}
}
}
}
/**
* 默认导出,便于直接导入使用
*/
export default WechatAuth