12 3 months ago
parent 52d3c14c78
commit d45eb6a1ba

@ -0,0 +1,71 @@
# 失物招领小程序 - 图片搜索功能使用说明
## 功能介绍
图片搜索功能允许用户通过上传图片来查找相似的失物或招领信息,帮助用户更便捷地找到自己丢失的物品或确认找到的物品信息。
## 使用步骤
1. 在搜索页面点击"图片搜索"标签切换到图片搜索模式
2. 点击图片上传区域或"选择图片"按钮,可从相册选择图片或拍照上传
3. 上传图片后,点击"图片搜索"按钮开始搜索
4. 查看搜索结果,系统会按相似度从高到低排序展示
5. 点击任一结果可查看物品详细信息
## 功能特点
### 1. 直观的界面设计
- 清晰的文字/图片搜索模式切换
- 虚线边框的图片上传区域,包含上传图标和提示文字
- 搜索提示文字,指导用户如何获得更好的搜索效果
### 2. 增强的用户体验
- 支持点击预览已上传的图片
- 图片上传和识别过程中显示进度提示
- 搜索结果按相似度排序,并显示相似度百分比
- 上拉加载更多功能,方便浏览大量结果
### 3. 优化的视觉效果
- 加载动画效果,提升等待体验
- 美观的卡片式搜索结果展示
- 合理的色彩搭配和间距设置
- 支持自定义滚动条样式
## 开发说明
### 文件结构
- `pages/search/search.wxml`: 图片搜索页面的结构文件
- `pages/search/search.wxss`: 图片搜索页面的样式文件
- `pages/search/search.js`: 图片搜索页面的逻辑实现
- `app.js`: 包含图片搜索的API接口实现
### 关键功能实现
1. **图片上传与预览**
- 使用微信原生API `wx.chooseMedia` 实现图片选择
- 添加 `previewSelectedImage` 方法支持图片预览
2. **相似度处理**
- 在 `searchByImage` 方法中处理相似度数据
- 为模拟数据添加合理的相似度值
- 按相似度降序排序结果
3. **加载状态管理**
- 分阶段显示上传和识别进度
- 添加加载动画效果
- 加载更多功能的状态管理
### 注意事项
1. 当前版本使用模拟数据进行展示在实际环境中需要接入真实的图片识别API
2. 图片识别功能可能会消耗较多资源,实际使用中建议优化识别策略
3. 如需修改UI样式请在 `search.wxss` 文件中进行调整
4. 如需修改搜索逻辑,请在 `search.js` 文件中进行调整
## 未来优化方向
1. 接入真实的图片识别API提升识别准确率
2. 添加图片裁剪和优化功能,提高识别效果
3. 支持多图上传和比对功能
4. 优化移动端图片搜索的性能和体验

File diff suppressed because it is too large Load Diff

@ -0,0 +1,77 @@
{
"pages": [
"pages/index/index",
"pages/login/login",
"pages/search/search",
"pages/publish/publish",
"pages/detail/detail",
"pages/message/message",
"pages/user/user",
"pages/claim/claim",
"pages/match/match",
"pages/about/about",
"pages/settings/settings",
"pages/privacy/privacy",
"pages/agreement/agreement",
"pages/webview/webview"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#2196F3",
"navigationBarTitleText": "失物招领",
"navigationBarTextStyle": "white"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#2196F3",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "images/home_new.png",
"selectedIconPath": "images/home_selected_new.png"
},
{
"pagePath": "pages/search/search",
"text": "搜索",
"iconPath": "images/search_new.png",
"selectedIconPath": "images/search_selected_new.png"
},
{
"pagePath": "pages/publish/publish",
"text": "发布",
"iconPath": "images/publish_new.png",
"selectedIconPath": "images/publish_selected_new.png"
},
{
"pagePath": "pages/message/message",
"text": "消息",
"iconPath": "images/message_new.png",
"selectedIconPath": "images/message_selected_new.png"
},
{
"pagePath": "pages/user/user",
"text": "我的",
"iconPath": "images/user_new.png",
"selectedIconPath": "images/user_selected_new.png"
}
]
},
"sitemapLocation": "sitemap.json",
"permission": {
"scope.userInfo": {
"desc": "您的信息将用于完善用户资料"
},
"scope.camera": {
"desc": "用于拍照上传物品图片"
},
"scope.writePhotosAlbum": {
"desc": "用于保存图片"
}
},
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
]
}

@ -0,0 +1,216 @@
/**app.wxss**/
.container {
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 0;
margin: 0;
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
}
/* 页面标题样式 */
.page-title {
font-size: 18px;
font-weight: bold;
color: #333;
padding: 15px;
text-align: center;
}
/* 搜索框样式 */
.search-bar {
display: flex;
align-items: center;
padding: 10px 15px;
background-color: #fff;
}
.search-input {
flex: 1;
height: 36px;
background-color: #f5f5f5;
border-radius: 18px;
padding: 0 15px;
font-size: 14px;
color: #333;
}
.search-button {
margin-left: 10px;
width: 36px;
height: 36px;
background-color: #2196F3;
border-radius: 18px;
display: flex;
justify-content: center;
align-items: center;
}
/* 物品列表样式 */
.item-list {
padding: 0 15px 20px;
}
.item-card {
background-color: #fff;
border-radius: 8px;
margin-top: 10px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.item-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.item-desc {
font-size: 14px;
color: #666;
margin-bottom: 8px;
line-height: 1.5;
}
.item-info {
font-size: 12px;
color: #999;
display: flex;
justify-content: space-between;
}
/* 按钮样式 */
.btn-primary {
width: 100%;
height: 44px;
background-color: #2196F3;
color: #fff;
border-radius: 22px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
font-weight: bold;
margin-top: 20px;
}
.btn-secondary {
width: 100%;
height: 44px;
background-color: #fff;
color: #2196F3;
border: 1px solid #2196F3;
border-radius: 22px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
margin-top: 10px;
}
/* 表单样式 */
.form-group {
margin-bottom: 15px;
}
.form-label {
font-size: 14px;
color: #666;
margin-bottom: 5px;
display: block;
}
.form-input {
width: 100%;
height: 40px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
}
.form-textarea {
width: 100%;
height: 100px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
font-size: 14px;
line-height: 1.5;
}
/* 图片上传样式 */
.image-upload {
display: flex;
flex-wrap: wrap;
}
.image-item {
width: 80px;
height: 80px;
margin-right: 10px;
margin-bottom: 10px;
position: relative;
}
.image-preview {
width: 100%;
height: 100%;
border-radius: 4px;
}
.image-upload-btn {
width: 80px;
height: 80px;
border: 1px dashed #ddd;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
color: #999;
}
/* 消息提示样式 */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 10px 20px;
border-radius: 4px;
font-size: 14px;
z-index: 9999;
}
/* 加载动画样式 */
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
}
/* 空状态样式 */
.empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px 0;
color: #999;
}
.empty-icon {
width: 80px;
height: 80px;
margin-bottom: 10px;
}
.empty-text {
font-size: 14px;
}

@ -0,0 +1,127 @@
# 失物招领小程序 - 云开发实施指南
本指南详细说明了如何使用微信云开发功能实现多账号数据互通,包括云数据库、云存储和云函数的配置与使用方法。
## 前提条件
1. 已开通微信小程序云开发功能
2. 已创建云开发环境
3. 已在小程序管理后台完成域名配置(如需)
## 配置步骤
### 1. 初始化云开发环境
在app.js中已经配置了云开发初始化代码需要将环境ID替换为您自己的环境ID
```javascript
wx.cloud.init({
env: 'shiwuzhaoling-xxxx', // 替换为您的云开发环境ID
traceUser: true
});
```
### 2. 创建数据库集合
在微信开发者工具的云开发控制台中,创建以下数据库集合:
- **items** - 存储失物和招领信息
为items集合设置适当的权限建议设置为"所有用户可读,仅创建者可写"
### 3. 部署云函数
1. 在微信开发者工具中,右键点击`cloudfunctions`文件夹
2. 选择"上传并部署所有云函数"
### 4. 配置云存储权限
在云开发控制台中,设置云存储权限,允许用户上传和读取图片文件。
## 功能说明
### 数据共享功能
1. **物品发布** - 用户发布的失物或招领信息会存储到云数据库,所有用户都可以查看
2. **图片上传** - 发布时上传的图片会存储到云存储生成的fileID会保存到数据库
3. **搜索功能** - 支持按关键词搜索云数据库中的物品信息
4. **用户登录** - 使用云函数获取用户的openid实现用户身份标识
### 数据模型
items集合中的文档结构如下
```javascript
{
_id: "数据库自动生成的ID",
_openid: "发布者的openid",
type: "lost"或"found", // 失物或招领
title: "物品标题",
description: "物品描述",
location: "丢失/发现地点",
time: "丢失/发现时间",
contactName: "联系人姓名",
contactPhone: "联系电话",
images: ["云存储fileID1", "云存储fileID2"], // 图片列表
publishTime: Date, // 发布时间
createTime: "创建时间字符串"
}
```
## 注意事项
1. **云开发环境ID**必须替换为您自己的环境ID否则会提示"env not exists"错误
2. **首次使用前**:需要部署云函数(如果有云函数)
3. **权限设置**:确保数据库和存储的权限设置正确
4. **测试多账号**:需要使用不同的微信账号扫码登录测试
5. **免费额度**:云开发基础版有免费额度,足够开发和小规模使用
## 部署检查清单
按照以下步骤完成部署:
1. **创建云开发环境**
- [ ] 在微信开发者工具中开通云开发
- [ ] 创建环境并获取环境ID
- [ ] 在代码中配置环境ID
2. **配置数据库**
- [ ] 创建 `items` 集合
- [ ] 设置集合权限为"所有用户可读,仅创建者可写"
- [ ] 创建必要的索引(可选但推荐)
3. **配置云存储**
- [ ] 设置存储权限为"所有用户可读,仅创建者可写"
- [ ] 创建 `item_images` 文件夹(可选)
4. **测试功能**
- [ ] 测试发布物品功能
- [ ] 测试查询物品功能
- [ ] 测试搜索功能
- [ ] 测试图片上传功能
详细步骤请参考 `云数据库部署指南.md` 文件。
## 常见问题
### 云函数部署失败
- 检查网络连接
- 确保已正确配置appID和云开发环境
- 检查Node.js版本是否兼容
### 数据库查询返回空
- 检查集合权限设置
- 确保集合名称正确
- 检查查询条件是否合理
### 图片上传失败
- 检查云存储权限设置
- 确保文件大小在限制范围内
- 检查网络连接
## 性能优化建议
1. 为常用查询字段创建索引,提高查询性能
2. 限制每次查询返回的记录数量,实现分页加载
3. 对图片进行压缩处理后再上传,减少存储空间占用
4. 使用云函数进行复杂的数据处理,减轻前端负担

@ -0,0 +1,8 @@
{
"permissions": {
"openapi": []
},
"timeout": 60,
"memorySize": 512
}

@ -0,0 +1,521 @@
// 云函数:图片搜索(以图搜图)
const cloud = require('wx-server-sdk');
const https = require('https');
const crypto = require('crypto');
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
});
const db = cloud.database();
// 腾讯AI特征提取
async function extractImageFeaturesWithTencentAI(imageBuffer) {
try {
const config = {
SECRET_ID: 'AKIDU4ScZlvNrGXcUCiw53pQn161mNoKn6FD',
SECRET_KEY: 'ujOuFPM9qUyISSHzYw9OoO4Qp14f7Rcf',
REGION: 'ap-beijing'
};
// 将图片转换为base64
const imageBase64 = imageBuffer.toString('base64');
// 调用tencentAI云函数来获取AI特征
try {
const aiResult = await cloud.callFunction({
name: 'tencentAI',
data: {
image: imageBase64,
secretId: config.SECRET_ID,
secretKey: config.SECRET_KEY,
region: config.REGION
}
});
if (aiResult && aiResult.result && aiResult.result.features) {
return aiResult.result.features;
}
} catch (aiError) {
console.warn('调用tencentAI云函数失败');
}
// 使用降级方法
} catch (error) {
console.error('腾讯AI特征提取失败');
return extractImageFeaturesFromBuffer(imageBuffer);
}
}
// 降级特征提取方法
function extractImageFeaturesFromBuffer(imageBuffer) {
try {
// 特征提取
const fileSize = imageBuffer.length;
const normalizedSize = Math.min(fileSize / 2000000, 1);
const md5Hash = crypto.createHash('md5').update(imageBuffer).digest('hex');
const sha1Hash = crypto.createHash('sha1').update(imageBuffer).digest('hex');
const header = imageBuffer.slice(0, 16);
const headerFeatures = [];
for (let i = 0; i < 16; i++) {
headerFeatures.push(header[i] / 255);
}
const hashFeatures = [];
const combinedHash = md5Hash + sha1Hash;
for (let i = 0; i < 128; i++) {
const charCode = combinedHash.charCodeAt(i % combinedHash.length);
hashFeatures.push((charCode % 1000) / 1000);
}
const sizeFeatures = [];
const chunkSize = Math.floor(imageBuffer.length / 32);
for (let i = 0; i < 32; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, imageBuffer.length);
let sum = 0;
for (let j = start; j < end; j++) {
sum += imageBuffer[j];
}
sizeFeatures.push((sum / (chunkSize * 255)) || 0);
}
// 合并所有特征16 + 128 + 32 = 176维
const features = [
normalizedSize, // 文件大小
...headerFeatures, // 文件头特征
...hashFeatures, // 哈希特征
...sizeFeatures // 文件内容分布特征
];
return features;
} catch (error) {
console.error('特征提取失败:', error);
// 返回默认特征向量
return new Array(176).fill(0);
}
}
// 提取图片特征(主函数)
async function extractImageFeatures(imageBuffer) {
// 优先使用腾讯AI失败则使用降级方法
try {
const features = await extractImageFeaturesWithTencentAI(imageBuffer);
if (features && features.length > 0) {
return features;
}
} catch (error) {
console.warn('AI特征提取失败使用降级方法:', error);
}
// 降级到基于Buffer的特征提取
return extractImageFeaturesFromBuffer(imageBuffer);
}
// 计算特征向量的差异(用于检测无效特征)
function calculateFeatureDifference(vecA, vecB) {
if (!vecA || !vecB || !Array.isArray(vecA) || !Array.isArray(vecB)) {
return 1; // 如果无法计算,返回最大差异
}
const minLength = Math.min(vecA.length, vecB.length);
if (minLength === 0) return 1;
let sumDiff = 0;
for (let i = 0; i < minLength; i++) {
const diff = Math.abs((vecA[i] || 0) - (vecB[i] || 0));
sumDiff += diff;
}
// 返回平均差异(归一化)
return sumDiff / minLength;
}
// 计算余弦相似度(改进版:增加维度匹配检查和异常值处理)
function cosineSimilarity(vecA, vecB) {
if (!vecA || !vecB || !Array.isArray(vecA) || !Array.isArray(vecB)) {
return 0;
}
// 如果维度差异太大超过20%直接返回0避免不准确的匹配
const lengthA = vecA.length;
const lengthB = vecB.length;
if (lengthA === 0 || lengthB === 0) return 0;
const lengthDiff = Math.abs(lengthA - lengthB) / Math.max(lengthA, lengthB);
if (lengthDiff > 0.2) {
console.warn(`特征向量维度差异过大: ${lengthA} vs ${lengthB}, 差异: ${(lengthDiff * 100).toFixed(1)}%`);
return 0;
}
const minLength = Math.min(lengthA, lengthB);
let dotProduct = 0;
let normA = 0;
let normB = 0;
let zeroCountA = 0;
let zeroCountB = 0;
for (let i = 0; i < minLength; i++) {
const valA = vecA[i] || 0;
const valB = vecB[i] || 0;
// 统计零值数量,如果零值太多,可能是无效特征
if (Math.abs(valA) < 1e-10) zeroCountA++;
if (Math.abs(valB) < 1e-10) zeroCountB++;
dotProduct += valA * valB;
normA += valA * valA;
normB += valB * valB;
}
// 仅记录零值占比不再直接将相似度置为0避免所有结果被过滤
if (zeroCountA / minLength > 0.8 || zeroCountB / minLength > 0.8) {
console.warn(`特征向量零值过多: A=${(zeroCountA/minLength*100).toFixed(1)}%, B=${(zeroCountB/minLength*100).toFixed(1)}%`);
}
normA = Math.sqrt(normA);
normB = Math.sqrt(normB);
if (normA === 0 || normB === 0) return 0;
const similarity = dotProduct / (normA * normB);
// 限制相似度范围在0-1之间并处理浮点误差
return Math.max(0, Math.min(1, similarity));
}
function extractStoredFeatures(item) {
if (!item) return null;
if (Array.isArray(item.imageFeatureVectors) && item.imageFeatureVectors.length > 0) {
const vector = item.imageFeatureVectors[0];
if (vector && Array.isArray(vector.features) && vector.features.length > 0) {
return vector.features;
}
}
if (Array.isArray(item.imageFeatures) && item.imageFeatures.length > 0) {
return item.imageFeatures;
}
return null;
}
async function persistItemFeatures(itemId, fileID, features) {
if (!itemId || !features || !features.length) return;
try {
await db.collection('items').doc(itemId).update({
data: {
imageFeatureVectors: [{
fileID: fileID || '',
features: features,
dimension: features.length,
source: features.length >= 512 ? 'tencentAI' : 'local',
updatedAt: new Date()
}]
}
});
console.log(`已持久化物品 ${itemId} 的图片特征,维度: ${features.length}`);
} catch (err) {
console.warn(`持久化物品 ${itemId} 特征失败:`, err.message || err);
}
}
async function getOrCreateItemFeatures(item, cache) {
if (!item) return null;
const imageFileID = item.images && item.images.length > 0 ? item.images[0] : '';
const cacheKey = imageFileID || item._id;
if (cache && cacheKey && cache.has(cacheKey)) {
return cache.get(cacheKey);
}
let stored = extractStoredFeatures(item);
if (stored && stored.length > 0) {
if (cache && cacheKey) cache.set(cacheKey, stored);
return stored;
}
if (!imageFileID) {
console.warn(`物品 ${item._id} 没有图片文件ID无法提取特征`);
return null;
}
const computed = await getImageFeaturesFromCloud(imageFileID);
if (computed && computed.length > 0) {
await persistItemFeatures(item._id, imageFileID, computed);
if (cache && cacheKey) cache.set(cacheKey, computed);
return computed;
}
return null;
}
// 从云存储下载图片并提取特征
async function getImageFeaturesFromCloud(fileID) {
try {
// 下载云存储文件(添加超时控制)
const fileContent = await Promise.race([
cloud.downloadFile({
fileID: fileID
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('下载超时')), 5000) // 5秒超时
)
]);
if (!fileContent || !fileContent.fileContent) {
console.error('下载文件内容为空:', fileID);
return null;
}
// 提取特征(异步)
const features = await extractImageFeatures(fileContent.fileContent);
return features;
} catch (error) {
console.error('获取图片特征失败:', fileID, error.message);
return null;
}
}
// 主函数
exports.main = async (event, context) => {
const { imageFileID, imageBase64 } = event;
console.log('开始图片搜索图片ID:', imageFileID);
try {
// 1. 提取查询图片的特征
let queryFeatures = null;
if (imageFileID) {
// 从云存储获取图片并提取特征(添加超时控制)
const fileContent = await Promise.race([
cloud.downloadFile({
fileID: imageFileID
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('下载查询图片超时')), 10000) // 10秒超时
)
]);
if (!fileContent || !fileContent.fileContent) {
return {
success: false,
error: '下载查询图片失败'
};
}
queryFeatures = await extractImageFeatures(fileContent.fileContent);
} else if (imageBase64) {
// 从base64获取图片并提取特征
const imageBuffer = Buffer.from(imageBase64, 'base64');
queryFeatures = await extractImageFeatures(imageBuffer);
} else {
return {
success: false,
error: '缺少图片参数'
};
}
if (!queryFeatures || queryFeatures.length === 0) {
return {
success: false,
error: '特征提取失败'
};
}
console.log('查询图片特征提取成功,维度:', queryFeatures.length);
// 2. 从云数据库获取所有物品(包含图片)
// 优化:限制数量,避免超时
const itemsResult = await db.collection('items')
.where({
images: db.command.exists(true)
})
.limit(50) // 减少到50条避免超时
.get();
console.log('从云数据库获取物品数量:', itemsResult.data.length);
if (itemsResult.data.length === 0) {
return {
success: true,
results: [],
total: 0
};
}
const featureCache = new Map();
// 3. 对每个物品的图片进行特征提取和相似度计算
// 优化使用Promise.all并行处理提高速度
const similarItems = [];
const processPromises = [];
for (const item of itemsResult.data) {
if (!item.images || !Array.isArray(item.images) || item.images.length === 0) {
continue;
}
// 只处理第一张图片
const itemImageFileID = item.images[0];
// 创建处理Promise
const processPromise = (async () => {
try {
// 获取或生成持久化的物品图片特征
const itemFeatures = await getOrCreateItemFeatures(item, featureCache);
if (itemFeatures && itemFeatures.length > 0) {
// 计算相似度
const similarity = cosineSimilarity(queryFeatures, itemFeatures);
// 阈值设置为0尽可能展示所有计算出的匹配结果
let threshold = 0;
// 如果相似度异常高(>0.99可能是特征向量有问题比如都是0或都相同需要额外检查
if (similarity > 0.99) {
// 检查特征向量是否过于相似(可能是无效特征)
const featureDiff = calculateFeatureDifference(queryFeatures, itemFeatures);
if (featureDiff < 0.01) {
console.warn(`物品 ${item._id} 相似度过高(${(similarity*100).toFixed(2)}%)但特征差异很小(${(featureDiff*100).toFixed(2)}%),可能是无效特征,跳过`);
return null;
}
}
console.log(`物品 ${item._id} 相似度: ${(similarity * 100).toFixed(2)}%, 阈值: ${(threshold * 100).toFixed(2)}%, 特征维度: ${queryFeatures.length}/${itemFeatures.length}`);
if (similarity >= threshold) {
return {
id: item._id,
title: item.title || '未命名物品',
description: item.description || '',
type: item.type || 'unknown',
location: item.location || '',
time: item.time || item.publishTime || item.createTime || '',
images: item.images || [],
similarity: Math.floor(similarity * 10000) / 10000, // 保留4位小数
publishTime: item.publishTime || item.createTime || '',
contactName: item.contactName || '',
contactPhone: item.contactPhone || ''
};
}
}
return null;
} catch (error) {
console.error('处理物品失败:', item._id, error);
return null;
}
})();
processPromises.push(processPromise);
}
// 等待所有处理完成(并行处理,提高速度)
console.log('开始并行处理', processPromises.length, '个物品');
const results = await Promise.all(processPromises);
// 过滤掉null值
for (const result of results) {
if (result) {
similarItems.push(result);
}
}
// 4. 按相似度排序
similarItems.sort((a, b) => b.similarity - a.similarity);
// 5. 过滤和返回结果
// 最终过滤阈值同样降为0确保返回全部计算结果
let finalThreshold = 0;
// 只返回相似度 >= 最终阈值 的结果最多返回前10个
const filteredResults = similarItems.filter(item => item.similarity >= finalThreshold);
const topResults = filteredResults.slice(0, 10);
console.log(`最终过滤:原始结果${similarItems.length}个,阈值>=${(finalThreshold*100).toFixed(0)}%后剩余${filteredResults.length}个,返回前${topResults.length}`);
// 6. 将 cloud:// 文件ID 转换为可直接访问的临时链接,避免 403
await convertCloudImagesToTempUrls(topResults);
console.log(`搜索完成,找到${similarItems.length}个相似物品,过滤后${filteredResults.length}个(阈值>=${(finalThreshold*100).toFixed(0)}%),返回前${topResults.length}`);
// 如果过滤后没有结果但原始结果有返回相似度最高的4个
if (topResults.length === 0 && similarItems.length > 0) {
const fallbackResults = similarItems.slice(0, 4);
await convertCloudImagesToTempUrls(fallbackResults);
console.log('过滤后无结果,返回相似度最高的', fallbackResults.length, '个结果');
return {
success: true,
results: fallbackResults,
total: similarItems.length,
warning: '相似度较低,结果仅供参考'
};
}
return {
success: true,
results: topResults,
total: filteredResults.length
};
} catch (error) {
console.error('图片搜索失败:', error);
return {
success: false,
error: error.message || '搜索失败'
};
}
};
// 将 cloud:// 图片文件ID 转换为临时 HTTPS 链接,避免前端直接访问被 403
async function convertCloudImagesToTempUrls(items) {
try {
if (!items || items.length === 0) {
return;
}
const fileIDs = new Set();
items.forEach(item => {
const images = Array.isArray(item.images) ? item.images : [];
images.forEach(id => {
if (typeof id === 'string' && id.startsWith('cloud://')) {
fileIDs.add(id);
}
});
});
if (fileIDs.size === 0) {
return;
}
const res = await cloud.getTempFileURL({
fileList: Array.from(fileIDs)
});
const urlMap = new Map();
if (res && Array.isArray(res.fileList)) {
res.fileList.forEach(file => {
if (file && file.fileID) {
urlMap.set(file.fileID, file.tempFileURL || '');
}
});
}
items.forEach(item => {
const imgIDs = Array.isArray(item.images) ? item.images : [];
// 原始 cloud:// 保存到新字段,方便前端需要时仍可获取
item.cloudImages = imgIDs;
item.images = imgIDs.map(id => {
if (typeof id === 'string' && id.startsWith('cloud://')) {
return urlMap.get(id) || id;
}
return id;
});
});
} catch (error) {
console.error('转换临时图片链接失败:', error);
}
}

@ -0,0 +1,10 @@
{
"name": "imageSearch",
"version": "1.0.0",
"description": "图片搜索云函数",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}

@ -0,0 +1,25 @@
// 云函数login.js
const cloud = require('wx-server-sdk');
// 初始化云环境
cloud.init({
env: 'cloud1-6guygewn9069e603'
});
/**
* 登录云函数用于获取用户的openid
* @param {Object} event - 事件对象
* @param {Object} context - 上下文对象
* @returns {Object} 包含openid和appid的对象
*/
exports.main = async (event, context) => {
// 获取微信上下文
const wxContext = cloud.getWXContext();
// 返回openid和appid
return {
openid: wxContext.OPENID,
appid: wxContext.APPID,
unionid: wxContext.UNIONID,
};
};

@ -0,0 +1,14 @@
{
"name": "login",
"version": "1.0.0",
"description": "用户登录云函数",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"wx-server-sdk": "~2.11.0"
}
}

@ -0,0 +1,6 @@
{
"permissions": {
"openapi": []
}
}

@ -0,0 +1,432 @@
// 腾讯云图像识别云函数
// 用于调用腾讯云图像识别APIAPI v3
const cloud = require('wx-server-sdk');
const crypto = require('crypto');
const https = require('https');
const sizeOf = require('image-size');
const jpeg = require('jpeg-js');
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
});
/**
* 生成腾讯云API v3签名
*/
function generateSignature(secretKey, service, region, action, timestamp, requestPayload) {
// 1. 构建规范请求串
const httpRequestMethod = 'POST';
const canonicalUri = '/';
const canonicalQueryString = '';
const canonicalHeaders = `content-type:application/json\nhost:${service}.tencentcloudapi.com\n`;
const signedHeaders = 'content-type;host';
const hashedRequestPayload = crypto.createHash('sha256').update(JSON.stringify(requestPayload)).digest('hex');
const canonicalRequest = `${httpRequestMethod}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${hashedRequestPayload}`;
// 2. 构建待签名字符串
const algorithm = 'TC3-HMAC-SHA256';
const date = new Date(timestamp * 1000).toISOString().substring(0, 10).replace(/-/g, '');
const credentialScope = `${date}/${service}/tc3_request`;
const hashedCanonicalRequest = crypto.createHash('sha256').update(canonicalRequest).digest('hex');
const stringToSign = `${algorithm}\n${timestamp}\n${credentialScope}\n${hashedCanonicalRequest}`;
// 3. 计算签名
const kDate = crypto.createHmac('sha256', 'TC3' + secretKey).update(date).digest();
const kService = crypto.createHmac('sha256', kDate).update(service).digest();
const kSigning = crypto.createHmac('sha256', kService).update('tc3_request').digest();
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex');
return signature;
}
/**
* 使用HTTPS发送HTTP请求
*/
function httpsRequest(options, data) {
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
try {
const result = JSON.parse(responseData);
resolve(result);
} catch (e) {
reject(new Error('解析响应失败: ' + e.message));
}
});
});
req.on('error', (error) => {
reject(error);
});
if (data) {
req.write(JSON.stringify(data));
}
req.end();
});
}
/**
* 调用腾讯云图像识别API
*/
async function callTencentCloudImageRecognition(imageBase64, secretId, secretKey, region) {
const service = 'iai'; // 图像识别服务
const action = 'DetectLabel'; // 图像标签识别
const version = '2020-03-03';
const timestamp = Math.floor(Date.now() / 1000);
// 构建请求参数
const requestPayload = {
ImageBase64: imageBase64,
MaxLabels: 10 // 最多返回10个标签
};
// 生成签名
const signature = generateSignature(secretKey, service, region, action, timestamp, requestPayload);
// 构建Authorization头
const date = new Date(timestamp * 1000).toISOString().substring(0, 10).replace(/-/g, '');
const credentialScope = `${date}/${service}/tc3_request`;
const authorization = `TC3-HMAC-SHA256 Credential=${secretId}/${credentialScope}, SignedHeaders=content-type;host, Signature=${signature}`;
// 构建请求选项
const options = {
hostname: `${service}.tencentcloudapi.com`,
port: 443,
path: '/',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': authorization,
'X-TC-Action': action,
'X-TC-Version': version,
'X-TC-Timestamp': timestamp.toString(),
'X-TC-Region': region || 'ap-beijing'
}
};
// 发送请求
try {
const response = await httpsRequest(options, requestPayload);
return response;
} catch (error) {
console.error('HTTPS请求失败:', error);
throw error;
}
}
/**
* 将腾讯云识别结果转换为特征向量
*/
function convertToFeatures(apiResult, imageBase64) {
const features = new Array(512).fill(0);
if (apiResult && apiResult.Response && apiResult.Response.Labels) {
const labels = apiResult.Response.Labels;
// 使用标签信息生成特征向量
labels.forEach((label, index) => {
if (index < 128) {
// 标签名称的哈希值
const nameHash = crypto.createHash('md5').update(label.Name || '').digest('hex');
features[index * 2] = parseInt(nameHash.substring(0, 2), 16) / 255;
// 置信度
features[index * 2 + 1] = label.Confidence ? label.Confidence / 100 : 0;
}
});
// 添加统计特征
if (labels.length > 0) {
const avgConfidence = labels.reduce((sum, label) => sum + (label.Confidence || 0), 0) / labels.length;
features[256] = avgConfidence / 100;
features[257] = Math.min(labels.length / 10, 1);
}
}
// 如果API返回为空或者标签数量为0使用基于图片内容的降级特征
if (features.every(v => v === 0)) {
console.warn('API返回为空或无标签使用图片内容生成特征向量');
const fallback = generateContentFeaturesFromBase64(imageBase64);
for (let i = 0; i < fallback.length && i < features.length; i++) {
features[i] = fallback[i];
}
}
return features;
}
/**
* 基于图片字节内容生成特征降级方案避免仅依赖路径/哈希
*/
function generateContentFeaturesFromBase64(imageBase64) {
try {
const buffer = Buffer.from(imageBase64 || '', 'base64');
if (!buffer || buffer.length === 0) {
return new Array(512).fill(0);
}
const features = [];
// 1. 文件大小特征
const normalizedSize = Math.min(buffer.length / (1024 * 1024 * 2), 1); // 以2MB为上限
features.push(normalizedSize);
// 2. 哈希特征
const md5Hash = crypto.createHash('md5').update(buffer).digest('hex');
const sha1Hash = crypto.createHash('sha1').update(buffer).digest('hex');
const combinedHash = md5Hash + sha1Hash;
for (let i = 0; i < 128; i++) {
const charCode = combinedHash.charCodeAt(i % combinedHash.length);
features.push((charCode % 1000) / 1000);
}
// 3. 文件头特征
const header = buffer.slice(0, 64);
for (let i = 0; i < 64; i++) {
features.push(header[i] ? header[i] / 255 : 0);
}
// 4. 字节分布特征
const chunkCount = 64;
const chunkSize = Math.max(1, Math.floor(buffer.length / chunkCount));
for (let i = 0; i < chunkCount; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, buffer.length);
let sum = 0;
for (let j = start; j < end; j++) {
sum += buffer[j];
}
features.push((sum / (chunkSize * 255)) || 0);
}
// 5. 随机投影特征(提高区分度)
for (let i = 0; i < 256; i++) {
const hash = crypto.createHash('md5').update(buffer.slice(i, i + 256)).digest('hex');
features.push(parseInt(hash.substring(0, 2), 16) / 255);
}
// 截断/填充到512维
if (features.length < 512) {
return [...features, ...new Array(512 - features.length).fill(0)];
}
return features.slice(0, 512);
} catch (err) {
console.error('生成内容特征失败,返回默认特征:', err);
return new Array(512).fill(0);
}
}
/**
* 根据图片基础信息生成可识别的标签用于腾讯AI未返回标签时的降级方案
*/
function generateFallbackLabels(imageBase64) {
const labels = [];
if (!imageBase64) {
return labels;
}
let buffer = null;
try {
buffer = Buffer.from(imageBase64, 'base64');
} catch (err) {
console.warn('生成标签时解析图片失败:', err.message);
return labels;
}
if (!buffer || buffer.length === 0) {
return labels;
}
const sizeKB = buffer.length / 1024;
if (sizeKB > 1500) {
labels.push({ Name: 'LargeImage', Confidence: 70 });
} else if (sizeKB > 600) {
labels.push({ Name: 'MediumImage', Confidence: 65 });
} else {
labels.push({ Name: 'CompactImage', Confidence: 60 });
}
try {
const info = sizeOf(buffer);
if (info && info.width && info.height) {
const { width, height, type } = info;
if (type) {
labels.push({ Name: `${type.toUpperCase()} Format`, Confidence: 60 });
}
const aspect = width / height;
if (aspect > 1.2) {
labels.push({ Name: 'LandscapeOrientation', Confidence: 70 });
} else if (aspect < 0.8) {
labels.push({ Name: 'PortraitOrientation', Confidence: 70 });
} else {
labels.push({ Name: 'SquareOrientation', Confidence: 65 });
}
if (width * height >= 1920 * 1080) {
labels.push({ Name: 'HighResolution', Confidence: 68 });
} else if (width * height <= 640 * 480) {
labels.push({ Name: 'LowResolution', Confidence: 60 });
}
}
} catch (err) {
console.warn('获取图片尺寸信息失败:', err.message);
}
try {
// 尝试解析JPEG以估算颜色与亮度
const decoded = jpeg.decode(buffer, { useTArray: true, formatAsRGBA: true });
if (decoded && decoded.data) {
let rSum = 0, gSum = 0, bSum = 0;
const data = decoded.data;
const total = data.length / 4;
for (let i = 0; i < data.length; i += 4) {
rSum += data[i];
gSum += data[i + 1];
bSum += data[i + 2];
}
const avgR = rSum / total;
const avgG = gSum / total;
const avgB = bSum / total;
const brightness = 0.299 * avgR + 0.587 * avgG + 0.114 * avgB;
if (brightness > 200) {
labels.push({ Name: 'BrightScene', Confidence: 65 });
} else if (brightness < 80) {
labels.push({ Name: 'DarkScene', Confidence: 65 });
} else {
labels.push({ Name: 'NormalLighting', Confidence: 60 });
}
const dominantChannel = Math.max(avgR, avgG, avgB);
if (dominantChannel === avgR) {
labels.push({ Name: 'WarmTone', Confidence: 60 });
} else if (dominantChannel === avgG) {
labels.push({ Name: 'GreenTone', Confidence: 60 });
} else {
labels.push({ Name: 'CoolTone', Confidence: 60 });
}
}
} catch (err) {
console.warn('解析颜色信息失败,使用基于哈希的标签:', err.message);
const hash = crypto.createHash('sha1').update(buffer).digest('hex');
const palette = ['WarmTone', 'CoolTone', 'NeutralTone', 'HighContrast', 'LowContrast'];
labels.push({
Name: palette[parseInt(hash.substring(0, 2), 16) % palette.length],
Confidence: 55
});
}
if (labels.length === 0) {
labels.push({ Name: 'GenericImage', Confidence: 40 });
}
return labels;
}
/**
* 主函数
*/
exports.main = async (event, context) => {
console.log('========== 腾讯云AI云函数被调用 ==========');
console.log('参数检查:', {
hasImage: !!event.image,
imageLength: event.image ? event.image.length : 0,
hasSecretId: !!event.secretId,
hasSecretKey: !!event.secretKey,
region: event.region || 'ap-beijing'
});
const { image, secretId, secretKey, region } = event;
// 参数验证
if (!image) {
console.error('❌ 缺少图片数据');
return {
error: '缺少图片数据',
code: -1
};
}
if (!secretId || !secretKey) {
console.error('❌ 缺少SecretId或SecretKey');
return {
error: '缺少SecretId或SecretKey',
code: -2
};
}
try {
console.log('开始调用腾讯云图像识别API...');
// 调用腾讯云API
const apiResult = await callTencentCloudImageRecognition(
image,
secretId,
secretKey,
region || 'ap-beijing'
);
console.log('腾讯云API调用成功');
console.log('API返回结果:', apiResult ? '有数据' : '无数据');
if (apiResult && apiResult.Response) {
console.log('识别标签数量:', apiResult.Response.Labels ? apiResult.Response.Labels.length : 0);
}
// 如果官方接口未返回标签,生成可识别的本地标签
let finalLabels = (apiResult && apiResult.Response && Array.isArray(apiResult.Response.Labels))
? apiResult.Response.Labels
: [];
if (!finalLabels || finalLabels.length === 0) {
finalLabels = generateFallbackLabels(image);
if (!apiResult.Response) {
apiResult.Response = {};
}
apiResult.Response.Labels = finalLabels;
console.log('标签为空,已生成本地标签:', finalLabels.map(l => l.Name));
}
// 转换为特征向量
const features = convertToFeatures(apiResult, image);
console.log('特征向量生成完成,维度:', features.length);
console.log('特征向量前5个值:', features.slice(0, 5));
return {
features: features,
labels: finalLabels,
success: true,
apiResult: apiResult // 可选返回原始API结果用于调试
};
} catch (error) {
console.error('❌ 调用腾讯云API失败:', error);
console.error('错误详情:', error.message);
console.error('错误堆栈:', error.stack);
// 降级方案使用Base64生成特征向量
console.warn('⚠️ 使用降级方案:基于图片内容生成特征向量');
const features = generateContentFeaturesFromBase64(image);
const fallbackLabels = generateFallbackLabels(image);
return {
features: features,
labels: fallbackLabels,
success: false,
error: error.message || '调用失败',
code: -3,
fallback: true // 标记为降级方案
};
}
};

@ -0,0 +1,12 @@
{
"name": "tencentAI",
"version": "1.0.0",
"description": "腾讯云图像识别云函数",
"main": "index.js",
"dependencies": {
"image-size": "^1.0.2",
"jpeg-js": "^0.4.4",
"wx-server-sdk": "~2.6.3"
}
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100" fill="none" stroke="#2196F3" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><circle cx="50" cy="50" r="45"></circle><line x1="50" y1="15" x2="50" y2="35"></line><line x1="30" y1="30" x2="70" y2="30"></line><path d="M30 70h40l-20-20z"></path></svg>

After

Width:  |  Height:  |  Size: 351 B

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>

After

Width:  |  Height:  |  Size: 329 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 18l6-6-6-6"></path><path d="M8 6l-6 6 6 6"></path><line x1="19" y1="12" x2="5" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 286 B

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 17.25V21H6.75L17.81 9.94L14.06 6.19L3 17.25Z" stroke="#999999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.71 7.04C21.1 6.65 21.1 6.02 20.71 5.63L18.37 3.29C17.98 2.9 17.35 2.9 16.96 3.29L15.13 5.12L18.88 8.87L20.71 7.04Z" stroke="#999999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 459 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>

After

Width:  |  Height:  |  Size: 317 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80" fill="none" stroke="#ddd" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><circle cx="40" cy="40" r="35"></circle><line x1="40" y1="25" x2="40" y2="35"></line><line x1="40" y1="45" x2="40" y2="55"></line><line x1="25" y1="40" x2="35" y2="40"></line><line x1="45" y1="40" x2="55" y2="40"></line></svg>

After

Width:  |  Height:  |  Size: 399 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4caf50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>

After

Width:  |  Height:  |  Size: 286 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2196F3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f44336" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>

After

Width:  |  Height:  |  Size: 314 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ff9800" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>

After

Width:  |  Height:  |  Size: 305 B

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.14 12.94c0.04-0.3 0.06-0.61 0.06-0.94 0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14 0.23-0.41 0.12-0.61l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39 0.96c-0.5-0.38-1.03-0.7-1.62-0.94l-0.36-2.54c-0.04-0.24-0.24-0.41-0.48-0.41h-3.84c-0.24 0-0.43 0.17-0.47 0.41l-0.36 2.54c-0.59 0.24-1.13 0.57-1.62 0.94l-2.39-0.96c-0.22-0.08-0.47 0-0.59 0.22L2.74 8.87c-0.12 0.21-0.08 0.47 0.12 0.61l2.03 1.58C4.84 11.36 4.8 11.69 4.8 12s0.02 0.64 0.07 0.94l-2.03 1.58c-0.18 0.14-0.23 0.41-0.12 0.61l1.92 3.32c0.12 0.22 0.37 0.29 0.59 0.22l2.39-0.96c0.5 0.38 1.03 0.7 1.62 0.94l0.36 2.54c0.05 0.24 0.24 0.41 0.48 0.41h3.84c0.24 0 0.44-0.17 0.47-0.41l0.36-2.54c0.59-0.24 1.13-0.56 1.62-0.94l2.39 0.96c0.22 0.08 0.47 0 0.59-0.22l1.92-3.32c0.12-0.22 0.07-0.47-0.12-0.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" fill="#FF9800"/>
<path d="M13 12h-2v2h2v-2zm0-4h-2v2h2v-2z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>

After

Width:  |  Height:  |  Size: 258 B

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 2H4C2.9 2 2.01 2.9 2.01 4L2 22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM6 9H18V11H6V9ZM18 7H6V5H18V7ZM17 15H7V13H17V15Z" fill="#2196F3"/>
</svg>

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2196F3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1,12 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" fill="#F5F5F5" stroke="#E0E0E0" stroke-width="2"/>
<path d="M35 35L65 65M65 35L35 65" stroke="#9E9E9E" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M50 25V30" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round"/>
<path d="M50 70V75" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round"/>
<path d="M25 50H30" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round"/>
<path d="M70 50H75" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round"/>
<path d="M37 37L34 34" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round"/>
<path d="M66 66L63 63" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round"/>
<path d="M37 63L34 66" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round"/>
<path d="M66 34L63 37" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 970 B

@ -0,0 +1,11 @@
<svg width="120" height="120" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
<!-- 背景圆 -->
<circle cx="60" cy="60" r="50" fill="#f5f5f5" stroke="#e0e0e0" stroke-width="1"/>
<!-- 邮件图标 -->
<path d="M30,40 L90,40 L90,65 L60,80 L30,65 Z" fill="#cccccc"/>
<path d="M30,65 L90,65 L60,80 Z" fill="#bbbbbb"/>
<!-- 斜杠 -->
<line x1="35" y1="35" x2="85" y2="85" stroke="#ff6b6b" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 464 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
</svg>

After

Width:  |  Height:  |  Size: 494 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2196F3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg>

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2196F3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg>

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.14 12.94c0.04-0.3 0.06-0.61 0.06-0.94 0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14 0.23-0.41 0.12-0.61l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39 0.96c-0.5-0.38-1.03-0.7-1.62-0.94l-0.36-2.54c-0.04-0.24-0.24-0.41-0.48-0.41h-3.84c-0.24 0-0.43 0.17-0.47 0.41l-0.36 2.54c-0.59 0.24-1.13 0.57-1.62 0.94l-2.39-0.96c-0.22-0.08-0.47 0-0.59 0.22L2.74 8.87c-0.12 0.21-0.08 0.47 0.12 0.61l2.03 1.58C4.84 11.36 4.8 11.69 4.8 12s0.02 0.64 0.07 0.94l-2.03 1.58c-0.18 0.14-0.23 0.41-0.12 0.61l1.92 3.32c0.12 0.22 0.37 0.29 0.59 0.22l2.39-0.96c0.5 0.38 1.03 0.7 1.62 0.94l0.36 2.54c0.05 0.24 0.24 0.41 0.48 0.41h3.84c0.24 0 0.44-0.17 0.47-0.41l0.36-2.54c0.59-0.24 1.13-0.56 1.62-0.94l2.39 0.96c0.22 0.08 0.47 0 0.59-0.22l1.92-3.32c0.12-0.22 0.07-0.47-0.12-0.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" fill="#666666"/>
</svg>

After

Width:  |  Height:  |  Size: 984 B

@ -0,0 +1,12 @@
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<!-- 背景圆 -->
<circle cx="20" cy="20" r="18" fill="#f0f0f0" stroke="#e0e0e0" stroke-width="1"/>
<!-- 系统图标主体 -->
<rect x="10" y="15" width="20" height="10" rx="2" fill="#4a90e2"/>
<circle cx="20" cy="20" r="3" fill="#ffffff"/>
<!-- 天线 -->
<line x1="20" y1="5" x2="20" y2="15" stroke="#4a90e2" stroke-width="2" stroke-linecap="round"/>
<line x1="10" y1="10" x2="30" y2="10" stroke="#4a90e2" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 564 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2196F3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h18M3 6h18M3 18h18"></path></svg>

After

Width:  |  Height:  |  Size: 220 B

@ -0,0 +1,75 @@
// pages/about/about.js
Page({
data: {
appInfo: {
name: '失物招领小程序',
version: '1.0.0',
description: '帮助用户寻找丢失物品和归还捡到物品的便捷平台',
developer: '失物招领团队',
contactEmail: 'support@shiwuzhaoling.com',
website: 'https://www.shiwuzhaoling.com',
updateLog: [
'v1.0.0 (2024-01-01): 首次发布,包含失物招领、搜索、匹配等核心功能'
]
}
},
onLoad: function() {
// 页面加载时的初始化操作
},
// 跳转到官方网站
openWebsite: function() {
wx.navigateTo({
url: '/pages/webview/webview?url=' + encodeURIComponent(this.data.appInfo.website)
});
},
// 联系我们
contactUs: function() {
wx.showModal({
title: '联系我们',
content: '您可以通过以下方式联系我们:\n邮箱' + this.data.appInfo.contactEmail,
showCancel: false,
confirmText: '复制邮箱',
success: (res) => {
if (res.confirm) {
wx.setClipboardData({
data: this.data.appInfo.contactEmail,
success: () => {
wx.showToast({
title: '邮箱已复制',
icon: 'success'
});
}
});
}
}
});
},
// 检查更新
checkUpdate: function() {
wx.showLoading({
title: '检查中...',
});
// 模拟检查更新
setTimeout(() => {
wx.hideLoading();
wx.showToast({
title: '已是最新版本',
icon: 'none'
});
}, 1000);
},
// 分享小程序
onShareAppMessage: function() {
return {
title: this.data.appInfo.name,
path: '/pages/index/index',
imageUrl: '/images/share_image.png'
};
}
});

@ -0,0 +1,78 @@
<!-- pages/about/about.wxml -->
<view class="container">
<!-- 顶部标题 -->
<view class="header">
<text class="title">关于我们</text>
</view>
<!-- 应用信息 -->
<view class="app-info">
<image class="app-logo" src="/images/app_logo.png" mode="aspectFit"></image>
<text class="app-name">{{appInfo.name}}</text>
<text class="app-version">版本 {{appInfo.version}}</text>
<text class="app-description">{{appInfo.description}}</text>
</view>
<!-- 功能介绍 -->
<view class="feature-section">
<view class="section-title">功能介绍</view>
<view class="feature-list">
<view class="feature-item">
<image class="feature-icon" src="/images/lost.png" mode="aspectFit"></image>
<text class="feature-text">发布失物信息</text>
</view>
<view class="feature-item">
<image class="feature-icon" src="/images/found.png" mode="aspectFit"></image>
<text class="feature-text">发布招领信息</text>
</view>
<view class="feature-item">
<image class="feature-icon" src="/images/search.png" mode="aspectFit"></image>
<text class="feature-text">搜索物品信息</text>
</view>
<view class="feature-item">
<image class="feature-icon" src="/images/match.svg" mode="aspectFit"></image>
<text class="feature-text">智能匹配物品</text>
</view>
<view class="feature-item">
<image class="feature-icon" src="/images/message.svg" mode="aspectFit"></image>
<text class="feature-text">消息通知提醒</text>
</view>
</view>
</view>
<!-- 联系信息 -->
<view class="contact-section">
<view class="section-title">联系我们</view>
<view class="contact-list">
<view class="contact-item" bindtap="openWebsite">
<image class="contact-icon" src="/images/website.png" mode="aspectFit"></image>
<text class="contact-text">官方网站</text>
<text class="arrow">→</text>
</view>
<view class="contact-item" bindtap="contactUs">
<image class="contact-icon" src="/images/email.png" mode="aspectFit"></image>
<text class="contact-text">{{appInfo.contactEmail}}</text>
<text class="arrow">→</text>
</view>
<view class="contact-item">
<image class="contact-icon" src="/images/developer.png" mode="aspectFit"></image>
<text class="contact-text">{{appInfo.developer}}</text>
</view>
</view>
</view>
<!-- 更新日志 -->
<view class="update-section">
<view class="section-title">更新日志</view>
<view class="update-list">
<view class="update-item" wx:for="{{appInfo.updateLog}}" wx:key="index">
<text class="update-text">{{item}}</text>
</view>
</view>
</view>
<!-- 检查更新 -->
<view class="update-button-section">
<button class="check-update-btn" type="primary" bindtap="checkUpdate">检查更新</button>
</view>
</view>

@ -0,0 +1,174 @@
/* pages/about/about.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 顶部标题 */
.header {
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
/* 应用信息 */
.app-info {
display: flex;
flex-direction: column;
align-items: center;
padding: 50rpx 0;
background-color: #fff;
margin-bottom: 20rpx;
}
.app-logo {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
}
.app-name {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.app-version {
font-size: 24rpx;
color: #999;
margin-bottom: 20rpx;
}
.app-description {
font-size: 28rpx;
color: #666;
text-align: center;
padding: 0 60rpx;
line-height: 40rpx;
}
/* 功能介绍 */
.feature-section {
background-color: #fff;
margin-bottom: 20rpx;
}
.section-title {
padding: 20rpx 30rpx;
font-size: 28rpx;
font-weight: bold;
color: #333;
border-bottom: 1rpx solid #eee;
}
.feature-list {
display: flex;
flex-wrap: wrap;
padding: 30rpx;
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
width: 33.33%;
margin-bottom: 30rpx;
}
.feature-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 10rpx;
}
.feature-text {
font-size: 24rpx;
color: #666;
text-align: center;
}
/* 联系信息 */
.contact-section {
background-color: #fff;
margin-bottom: 20rpx;
}
.contact-list {
padding: 0 30rpx;
}
.contact-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #eee;
}
.contact-item:last-child {
border-bottom: none;
}
.contact-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.contact-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.arrow {
font-size: 28rpx;
color: #ddd;
}
/* 更新日志 */
.update-section {
background-color: #fff;
margin-bottom: 30rpx;
}
.update-list {
padding: 30rpx;
}
.update-item {
margin-bottom: 20rpx;
}
.update-item:last-child {
margin-bottom: 0;
}
.update-text {
font-size: 28rpx;
color: #666;
line-height: 44rpx;
}
/* 检查更新按钮 */
.update-button-section {
padding: 0 30rpx 30rpx;
}
.check-update-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
font-size: 28rpx;
background-color: #2196F3;
color: #fff;
border-radius: 8rpx;
}

@ -0,0 +1,87 @@
// pages/agreement/agreement.js
Page({
data: {
agreementContent: ''
},
onLoad: function() {
// 加载用户协议内容
this.loadUserAgreement();
},
// 加载用户协议内容
loadUserAgreement: function() {
// 在实际项目中,这里应该从服务器获取用户协议内容
// 为了演示,这里使用静态内容
const agreementContent = `失物招领小程序用户协议
更新日期2024年1月1日
欢迎使用失物招领小程序以下简称"我们"请您仔细阅读以下条款使用我们的服务即表示您同意遵守本协议的所有规定
服务内容
失物招领小程序是一个提供失物发布招领发布物品搜索智能匹配消息通知等功能的平台旨在帮助用户寻找丢失的物品或归还捡到的物品
用户注册与登录
2.1 您需要使用微信账号登录我们的服务登录后您将获得一个唯一的用户ID
2.2 您应当保证提供的个人信息真实准确完整并在信息发生变更时及时更新
2.3 您应当妥善保管您的账号和密码因账号或密码保管不善造成的损失由您自行承担
用户行为规范
3.1 您在使用我们的服务时应当遵守中华人民共和国法律法规和社会公德
3.2 您不得发布以下内容
- 违反法律法规的内容
- 涉及色情暴力恐怖赌博等违法或不良信息
- 侵犯他人知识产权隐私权等合法权益的内容
- 虚假欺诈或误导性的内容
- 其他我们认为不适当的内容
3.3 您不得利用我们的服务从事以下活动
- 干扰我们服务的正常运行
- 窃取其他用户的信息
- 传播计算机病毒或恶意程序
- 其他违法或损害我们利益的活动
用户发布的信息
4.1 您发布的失物或招领信息应当真实准确完整
4.2 您应对您发布的信息承担全部责任因您发布的信息造成的纠纷或损失由您自行承担
4.3 我们有权对您发布的信息进行审核并在发现违规内容时删除或修改相关信息
知识产权
5.1 我们拥有本服务的全部知识产权包括但不限于软件文字图片音频视频等
5.2 未经我们授权您不得复制修改传播或用于其他商业目的
免责声明
6.1 我们不对用户发布信息的真实性准确性完整性负责用户应当自行核实信息
6.2 我们不对用户之间因使用本服务而产生的纠纷或损失负责
6.3 因不可抗力或第三方原因导致的服务中断或数据丢失我们不承担责任
服务变更中断或终止
7.1 我们有权根据业务需要变更中断或终止部分或全部服务
7.2 如您违反本协议我们有权暂停或终止向您提供服务
协议的修改
我们有权随时修改本协议并在小程序内公布修改后的协议您继续使用我们的服务即表示您接受修改后的协议
法律适用与争议解决
本协议的解释效力及纠纷的解决适用中华人民共和国法律如就本协议发生任何争议双方应友好协商解决协商不成的任何一方均有权向有管辖权的人民法院提起诉讼
其他条款
如本协议的任何条款被视为无效或不可执行不影响其他条款的效力
感谢您使用失物招领小程序`;
this.setData({
agreementContent: agreementContent
});
}
});

@ -0,0 +1,16 @@
<!-- pages/agreement/agreement.wxml -->
<view class="container">
<!-- 顶部标题 -->
<view class="header">
<text class="title">用户协议</text>
</view>
<!-- 用户协议内容 -->
<scroll-view class="content" scroll-y>
<view class="agreement-content">
<text class="content-text">
{{agreementContent}}
</text>
</view>
</scroll-view>
</view>

@ -0,0 +1,46 @@
/* pages/agreement/agreement.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 顶部标题 */
.header {
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
/* 内容区域 */
.content {
flex: 1;
padding: 30rpx;
}
.agreement-content {
background-color: #fff;
padding: 40rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.content-text {
font-size: 28rpx;
line-height: 48rpx;
color: #333;
white-space: pre-line;
}
/* 添加段落间距 */
.content-text text {
display: block;
margin-bottom: 20rpx;
}

@ -0,0 +1,225 @@
// pages/claim/claim.js
Page({
data: {
claimRequests: [], // 认领请求列表
loading: true, // 是否正在加载
hasMore: true, // 是否还有更多请求
pageNum: 1, // 当前页码
itemType: 'all' // 物品类型,'all'全部,'lost'失物,'found'招领
},
onLoad: function() {
// 检查用户是否已登录
this.checkLoginStatus();
// 加载认领请求列表
this.loadClaimRequests();
},
// 检查登录状态
checkLoginStatus: function() {
const app = getApp();
if (!app.globalData.hasUserInfo) {
// 用户未登录,跳转到登录页面
wx.navigateTo({
url: '/pages/login/login'
});
}
},
// 切换物品类型
switchItemType: function(e) {
const type = e.currentTarget.dataset.type;
this.setData({
itemType: type,
pageNum: 1,
hasMore: true
});
this.loadClaimRequests();
},
// 加载认领请求列表
loadClaimRequests: function() {
if (this.data.loading || !this.data.hasMore) return;
this.setData({ loading: true });
const app = getApp();
wx.request({
url: app.globalData.baseUrl + '/claim/requests',
data: {
type: this.data.itemType,
pageNum: this.data.pageNum,
pageSize: 10
},
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
success: (res) => {
if (res.data && res.data.requests) {
const newRequests = res.data.requests;
// 如果是第一页,直接替换数据;否则追加数据
if (this.data.pageNum === 1) {
this.setData({
claimRequests: newRequests,
pageNum: 2,
hasMore: newRequests.length === 10
});
} else {
this.setData({
claimRequests: [...this.data.claimRequests, ...newRequests],
pageNum: this.data.pageNum + 1,
hasMore: newRequests.length === 10
});
}
}
},
fail: (err) => {
console.error('加载认领请求失败', err);
wx.showToast({
title: '加载失败,请重试',
icon: 'none'
});
},
complete: () => {
this.setData({ loading: false });
}
});
},
// 加载更多认领请求
onReachBottom: function() {
this.loadClaimRequests();
},
// 下拉刷新
onPullDownRefresh: function() {
this.setData({
pageNum: 1,
hasMore: true
});
this.loadClaimRequests();
},
// 跳转到物品详情页
goToDetail: function(e) {
const itemId = e.currentTarget.dataset.id;
const type = e.currentTarget.dataset.type;
wx.navigateTo({
url: `/pages/detail/detail?id=${itemId}&type=${type}`
});
},
// 查看认领人信息
viewClaimUserInfo: function(e) {
const { userId } = e.currentTarget.dataset;
const app = getApp();
wx.request({
url: app.globalData.baseUrl + `/users/${userId}`,
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
success: (res) => {
if (res.data && res.data.user) {
const userInfo = res.data.user;
wx.showModal({
title: '认领人信息',
content: `姓名:${userInfo.nickName}\n联系方式:${userInfo.phone}`,
showCancel: false
});
}
},
fail: (err) => {
console.error('获取用户信息失败', err);
wx.showToast({
title: '获取信息失败',
icon: 'none'
});
}
});
},
// 同意认领
approveClaim: function(e) {
const { id, itemId, itemType } = e.currentTarget.dataset;
wx.showModal({
title: '确认同意认领',
content: '同意认领后,物品将标记为已认领,您可以与认领人联系确认物品归属。',
success: (res) => {
if (res.confirm) {
this.submitClaimApproval(id, itemId, itemType, 'approve');
}
}
});
},
// 拒绝认领
rejectClaim: function(e) {
const { id, itemId, itemType } = e.currentTarget.dataset;
wx.showModal({
title: '确认拒绝认领',
content: '拒绝认领后,该认领请求将被驳回,您可以继续等待其他认领请求。',
success: (res) => {
if (res.confirm) {
this.submitClaimApproval(id, itemId, itemType, 'reject');
}
}
});
},
// 提交认领处理结果
submitClaimApproval: function(claimId, itemId, itemType, action) {
wx.showLoading({
title: '处理中...',
});
const app = getApp();
wx.request({
url: app.globalData.baseUrl + `/claim/${claimId}/${action}`,
method: 'POST',
data: {
itemId: itemId,
itemType: itemType
},
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
success: (res) => {
if (res.data && res.data.success) {
// 从列表中移除该认领请求
const newClaimRequests = this.data.claimRequests.filter(request => request.id !== claimId);
this.setData({
claimRequests: newClaimRequests
});
wx.showToast({
title: action === 'approve' ? '已同意认领' : '已拒绝认领',
icon: 'success'
});
} else {
wx.showToast({
title: '处理失败,请重试',
icon: 'none'
});
}
},
fail: (err) => {
console.error('处理认领请求失败', err);
wx.showToast({
title: '网络错误,请重试',
icon: 'none'
});
},
complete: () => {
wx.hideLoading();
}
});
}
});

@ -0,0 +1,71 @@
<!-- pages/claim/claim.wxml -->
<view class="container">
<!-- 顶部标题 -->
<view class="header">
<text class="title">认领管理</text>
</view>
<!-- 物品类型切换 -->
<view class="item-type">
<view class="type-tab {{itemType === 'all' ? 'active' : ''}}" data-type="all" bindtap="switchItemType">
<text>全部</text>
</view>
<view class="type-tab {{itemType === 'lost' ? 'active' : ''}}" data-type="lost" bindtap="switchItemType">
<text>失物</text>
</view>
<view class="type-tab {{itemType === 'found' ? 'active' : ''}}" data-type="found" bindtap="switchItemType">
<text>招领</text>
</view>
</view>
<!-- 认领请求列表 -->
<scroll-view class="claim-list" scroll-y bindscrolltolower="onReachBottom" enablePullDownRefresh="true" bindrefresh="onPullDownRefresh">
<view class="claim-item" wx:for="{{claimRequests}}" wx:key="id">
<!-- 认领请求基本信息 -->
<view class="claim-header">
<view class="claim-info">
<text class="claim-title">来自 {{item.claimerName}} 的认领请求</text>
<text class="claim-time">{{item.createTime}}</text>
</view>
<text class="claim-status" wx:if="{{item.status === 'pending'}}">待处理</text>
<text class="claim-status approved" wx:elif="{{item.status === 'approved'}}">已同意</text>
<text class="claim-status rejected" wx:else>已拒绝</text>
</view>
<!-- 相关物品信息 -->
<view class="related-item" data-id="{{item.itemId}}" data-type="{{item.itemType}}" bindtap="goToDetail">
<image class="item-image" wx:if="{{item.itemImages && item.itemImages.length > 0}}" src="{{item.itemImages[0]}}" mode="aspectFill"></image>
<view class="item-info">
<text class="item-title">{{item.itemTitle}}</text>
<text class="item-desc">{{item.itemDesc}}</text>
<text class="item-type">{{item.itemType === 'lost' ? '失物' : '招领'}}</text>
</view>
</view>
<!-- 认领理由 -->
<view class="claim-reason">
<text class="reason-label">认领理由:</text>
<text class="reason-content">{{item.reason}}</text>
</view>
<!-- 操作按钮 -->
<view class="action-buttons" wx:if="{{item.status === 'pending'}}">
<button class="view-user-btn" type="default" bindtap="viewClaimUserInfo" data-user-id="{{item.claimerId}}">查看认领人信息</button>
<button class="approve-btn" type="primary" bindtap="approveClaim" data-id="{{item.id}}" data-item-id="{{item.itemId}}" data-item-type="{{item.itemType}}">同意认领</button>
<button class="reject-btn" type="default" bindtap="rejectClaim" data-id="{{item.id}}" data-item-id="{{item.itemId}}" data-item-type="{{item.itemType}}">拒绝认领</button>
</view>
</view>
<!-- 空状态 -->
<view class="empty" wx:if="{{claimRequests.length === 0 && !loading}}">
<image class="empty-icon" src="/images/no_claims.png" mode="aspectFit"></image>
<text class="empty-text">暂无认领请求</text>
<text class="empty-subtext">用户提交认领请求后,将显示在此处</text>
</view>
<!-- 加载中 -->
<view class="loading" wx:if="{{loading && claimRequests.length > 0}}">
<text>加载中...</text>
</view>
</scroll-view>
</view>

@ -0,0 +1,254 @@
/* pages/claim/claim.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 顶部标题 */
.header {
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
/* 物品类型切换 */
.item-type {
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.type-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
}
.type-tab.active {
color: #2196F3;
}
.type-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #2196F3;
}
/* 认领请求列表 */
.claim-list {
flex: 1;
padding: 20rpx;
}
/* 认领请求项 */
.claim-item {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 认领请求头部 */
.claim-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background-color: #f9f9f9;
border-bottom: 1rpx solid #eee;
}
.claim-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.claim-title {
font-size: 28rpx;
color: #333;
}
.claim-time {
font-size: 24rpx;
color: #999;
}
.claim-status {
padding: 8rpx 20rpx;
font-size: 24rpx;
background-color: #ff9800;
color: #fff;
border-radius: 20rpx;
}
.claim-status.approved {
background-color: #4caf50;
}
.claim-status.rejected {
background-color: #f44336;
}
/* 相关物品信息 */
.related-item {
display: flex;
padding: 24rpx;
}
.item-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
margin-right: 20rpx;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-desc {
font-size: 28rpx;
color: #666;
line-height: 40rpx;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-type {
font-size: 24rpx;
color: #fff;
background-color: #2196F3;
padding: 4rpx 16rpx;
border-radius: 16rpx;
align-self: flex-start;
}
/* 认领理由 */
.claim-reason {
padding: 24rpx;
border-top: 1rpx solid #eee;
}
.reason-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-right: 10rpx;
}
.reason-content {
font-size: 28rpx;
color: #666;
line-height: 44rpx;
}
/* 操作按钮 */
.action-buttons {
display: flex;
flex-direction: column;
padding: 24rpx;
border-top: 1rpx solid #eee;
gap: 16rpx;
}
.view-user-btn,
.approve-btn,
.reject-btn {
height: 80rpx;
font-size: 28rpx;
line-height: 80rpx;
border-radius: 8rpx;
}
.view-user-btn {
background-color: #fff;
color: #666;
border: 1rpx solid #ddd;
}
.approve-btn {
background-color: #4caf50;
color: #fff;
}
.reject-btn {
background-color: #f44336;
color: #fff;
border: 1rpx solid #f44336;
}
/* 空状态 */
.empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 60vh;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.empty-subtext {
font-size: 24rpx;
color: #999;
text-align: center;
padding: 0 60rpx;
}
/* 加载中 */
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx 0;
}
.loading text {
font-size: 28rpx;
color: #999;
}

@ -0,0 +1,920 @@
// pages/detail/detail.js
Page({
data: {
itemId: '', // 物品ID
itemType: '', // 物品类型,'lost'表示失物,'found'表示招领
itemDetail: null, // 物品详细信息
loading: true, // 是否正在加载
currentImageIndex: 0, // 当前显示的图片索引
isAuthor: false, // 是否是发布者本人
claimStatus: '' // 认领状态
},
onLoad: function(options) {
// 获取物品ID和类型
if (options.id && options.type) {
this.setData({
itemId: options.id,
itemType: options.type,
// 从URL参数获取是否是从通知页面进入的标志
fromNotification: options.fromNotification === 'true'
});
// 加载物品详情
this.loadItemDetail();
} else {
wx.showToast({
title: '参数错误',
icon: 'none',
complete: () => {
wx.navigateBack();
}
});
}
},
// 加载物品详情
loadItemDetail: function() {
const app = getApp();
console.log('========== 开始查找物品详情 ==========');
console.log('查找参数 - itemId:', this.data.itemId, 'itemType:', this.data.itemType);
// 首先尝试从云数据库获取(跨设备数据共享的关键)
this.tryLoadFromCloud(() => {
// 如果云数据库获取失败,再尝试从本地内存查找
this.loadFromMemory();
});
},
// 尝试从云数据库加载物品详情
tryLoadFromCloud: function(onFail) {
const app = getApp();
// 检查云数据库是否可用
if (app.globalData.cloudEnvValidated === false && app.globalData.db === null) {
console.log('云开发环境已确认为无效,跳过云数据库查询');
if (onFail) onFail();
return;
}
const db = app.globalData.db;
if (!db || !wx.cloud) {
console.log('云数据库不可用,跳过云数据库查询');
if (onFail) onFail();
return;
}
console.log('尝试从云数据库获取物品详情...');
// 从云数据库查询物品
db.collection('items').doc(this.data.itemId).get({
success: (res) => {
console.log('✅ 从云数据库获取物品详情成功');
console.log('物品数据:', res.data);
if (res.data) {
let itemDetail = {
id: res.data._id || this.data.itemId,
...res.data,
isUserPublished: res.data._openid === (wx.getStorageSync('openid') || 'local_user')
};
console.log('从云数据库获取的原始数据:', JSON.stringify(itemDetail, null, 2));
console.log('图片字段:', itemDetail.images, '类型:', typeof itemDetail.images, '是否为数组:', Array.isArray(itemDetail.images));
// 处理物品详情
if (app.processItemImages) {
itemDetail = app.processItemImages(itemDetail);
console.log('经过processItemImages处理后的图片:', itemDetail.images);
}
itemDetail = this.completeItemDetail(itemDetail);
console.log('最终处理后的itemDetail:', JSON.stringify(itemDetail, null, 2));
console.log('最终图片数组:', itemDetail.images, '数量:', itemDetail.images ? itemDetail.images.length : 0);
// 检查是否是发布者本人
const currentOpenId = wx.getStorageSync('openid') || 'local_user';
let isAuthor = itemDetail.isUserPublished === true ||
(itemDetail._openid && itemDetail._openid === currentOpenId);
// 转换云存储路径为临时URL异步
this.convertCloudImages(itemDetail.images || [], (convertedImages) => {
itemDetail.images = convertedImages;
this.setData({
itemDetail: itemDetail,
isAuthor: isAuthor,
claimStatus: itemDetail.claimStatus || 'none',
loading: false
});
// 确保图片数组存在且不为空
if (!itemDetail.images || itemDetail.images.length === 0) {
console.warn('⚠️ 物品没有图片,添加默认图片');
const defaultImage = itemDetail.type === 'lost' ? '/images/lost.png' : '/images/found.png';
this.setData({
'itemDetail.images': [defaultImage]
});
}
console.log('✅ 最终设置到data的图片数组:', this.data.itemDetail.images);
});
// 同步到本地内存,以便后续查找
this.syncToMemory(itemDetail);
} else {
console.log('云数据库返回空数据');
if (onFail) onFail();
}
},
fail: (err) => {
console.log('从云数据库获取物品详情失败:', err.errMsg || err);
// 检查是否是环境不存在的错误
const isEnvError = err.errCode === -501000 || (err.errMsg && err.errMsg.includes('env not exists'));
if (isEnvError) {
console.warn('云开发环境不存在,标记为无效');
app.globalData.db = null;
app.globalData.cloudEnvValidated = false;
}
// 尝试按ID查询如果doc查询失败
this.tryQueryByField(onFail);
}
});
},
// 尝试按字段查询如果doc查询失败
tryQueryByField: function(onFail) {
const app = getApp();
const db = app.globalData.db;
if (!db) {
if (onFail) onFail();
return;
}
console.log('尝试按字段查询物品...');
// 尝试按_id字段查询
db.collection('items').where({
_id: this.data.itemId
}).get({
success: (res) => {
if (res.data && res.data.length > 0) {
console.log('✅ 按字段查询成功,找到物品');
let itemDetail = {
id: res.data[0]._id || this.data.itemId,
...res.data[0],
isUserPublished: res.data[0]._openid === (wx.getStorageSync('openid') || 'local_user')
};
if (app.processItemImages) {
itemDetail = app.processItemImages(itemDetail);
}
itemDetail = this.completeItemDetail(itemDetail);
const currentOpenId = wx.getStorageSync('openid') || 'local_user';
let isAuthor = itemDetail.isUserPublished === true ||
(itemDetail._openid && itemDetail._openid === currentOpenId);
this.setData({
itemDetail: itemDetail,
isAuthor: isAuthor,
claimStatus: itemDetail.claimStatus || 'none',
loading: false
});
// 确保图片数组存在且不为空
if (!itemDetail.images || itemDetail.images.length === 0) {
console.warn('⚠️ 物品没有图片,添加默认图片');
const defaultImage = itemDetail.type === 'lost' ? '/images/lost.png' : '/images/found.png';
this.setData({
'itemDetail.images': [defaultImage]
});
}
console.log('✅ 最终设置到data的图片数组:', this.data.itemDetail.images);
this.syncToMemory(itemDetail);
} else {
console.log('按字段查询未找到物品');
if (onFail) onFail();
}
},
fail: (err) => {
console.log('按字段查询失败:', err.errMsg || err);
if (onFail) onFail();
}
});
},
// 从本地内存查找(降级方案)
loadFromMemory: function() {
const app = getApp();
// 模拟网络延迟
setTimeout(() => {
try {
let itemDetail = null;
console.log('========== 从本地内存查找物品详情 ==========');
// 1. 首先从用户自己发布的物品列表中查找需要匹配ID和类型
const userItems = app.globalData.userPublishedItems || [];
console.log('从用户物品列表查找,总数:', userItems.length);
userItems.forEach((item, index) => {
console.log(`用户物品 ${index}:`, item.id, item.type, item.title);
if (item.id === this.data.itemId && item.type === this.data.itemType) {
itemDetail = item;
console.log('✅ 从用户物品列表找到物品详情:', item.title);
}
});
// 2. 如果找不到,再从所有用户发布的物品列表中查找
if (!itemDetail) {
const allItems = app.globalData.allPublishedItems || [];
console.log('从全局物品列表查找,总数:', allItems.length);
console.log('全局物品列表内容:');
allItems.forEach((item, index) => {
console.log(`全局物品 ${index}:`, {
id: item.id,
type: item.type,
title: item.title,
isUserPublished: item.isUserPublished
});
// 检查ID是否匹配忽略类型先尝试匹配
if (item.id === this.data.itemId) {
console.log(` → ID匹配类型: ${item.type}, 期望类型: ${this.data.itemType}`);
if (item.type === this.data.itemType) {
itemDetail = item;
console.log('✅ 从全局物品列表找到物品详情ID和类型都匹配:', item.title);
} else {
console.log(` ⚠️ ID匹配但类型不匹配跳过`);
}
}
});
// 如果还没找到尝试只按ID匹配类型可能不同
if (!itemDetail) {
console.log('按ID和类型匹配失败尝试只按ID匹配...');
const matchedById = allItems.find(item => item.id === this.data.itemId);
if (matchedById) {
console.log('⚠️ 找到匹配ID的物品但类型不匹配:', matchedById.type, '期望:', this.data.itemType);
// 如果类型不匹配,仍然使用找到的物品,但记录警告
itemDetail = matchedById;
console.log('✅ 使用找到的物品(类型可能不准确):', matchedById.title);
}
}
}
// 3. 只有在所有列表都找不到时,才生成模拟数据
if (!itemDetail) {
console.log('⚠️ 未找到对应的物品信息,使用模拟数据');
console.log('全局数据状态:');
console.log(' - userPublishedItems:', app.globalData.userPublishedItems?.length || 0);
console.log(' - allPublishedItems:', app.globalData.allPublishedItems?.length || 0);
console.log('⚠️ 提示:如果这是跨设备访问,请确保云数据库可用');
itemDetail = this.generateMockItemDetail();
} else {
console.log('========== 成功找到物品信息 ==========');
console.log('物品标题:', itemDetail.title);
console.log('物品类型:', itemDetail.type);
console.log('物品ID:', itemDetail.id);
}
// 确保物品详情包含所有必要的字段
itemDetail = this.completeItemDetail(itemDetail);
// 检查是否是发布者本人
let isAuthor = itemDetail.isUserPublished === true;
// 转换云存储路径为临时URL异步
this.convertCloudImages(itemDetail.images || [], (convertedImages) => {
itemDetail.images = convertedImages;
// 增强判断添加对_openid的检查以更准确判断是否为作者
const currentOpenId = wx.getStorageSync('openid') || 'local_user';
let finalIsAuthor = isAuthor || (itemDetail._openid && itemDetail._openid === currentOpenId);
// 特别处理:从通知页面进入时,对于失物信息,应该检查当前用户是否是失主
if (this.data.fromNotification && this.data.itemType === 'lost') {
// 在实际应用中这里应该通过用户ID判断是否是失主
// 在模拟环境中,我们假设从通知进入的用户就是失主
finalIsAuthor = true;
}
// 开发环境模拟如果URL中带有author=true参数设置为发布者身份
if (getApp().globalData && getApp().globalData.isDebug && this.data.itemId && this.data.fromNotification) {
// 确保从通知进入的用户(失主)不会看到认领按钮
finalIsAuthor = true;
}
this.setData({
itemDetail: itemDetail,
isAuthor: finalIsAuthor,
claimStatus: itemDetail.claimStatus || 'none',
loading: false
});
// 确保图片数组存在且不为空
if (!itemDetail.images || itemDetail.images.length === 0) {
console.warn('⚠️ 物品没有图片,添加默认图片');
const defaultImage = itemDetail.type === 'lost' ? '/images/lost.png' : '/images/found.png';
this.setData({
'itemDetail.images': [defaultImage]
});
}
});
console.log('✅ 最终设置到data的图片数组:', this.data.itemDetail.images);
} catch (err) {
console.error('获取物品详情失败', err);
wx.showToast({
title: '加载详情失败',
icon: 'none',
complete: () => {
wx.navigateBack();
}
});
this.setData({ loading: false });
}
}, 500);
},
// 同步物品到本地内存(用于后续查找)
syncToMemory: function(itemDetail) {
const app = getApp();
if (!app.globalData.allPublishedItems) {
app.globalData.allPublishedItems = [];
}
// 检查是否已存在
const exists = app.globalData.allPublishedItems.some(item => item.id === itemDetail.id);
if (!exists) {
app.globalData.allPublishedItems.push(itemDetail);
console.log('已同步物品到本地内存');
}
},
// 生成模拟物品详情
generateMockItemDetail: function() {
const isLost = this.data.itemType === 'lost';
const now = new Date();
const mockImages = [
'/images/lost.png',
'/images/found.png',
'/images/match.svg'
];
return {
id: this.data.itemId,
type: this.data.itemType,
title: isLost ? '模拟失物标题' : '模拟招领标题',
description: isLost ? '这是一个失物信息的详细描述。包含了物品的特征、丢失的具体情况等详细信息。请有相关线索的人与我联系。' : '这是一个招领信息的详细描述。我在某处捡到了这个物品,请失主看到信息后与我联系,并提供物品的详细特征以确认身份。',
time: now.toISOString().split('T')[0],
location: '学校内',
contactName: '张同学',
contactPhone: '13800138000',
publishTime: now.toISOString(),
images: [mockImages[Math.floor(Math.random() * mockImages.length)]],
isUserPublished: false,
claimStatus: 'none'
};
},
// 确保物品详情包含所有必要的字段
completeItemDetail: function(itemDetail) {
const now = new Date();
const app = getApp();
// 创建深拷贝以避免修改原始对象
let processedItem = JSON.parse(JSON.stringify(itemDetail));
// 使用app.processItemImages函数处理图片路径确保正确格式
if (app && app.processItemImages) {
processedItem = app.processItemImages(processedItem);
console.log('使用app.processItemImages处理图片路径');
}
// 独立的图片路径最终处理,记录详细日志
let processedImages = [];
console.log('原始图片列表:', JSON.stringify(processedItem.images));
// 确保images字段存在且为数组
if (processedItem.images && Array.isArray(processedItem.images)) {
processedImages = processedItem.images.map(img => {
try {
console.log('处理单张图片:', img, '类型:', typeof img);
if (typeof img === 'string') {
// 最小化清理:只移除明显的引号和空白字符
let cleanPath = img.trim();
cleanPath = cleanPath.replace(/^["'`]+|["'`]+$/g, '');
console.log('清理后路径:', cleanPath);
// 确保路径不为空
if (!cleanPath) {
console.warn('清理后图片路径为空,跳过(不在最后添加默认图片)');
return null; // 返回null稍后过滤掉
}
// 验证路径格式(允许各种格式)
const isValidPath = cleanPath.startsWith('wxfile://') ||
cleanPath.startsWith('http://') ||
cleanPath.startsWith('https://') ||
cleanPath.startsWith('cloud://') ||
cleanPath.startsWith('/');
if (!isValidPath) {
console.warn('图片路径格式无效:', cleanPath);
// 仍然尝试使用,因为可能是相对路径
}
// 对于cloud://路径,直接返回,不需要任何处理
if (cleanPath.startsWith('cloud://')) {
console.log('detail.js - 检测到cloud://路径,直接返回:', cleanPath);
return cleanPath;
}
// 针对local_image_类型的路径实现与app.js相同的优化逻辑
if (cleanPath.includes('local_image_')) {
const app = getApp();
console.log('detail.js - 处理local_image_类型图片原始路径:', cleanPath);
// 提取实际的图片ID部分
let imageId = cleanPath.match(/local_image_[0-9]+_[0-9]+/);
imageId = imageId ? imageId[0] : cleanPath.split('/').pop().replace(/[`'"\s]/g, '');
console.log('detail.js - 提取的图片ID:', imageId);
// 尝试多种可能的键名来查找图片
const possibleKeys = [
imageId, // 直接使用提取的ID
cleanPath, // 使用完整路径作为键
cleanPath.replace(/^\/pages\/publish\//, ''), // 移除/pages/publish/前缀
cleanPath.replace(/^[\/\\]?/, '') // 移除开头的斜杠
];
// 遍历所有可能的键名
let actualPath = null;
if (app && app.globalData && app.globalData.localImages) {
for (const key of possibleKeys) {
if (app.globalData.localImages[key]) {
actualPath = app.globalData.localImages[key];
console.log('detail.js - 找到匹配的图片路径,键名:', key, '->', actualPath);
break;
}
}
}
// 如果找到实际路径,返回它
if (actualPath && typeof actualPath === 'string') {
// 验证路径是否有效
if (actualPath.startsWith('wxfile://') || actualPath.startsWith('http://') || actualPath.startsWith('https://') || actualPath.startsWith('/')) {
return actualPath;
} else {
console.warn('detail.js - 找到的路径格式无效,跳过:', actualPath);
return null;
}
} else {
// 如果找不到映射,说明这是旧的无效路径,跳过
console.warn('detail.js - 未找到local_image_路径映射跳过。路径:', cleanPath);
return null;
}
}
// 对于其他类型的路径,直接返回清理后的路径
console.log('detail.js - 直接返回路径:', cleanPath);
return cleanPath;
} else {
console.warn('非字符串类型图片,跳过');
return null;
}
} catch (error) {
console.error('处理图片路径时出错:', error);
return null;
}
}).filter(img => img !== null && img !== undefined); // 过滤掉null和undefined
}
console.log('处理后的图片数组(过滤后):', processedImages);
// 如果没有有效图片或处理后为空,添加默认图片
if (!processedImages || processedImages.length === 0) {
console.log('⚠️ 没有有效的图片路径,添加默认图片');
const defaultImage = processedItem.type === 'lost' ? '/images/lost.png' : '/images/found.png';
processedImages = [defaultImage];
console.log('添加默认图片:', defaultImage);
}
// 最终设置处理后的图片数组
processedItem.images = processedImages;
console.log('最终处理后的图片路径:', JSON.stringify(processedItem.images));
console.log('最终图片数组长度:', processedItem.images ? processedItem.images.length : 0);
// 双重检查:确保最终有图片
if (!processedItem.images || processedItem.images.length === 0 || processedItem.images.every(img => !img)) {
console.warn('⚠️ 双重检查:仍然没有图片,强制添加默认图片');
const defaultImage = processedItem.type === 'lost' ? '/images/lost.png' : '/images/found.png';
processedItem.images = [defaultImage];
console.log('强制添加默认图片:', defaultImage);
}
// 返回完整的物品详情,确保所有必要字段都存在
return {
id: processedItem.id || this.data.itemId,
type: processedItem.type || this.data.itemType,
title: processedItem.title || '未命名物品',
description: processedItem.description || '暂无详细描述',
time: processedItem.time || processedItem.date || now.toISOString().split('T')[0],
location: processedItem.location || '未知地点',
contactName: processedItem.contactName || '联系人',
contactPhone: processedItem.contactPhone || '13800138000',
publishTime: processedItem.publishTime || now.toISOString(),
images: processedItem.images || [processedItem.type === 'lost' ? '/images/lost.png' : '/images/found.png'], // 确保至少有默认图片
isUserPublished: processedItem.isUserPublished || false,
claimStatus: processedItem.claimStatus || 'none'
};
},
// 图片切换事件
onImageChange: function(e) {
this.setData({
currentImageIndex: e.detail.current
});
},
// 批量转换云存储路径为临时URL
convertCloudImages: function(images, callback) {
if (!images || !Array.isArray(images) || images.length === 0) {
callback(images || []);
return;
}
const app = getApp();
const cloudPaths = images.filter(img => img && img.startsWith('cloud://'));
// 如果没有云存储路径,直接返回
if (cloudPaths.length === 0) {
callback(images);
return;
}
console.log('发现', cloudPaths.length, '个云存储路径需要转换');
// 批量转换
if (wx.cloud && typeof wx.cloud.getTempFileURL === 'function') {
wx.cloud.getTempFileURL({
fileList: cloudPaths,
success: (res) => {
const urlMap = {};
if (res.fileList) {
res.fileList.forEach((file, index) => {
if (file.tempFileURL) {
urlMap[file.fileID] = file.tempFileURL;
// 更新app.js中的缓存
if (app.cloudUrlCache) {
app.cloudUrlCache[file.fileID] = file.tempFileURL;
}
}
});
}
// 替换图片数组中的cloud://路径
const convertedImages = images.map(img => {
if (img && img.startsWith('cloud://')) {
const convertedUrl = urlMap[img];
if (convertedUrl) {
console.log('云存储路径转换成功:', img, '->', convertedUrl);
return convertedUrl;
} else {
console.warn('云存储路径转换失败未找到对应URL:', img);
// 转换失败时尝试使用app.js的转换函数
return img; // 先返回原路径,让错误处理机制处理
}
}
return img;
});
callback(convertedImages);
},
fail: (err) => {
console.error('批量转换云存储路径失败:', err);
// 转换失败时,逐个尝试转换
this.convertCloudImagesOneByOne(images, callback);
}
});
} else {
console.warn('不支持云开发API无法转换cloud://路径');
// 不支持时,逐个尝试转换
this.convertCloudImagesOneByOne(images, callback);
}
},
// 逐个转换云存储路径(降级方案)
convertCloudImagesOneByOne: function(images, callback) {
const app = getApp();
const convertedImages = [];
let completed = 0;
images.forEach((img, index) => {
if (img && img.startsWith('cloud://')) {
if (app.convertCloudPathToUrl) {
app.convertCloudPathToUrl(img, (convertedUrl) => {
convertedImages[index] = convertedUrl;
completed++;
if (completed === images.filter(i => i && i.startsWith('cloud://')).length) {
// 填充非cloud://路径
images.forEach((origImg, origIndex) => {
if (!origImg || !origImg.startsWith('cloud://')) {
convertedImages[origIndex] = origImg;
}
});
callback(convertedImages.filter(Boolean));
}
});
} else {
convertedImages[index] = img; // 无法转换,保留原路径
completed++;
}
} else {
convertedImages[index] = img;
}
});
// 如果所有图片都不是cloud://路径,直接返回
if (completed === 0) {
callback(images);
}
},
// 图片加载错误处理
onImageError: function(e) {
const index = e.currentTarget.dataset.index;
const images = this.data.itemDetail.images || [];
console.error('图片加载失败,索引:', index, '路径:', images[index]);
// 如果失败的是cloud://路径,尝试重新转换
const failedPath = images[index];
if (failedPath && failedPath.startsWith('cloud://')) {
const app = getApp();
if (app.convertCloudPathToUrl) {
app.convertCloudPathToUrl(failedPath, (convertedUrl) => {
const newImages = [...images];
newImages[index] = convertedUrl;
this.setData({
'itemDetail.images': newImages
});
console.log('重新转换云存储路径成功:', convertedUrl);
});
return;
}
}
// 如果图片加载失败,使用默认图片替换
const defaultImage = this.data.itemType === 'lost' ? '/images/lost.png' : '/images/found.png';
const newImages = [...images];
newImages[index] = defaultImage;
this.setData({
'itemDetail.images': newImages
});
console.log('已替换为默认图片:', defaultImage);
},
// 预览图片
previewImage: function(e) {
const { index } = e.currentTarget.dataset;
const images = this.data.itemDetail.images || [];
console.log('预览图片,索引:', index, '图片列表:', images);
// 确保是有效的图片地址
if (images.length > 0 && index < images.length && typeof images[index] === 'string') {
wx.previewImage({
current: images[index],
urls: images,
success: function() {
console.log('图片预览成功');
},
fail: function(error) {
console.error('图片预览失败:', error);
wx.showToast({
title: '预览图片失败',
icon: 'none'
});
}
});
} else {
wx.showToast({
title: '无效的图片',
icon: 'none'
});
}
},
// 拨打电话
makePhoneCall: function() {
const phoneNumber = this.data.itemDetail.contactPhone;
wx.makePhoneCall({
phoneNumber: phoneNumber,
fail: (err) => {
console.error('拨打电话失败', err);
}
});
},
// 查看位置
viewLocation: function() {
const { location } = this.data.itemDetail;
wx.chooseLocation({
complete: () => {
// 这里可以添加显示地图的逻辑
wx.showToast({
title: `位置:${location}`,
icon: 'none'
});
}
});
},
// 认领物品
claimItem: function() {
wx.showModal({
title: '确认删除',
content: '确定要删除这条信息吗?删除后将无法恢复。',
success: (res) => {
if (res.confirm) {
this.submitDelete();
}
}
});
},
// 提交认领请求
submitClaim: function() {
wx.showLoading({
title: '提交中...',
});
const { itemType } = this.data;
const isLostItem = itemType === 'lost';
// 模拟网络延迟
setTimeout(() => {
// 在开发环境下,模拟认领成功
console.log('开发环境:模拟认领请求已提交');
// 向发布人发送消息
this.sendClaimMessageToPublisher();
// 更新本地状态
this.setData({
claimStatus: 'claiming'
});
wx.showToast({
title: isLostItem ? '捡到信息已提交' : '认领请求已提交',
icon: 'success',
complete: () => {
// 模拟认领成功后状态变为已认领
setTimeout(() => {
this.setData({ claimStatus: 'claimed' });
}, 2000);
}
});
wx.hideLoading();
}, 1000);
},
// 向发布人发送认领消息
sendClaimMessageToPublisher: function() {
const { itemDetail, itemType } = this.data;
const isLostItem = itemType === 'lost';
const app = getApp();
// 使用统一的createSystemMessage函数创建认领消息
if (app && app.createSystemMessage) {
const title = isLostItem ? '收到认领申请' : '收到认领申请';
const content = isLostItem
? `有人声称捡到了您发布的失物信息"${itemDetail.title}",请查看消息了解详情。`
: `有人申请认领您发布的招领信息"${itemDetail.title}",请查看消息了解详情。`;
// 创建认领消息,包含额外信息
app.createSystemMessage(
'claim',
title,
content,
itemDetail.id,
itemType,
{
itemTitle: itemDetail.title,
claimTime: new Date().toISOString(),
status: 'pending'
}
);
console.log('已创建认领消息通知');
} else {
console.error('无法创建认领消息app.createSystemMessage 不存在');
}
// 显示提示信息,告知用户已向发布者发送认领消息
wx.showToast({
title: isLostItem ? '已向失主发送捡到消息' : '已向发布者发送认领消息',
icon: 'none',
duration: 2000
});
},
// 删除物品信息
deleteItem: function() {
wx.showModal({
title: '确认删除',
content: '确定要删除这条信息吗?删除后将无法恢复。',
success: (res) => {
if (res.confirm) {
this.submitDelete();
}
}
});
},
// 返回上一页
navigateBack: function() {
wx.navigateBack();
wx.navigateBack();
},
// 提交删除请求
submitDelete: function() {
wx.showLoading({
title: '删除中...',
});
// 模拟网络延迟
setTimeout(() => {
try {
// 在开发环境下,模拟删除成功
console.log('开发环境:模拟删除物品成功');
// 从全局用户发布的物品列表中删除对应的物品
const app = getApp();
const userItems = app.globalData.userPublishedItems || [];
const itemIndex = userItems.findIndex(item =>
item.id === this.data.itemId && item.type === this.data.itemType
);
if (itemIndex !== -1) {
userItems.splice(itemIndex, 1);
app.globalData.userPublishedItems = userItems;
}
// 直接设置状态,移除物品信息
this.setData({
itemDetail: null,
loading: false
});
wx.showToast({
title: '删除成功',
icon: 'success'
});
} catch (err) {
console.error('删除物品失败', err);
wx.showToast({
title: '删除失败,请重试',
icon: 'none'
});
this.setData({ loading: false });
} finally {
wx.hideLoading();
}
}, 1000);
},
// 复制联系电话
copyPhone: function() {
const phoneNumber = this.data.itemDetail.contactPhone;
wx.setClipboardData({
data: phoneNumber,
success: () => {
wx.showToast({
title: '电话号码已复制',
icon: 'success'
});
}
});
}
});

@ -0,0 +1,101 @@
<!-- pages/detail/detail.wxml -->
<view class="container">
<!-- 图片轮播 -->
<view class="image-carousel" wx:if="{{itemDetail}}">
<swiper class="swiper" bindchange="onImageChange" indicator-dots="{{itemDetail.images && itemDetail.images.length > 1}}">
<!-- 如果有图片,显示图片 -->
<block wx:for="{{itemDetail.images}}" wx:key="index">
<swiper-item>
<image class="swiper-image" src="{{item}}" mode="aspectFill" bindtap="previewImage" binderror="onImageError" data-index="{{index}}"></image>
</swiper-item>
</block>
</swiper>
<view class="image-count" wx:if="{{itemDetail.images && itemDetail.images.length > 1}}">{{currentImageIndex + 1}}/{{itemDetail.images.length}}</view>
</view>
<!-- 物品类型标签 -->
<view class="item-type" wx:if="{{itemDetail}}">
<text>{{itemType === 'lost' ? '失物信息' : '招领信息'}}</text>
</view>
<!-- 认领状态标签 -->
<view class="claim-status" wx:if="{{claimStatus === 'claimed'}}">
<text>已被认领</text>
</view>
<view class="claim-status pending" wx:if="{{claimStatus === 'claiming'}}">
<text>认领审核中</text>
</view>
<!-- 物品详细信息 -->
<view class="detail-content" wx:if="{{itemDetail}}">
<!-- 标题 -->
<view class="title-section">
<text class="item-title">{{itemDetail.title}}</text>
</view>
<!-- 描述 -->
<view class="desc-section">
<text class="section-label">详细描述</text>
<text class="item-desc">{{itemDetail.description}}</text>
</view>
<!-- 时间和地点 -->
<view class="info-section">
<text class="section-label">{{itemType === 'lost' ? '丢失' : '捡到'}}信息</text>
<view class="info-item">
<image class="info-icon" src="/images/match.svg"></image>
<text class="info-text">{{itemType === 'lost' ? '丢失时间:' : '捡到时间:'}}{{itemDetail.time}}</text>
</view>
<view class="info-item">
<image class="info-icon" src="/images/match.png"></image>
<text class="info-text">{{itemType === 'lost' ? '丢失地点:' : '捡到地点:'}}{{itemDetail.location}}</text>
</view>
</view>
<!-- 联系人信息 -->
<view class="contact-section">
<text class="section-label">联系方式</text>
<view class="contact-item">
<image class="contact-icon" src="/images/user.png"></image>
<text class="contact-text">{{itemDetail.contactName}}</text>
</view>
<view class="contact-item" bindtap="makePhoneCall">
<image class="contact-icon" src="/images/phone.svg"></image>
<text class="contact-text phone">{{itemDetail.contactPhone}}</text>
<image class="copy-icon" src="/images/copy.svg" bindtap="copyPhone"></image>
</view>
</view>
<!-- 发布时间 -->
<view class="publish-time">
<text>发布时间:{{itemDetail.publishTime}}</text>
</view>
</view>
<!-- 加载中 -->
<view class="loading" wx:if="{{loading}}">
<text>加载中...</text>
</view>
<!-- 删除后的空状态 -->
<view class="empty-state" wx:if="{{!loading && !itemDetail}}">
<image class="empty-icon" src="/images/empty.png" mode="aspectFit"></image>
<text class="empty-text">该物品信息已被删除</text>
<button class="back-button" bindtap="navigateBack">返回</button>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar" wx:if="{{!loading && itemDetail}}">
<!-- 浏览别人发布的物品时显示"我捡到了"按钮 -->
<view class="action-buttons" wx:if="{{!isAuthor}}">
<button class="claim-btn" type="primary" bindtap="claimItem" disabled="{{claimStatus === 'claimed'}}">
{{claimStatus === 'claimed' ? '已被认领' : (claimStatus === 'claiming' ? '认领审核中' : '删除')}}
</button>
</view>
<!-- 自己发布的物品显示"删除"按钮 -->
<view class="author-actions" wx:if="{{isAuthor}}">
<button class="delete-btn" type="warn" bindtap="deleteItem">删除</button>
</view>
</view>
</view>

@ -0,0 +1,210 @@
/* pages/detail/detail.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #fff;
}
/* 图片轮播 */
.image-carousel {
position: relative;
width: 100%;
height: 500rpx;
background-color: #f5f5f5;
}
.swiper {
width: 100%;
height: 100%;
}
.swiper-image {
width: 100%;
height: 100%;
}
.image-count {
position: absolute;
bottom: 20rpx;
right: 20rpx;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 16rpx;
}
/* 物品类型标签 */
.item-type {
padding: 20rpx 30rpx;
background-color: #fff;
}
.item-type text {
display: inline-block;
background-color: #2196F3;
color: #fff;
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
/* 认领状态标签 */
.claim-status {
padding: 0 30rpx 20rpx;
}
.claim-status text {
display: inline-block;
background-color: #f44336;
color: #fff;
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
.claim-status.pending text {
background-color: #ff9800;
}
/* 物品详细信息 */
.detail-content {
padding: 30rpx;
flex: 1;
overflow-y: auto;
}
/* 标题 */
.title-section {
margin-bottom: 30rpx;
}
.item-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
line-height: 50rpx;
}
/* 各信息区块 */
.desc-section,
.info-section,
.contact-section {
margin-bottom: 30rpx;
}
.section-label {
display: block;
font-size: 28rpx;
color: #999;
margin-bottom: 16rpx;
}
.item-desc {
font-size: 28rpx;
color: #666;
line-height: 48rpx;
}
/* 信息项 */
.info-item,
.contact-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #eee;
}
.info-item:last-child,
.contact-item:last-child {
border-bottom: none;
}
.info-icon,
.contact-icon {
width: 32rpx;
height: 32rpx;
margin-right: 20rpx;
}
.info-text,
.contact-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.contact-text.phone {
color: #2196F3;
}
.copy-icon {
width: 32rpx;
height: 32rpx;
}
/* 发布时间 */
.publish-time {
padding: 20rpx 0;
border-top: 1rpx solid #eee;
}
.publish-time text {
font-size: 24rpx;
color: #999;
}
/* 加载中 */
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.loading text {
font-size: 28rpx;
color: #999;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
border-top: 1rpx solid #eee;
}
.action-buttons {
display: flex;
gap: 20rpx;
}
.claim-btn {
flex: 1;
height: 96rpx;
font-size: 32rpx;
background-color: #2196F3;
color: #fff;
border-radius: 48rpx;
line-height: 96rpx;
}
.author-actions {
display: flex;
gap: 20rpx;
}
.delete-btn {
flex: 1;
height: 96rpx;
font-size: 32rpx;
background-color: #f44336;
color: #fff;
border-radius: 48rpx;
line-height: 96rpx;
}

@ -0,0 +1,495 @@
// pages/index/index.js
Page({
data: {
activeTab: 'lost', // 当前选中的标签,'lost'表示失物,'found'表示招领
lostItems: [], // 失物列表数据
foundItems: [], // 招领列表数据
pageNum: 1, // 当前页码
hasMore: true, // 是否还有更多数据
loading: false, // 是否正在加载
refreshing: false // 是否正在刷新
},
onLoad: function() {
// 检查用户是否已登录
this.checkLoginStatus();
// 加载初始数据
this.loadItems();
},
onShow: function() {
// 更新消息角标
const app = getApp();
if (app && app.updateTabBarBadge) {
app.updateTabBarBadge();
}
// 检查是否有新发布的物品,如果有则刷新页面数据
if (app.globalData.hasNewPublishedItem) {
// 清除标记
app.globalData.hasNewPublishedItem = false;
// 检查是否有存储的发布类型,如果有则自动切换到对应的标签
if (app.globalData.lastPublishedType) {
this.setData({
activeTab: app.globalData.lastPublishedType,
lostItems: [],
foundItems: []
});
// 清除存储的发布类型
delete app.globalData.lastPublishedType;
} else {
// 清空对应列表数据
if (this.data.activeTab === 'lost') {
this.setData({ lostItems: [] });
} else {
this.setData({ foundItems: [] });
}
}
// 刷新页面数据
this.setData({
pageNum: 1,
hasMore: true
});
// 重新加载数据
this.loadItems();
}
},
// 检查登录状态(简化版,不强制要求登录)
checkLoginStatus: function() {
const app = getApp();
// 不强制要求登录,使用模拟数据继续运行
// 如果用户未登录,我们仍然允许浏览首页,只是使用默认的模拟数据
if (!app.globalData.hasUserInfo) {
console.log('用户未登录,使用模拟数据');
// 如果需要,可以设置一个默认的用户信息
if (!app.globalData.userInfo) {
app.globalData.userInfo = { nickName: '访客', avatarUrl: '/images/default_avatar.svg' };
}
}
},
// 切换标签页
switchTab: function(e) {
const tab = e.currentTarget.dataset.tab;
if (tab !== this.data.activeTab) {
this.setData({
activeTab: tab,
pageNum: 1,
hasMore: true
});
// 清空对应列表数据
if (tab === 'lost') {
this.setData({ lostItems: [] });
} else {
this.setData({ foundItems: [] });
}
// 加载对应列表数据
this.loadItems();
}
},
// 加载物品列表
loadItems: function() {
const activeTab = this.data.activeTab;
console.log('加载物品,当前标签:', activeTab);
this.setData({
loading: true
});
// 调用app.js中的云数据库方法获取物品列表
const app = getApp();
// 优先从云数据库获取数据
app.getItemsFromCloud(activeTab, 1, 20, (result) => {
console.log('从云数据库获取物品成功,数量:', result && result.items ? result.items.length : 0);
// 确保result和items存在且是数组
if (!result || !result.items || !Array.isArray(result.items)) {
console.error('获取物品列表失败,返回数据格式错误');
this.setData({ loading: false });
return;
}
// 按发布时间倒序排序
const sortedItems = result.items.sort((a, b) => {
return new Date(b.publishTime || b.createTime) - new Date(a.publishTime || a.createTime);
});
// 处理每个物品,确保图片和发布者标识
const processedItems = sortedItems.map(item => {
// 确保物品对象有效
if (!item) return null;
// 使用app.js中的processItemImages函数处理图片路径
const processedItem = app.processItemImages(item);
// 确保每个item都有images字段且为数组
if (!processedItem.images || !Array.isArray(processedItem.images) || processedItem.images.length === 0) {
processedItem.images = [processedItem.type === 'lost' ? '/images/lost.png' : '/images/found.png'];
}
// 确保每个item都有isUserPublished字段
if (processedItem.isUserPublished === undefined) {
// 根据_openid判断是否为当前用户发布
const currentOpenId = wx.getStorageSync('openid') || 'local_user';
processedItem.isUserPublished = processedItem._openid === currentOpenId;
}
return processedItem;
}).filter(Boolean); // 过滤掉可能的null值
console.log('处理后的物品列表长度:', processedItems.length);
console.log('是否包含其他用户物品:', processedItems.some(item => !item.isUserPublished));
// 更新对应列表
if (activeTab === 'lost') {
this.setData({
lostItems: processedItems,
hasMoreLostItems: result.hasMore,
loading: false
});
} else if (activeTab === 'found') {
this.setData({
foundItems: processedItems,
hasMoreFoundItems: result.hasMore,
loading: false
});
} else {
// 全部标签,同时更新两个列表
const lostItems = processedItems.filter(item => item.type === 'lost');
const foundItems = processedItems.filter(item => item.type === 'found');
this.setData({
lostItems: lostItems,
foundItems: foundItems,
hasMoreLostItems: result.hasMore,
hasMoreFoundItems: result.hasMore,
loading: false
});
}
console.log('搜索结果详情:', JSON.stringify(this.data[activeTab === 'lost' ? 'lostItems' : 'foundItems']));
console.log('搜索完成');
}, (error) => {
console.error('从云数据库获取物品失败:', error);
// 降级处理:使用本地模拟数据
console.log('降级到本地数据获取');
this.loadLocalItems();
});
},
// 加载本地模拟物品数据(包括所有用户)
loadLocalItems: function() {
const activeTab = this.data.activeTab;
console.log('加载本地模拟物品数据,当前标签:', activeTab);
const app = getApp();
// 调用app.js中的本地数据方法获取所有用户物品
app.getLocalItems(activeTab, 1, 20, (result) => {
console.log('从本地获取物品成功,数量:', result && result.items ? result.items.length : 0);
// 确保result和items存在且是数组
if (!result || !result.items || !Array.isArray(result.items)) {
console.error('获取本地物品列表失败,返回数据格式错误');
this.setData({ loading: false });
return;
}
// 处理每个物品,确保图片和发布者标识
const processedItems = result.items.map(item => {
// 确保物品对象有效
if (!item) return null;
// 使用app.js中的processItemImages函数处理图片路径
const processedItem = app.processItemImages(item);
// 确保每个item都有images字段且为数组
if (!processedItem.images || !Array.isArray(processedItem.images) || processedItem.images.length === 0) {
processedItem.images = [processedItem.type === 'lost' ? '/images/lost.png' : '/images/found.png'];
}
// 确保每个item都有isUserPublished字段
if (processedItem.isUserPublished === undefined) {
// 根据_openid判断是否为当前用户发布
const currentOpenId = wx.getStorageSync('openid') || 'local_user';
processedItem.isUserPublished = processedItem._openid === currentOpenId;
}
return processedItem;
}).filter(Boolean); // 过滤掉可能的null值
// 将非用户发布的物品保存到全局的allPublishedItems中以便详情页能找到
if (!app.globalData.allPublishedItems) {
app.globalData.allPublishedItems = [];
}
// 过滤出非用户发布的物品
const otherUsersItems = processedItems.filter(item => !item.isUserPublished);
// 创建物品ID集合用于去重
const existingItemIds = new Set(app.globalData.allPublishedItems.map(item => item.id));
// 添加新的非用户发布物品,确保使用完整处理后的物品对象
let addedCount = 0;
otherUsersItems.forEach(item => {
if (!existingItemIds.has(item.id)) {
// 确保保存的是完整处理后的物品对象,特别是图片路径
const fullyProcessedItem = {
...item,
// 确保images字段存在且为数组
images: item.images && Array.isArray(item.images) ? item.images.map(img => {
// 再次清理图片路径,确保没有任何无效字符
if (typeof img === 'string') {
return img.replace(/[`'"\\s]/g, '');
}
return item.type === 'lost' ? '/images/lost.png' : '/images/found.png';
}) : [item.type === 'lost' ? '/images/lost.png' : '/images/found.png']
};
app.globalData.allPublishedItems.push(fullyProcessedItem);
existingItemIds.add(item.id);
addedCount++;
// 调试日志,打印物品详情和图片路径
console.log('添加物品到全局ID:', item.id, '图片路径:', JSON.stringify(fullyProcessedItem.images));
}
});
console.log('已更新全局allPublishedItems添加了', addedCount, '个新物品,当前总数:', app.globalData.allPublishedItems.length);
console.log('处理后的本地物品列表长度:', processedItems.length);
console.log('是否包含其他用户物品:', processedItems.some(item => !item.isUserPublished));
// 更新对应列表
if (activeTab === 'lost') {
this.setData({
lostItems: processedItems,
hasMoreLostItems: result.hasMore,
loading: false
});
} else if (activeTab === 'found') {
this.setData({
foundItems: processedItems,
hasMoreFoundItems: result.hasMore,
loading: false
});
} else {
// 全部标签,同时更新两个列表
const lostItems = processedItems.filter(item => item.type === 'lost');
const foundItems = processedItems.filter(item => item.type === 'found');
this.setData({
lostItems: lostItems,
foundItems: foundItems,
hasMoreLostItems: result.hasMore,
hasMoreFoundItems: result.hasMore,
loading: false
});
}
});
},
// 刷新页面数据
onPullDownRefresh: function() {
this.setData({
refreshing: true,
pageNum: 1,
hasMore: true
});
// 清空对应列表数据
if (this.data.activeTab === 'lost') {
this.setData({ lostItems: [] });
} else {
this.setData({ foundItems: [] });
}
// 重新加载数据,确保获取所有用户的最新数据
this.loadItems();
// 调试:打印当前全局数据状态
const app = getApp();
if (app.globalData.isDebug) {
console.log('调试信息:当前全局数据状态:', app.globalData);
}
// 停止下拉刷新
wx.stopPullDownRefresh();
},
// 加载更多数据
onReachBottom: function() {
this.loadItems();
},
// 图片加载错误处理
onImageError: function(e) {
const item = e.currentTarget.dataset.item;
console.error('图片加载失败物品ID:', item.id, '图片路径:', item.images && item.images[0]);
// 如果图片加载失败,使用默认图片替换
const defaultImage = item.type === 'lost' ? '/images/lost.png' : '/images/found.png';
// 更新对应列表中的图片
if (item.type === 'lost') {
const lostItems = this.data.lostItems.map(lostItem => {
if (lostItem.id === item.id) {
return {
...lostItem,
images: [defaultImage]
};
}
return lostItem;
});
this.setData({ lostItems });
} else {
const foundItems = this.data.foundItems.map(foundItem => {
if (foundItem.id === item.id) {
return {
...foundItem,
images: [defaultImage]
};
}
return foundItem;
});
this.setData({ foundItems });
}
console.log('已替换为默认图片:', defaultImage);
},
// 跳转到物品详情页
goToDetail: function(e) {
const itemId = e.currentTarget.dataset.id;
// 优先使用物品的type如果没有则使用activeTab
let type = e.currentTarget.dataset.type || this.data.activeTab;
// 确保type是 'lost' 或 'found',如果是 'all' 则从列表中查找
if (type === 'all') {
// 从当前显示的列表中查找物品类型
const lostItems = this.data.lostItems || [];
const foundItems = this.data.foundItems || [];
const lostItem = lostItems.find(item => item.id === itemId);
const foundItem = foundItems.find(item => item.id === itemId);
if (lostItem) {
type = 'lost';
} else if (foundItem) {
type = 'found';
} else {
// 如果找不到,默认使用 'lost'
type = 'lost';
console.warn('无法确定物品类型,默认使用 lostitemId:', itemId);
}
}
console.log('跳转到详情页itemId:', itemId, 'type:', type);
wx.navigateTo({
url: `/pages/detail/detail?id=${itemId}&type=${type}`
});
},
// 搜索物品
searchItem: function(e) {
const keyword = e.detail.value.trim();
console.log('搜索关键词:', keyword);
if (!keyword) {
// 如果搜索框为空,显示所有物品
this.setData({
pageNum: 1,
hasMore: true
});
if (this.data.activeTab === 'lost') {
this.setData({ lostItems: [] });
} else {
this.setData({ foundItems: [] });
}
this.loadItems();
return;
}
this.setData({ loading: true });
// 调用app.js中的云数据库搜索方法
const app = getApp();
app.searchItemsFromCloud(keyword, this.data.activeTab, (items) => {
console.log('搜索结果数量:', items.length);
// 按发布时间倒序排序
const sortedItems = items.sort((a, b) => {
const timeA = new Date(a.publishTime || a.createTime || 0).getTime();
const timeB = new Date(b.publishTime || b.createTime || 0).getTime();
return timeB - timeA;
});
// 处理每个搜索结果,确保图片路径正确
const processedSearchItems = sortedItems.map(item => {
// 使用app.js中的processItemImages函数处理图片路径
const processedItem = app.processItemImages(item);
// 确保每个item都有images字段且为数组
if (!processedItem.images || !Array.isArray(processedItem.images) || processedItem.images.length === 0) {
processedItem.images = [processedItem.type === 'lost' ? '/images/lost.png' : '/images/found.png'];
}
return processedItem;
});
console.log('搜索结果详情:', JSON.stringify(processedSearchItems));
console.log('处理后的搜索结果数量:', processedSearchItems.length);
// 更新列表显示
if (this.data.activeTab === 'lost') {
this.setData({
lostItems: processedSearchItems,
hasMore: false,
loading: false
});
} else {
this.setData({
foundItems: processedSearchItems,
hasMore: false,
loading: false
});
}
console.log('搜索完成,列表已更新');
});
},
// 检查未读消息
checkUnreadMessages: function() {
// 开发环境下的模拟实现直接返回mock数据
// 实际项目中请替换为真实的网络请求
setTimeout(() => {
// 随机生成是否有未读消息模拟有20%的概率有1条未读消息
const hasUnread = Math.random() < 0.2;
if (hasUnread) {
// 设置tabBar角标
wx.setTabBarBadge({
index: 3, // 消息tab的索引
text: '1'
});
} else {
// 移除tabBar角标
wx.removeTabBarBadge({
index: 3
});
}
}, 500); // 添加500ms的延迟模拟网络请求时间
}
});

@ -0,0 +1,66 @@
<!-- pages/index/index.wxml -->
<view class="container">
<!-- 顶部标签栏 -->
<view class="tab-bar">
<view class="tab {{activeTab === 'lost' ? 'active' : ''}}" data-tab="lost" bindtap="switchTab">
<text>失物</text>
<view class="tab-indicator {{activeTab === 'lost' ? 'show' : ''}}"></view>
</view>
<view class="tab {{activeTab === 'found' ? 'active' : ''}}" data-tab="found" bindtap="switchTab">
<text>招领</text>
<view class="tab-indicator {{activeTab === 'found' ? 'show' : ''}}"></view>
</view>
</view>
<!-- 物品列表 -->
<scroll-view class="item-list" scroll-y>
<block wx:if="{{activeTab === 'lost' && lostItems.length > 0}}">
<view class="item-card" wx:for="{{lostItems}}" wx:key="id" data-id="{{item.id}}" data-type="{{item.type}}" bindtap="goToDetail">
<view class="item-header">
<text class="item-title">{{item.title}}</text>
<text class="item-status">寻找中</text>
</view>
<view class="item-content">
<image class="item-image" wx:if="{{item.images && item.images.length > 0}}" src="{{item.images[0]}}" mode="aspectFill" binderror="onImageError" data-item="{{item}}"></image>
<view class="item-info">
<text class="item-desc">{{item.description}}</text>
<view class="item-meta">
<text class="item-time">{{item.lostTime}}</text>
<text class="item-location">{{item.location}}</text>
</view>
</view>
</view>
</view>
</block>
<block wx:elif="{{activeTab === 'found' && foundItems.length > 0}}">
<view class="item-card" wx:for="{{foundItems}}" wx:key="id" data-id="{{item.id}}" data-type="{{item.type}}" bindtap="goToDetail">
<view class="item-header">
<text class="item-title">{{item.title}}</text>
<text class="item-status">待认领</text>
</view>
<view class="item-content">
<image class="item-image" wx:if="{{item.images && item.images.length > 0}}" src="{{item.images[0]}}" mode="aspectFill" binderror="onImageError" data-item="{{item}}"></image>
<view class="item-info">
<text class="item-desc">{{item.description}}</text>
<view class="item-meta">
<text class="item-time">{{item.foundTime}}</text>
<text class="item-location">{{item.location}}</text>
</view>
</view>
</view>
</view>
</block>
<!-- 空状态 -->
<view class="empty" wx:if="{{(activeTab === 'lost' && lostItems.length === 0) || (activeTab === 'found' && foundItems.length === 0)}} && !loading">
<image class="empty-icon" src="/images/empty.png" mode="aspectFit"></image>
<text class="empty-text">{{activeTab === 'lost' ? '暂无失物信息' : '暂无招领信息'}}</text>
</view>
<!-- 加载中 -->
<view class="loading" wx:if="{{loading && (lostItems.length > 0 || foundItems.length > 0)}}">
<text>加载中...</text>
</view>
</scroll-view>
</view>

@ -0,0 +1,159 @@
/* pages/index/index.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 顶部标签栏 */
.tab-bar {
display: flex;
background-color: #fff;
border-bottom: 1px solid #eee;
}
.tab {
flex: 1;
text-align: center;
padding: 20rpx 0;
position: relative;
}
.tab text {
font-size: 32rpx;
color: #666;
}
.tab.active text {
color: #2196F3;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 30%;
width: 40%;
height: 4rpx;
background-color: #2196F3;
transform: scaleX(0);
transition: transform 0.3s;
}
.tab-indicator.show {
transform: scaleX(1);
}
/* 物品列表 */
.item-list {
flex: 1;
padding: 20rpx;
}
/* 物品卡片 */
.item-card {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 24rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.item-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-status {
font-size: 24rpx;
color: #fff;
background-color: #2196F3;
padding: 4rpx 16rpx;
border-radius: 16rpx;
margin-left: 16rpx;
}
.item-content {
display: flex;
}
.item-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
margin-right: 20rpx;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-desc {
font-size: 28rpx;
color: #666;
line-height: 40rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-meta {
display: flex;
justify-content: space-between;
margin-top: 12rpx;
}
.item-time, .item-location {
font-size: 24rpx;
color: #999;
}
/* 空状态 */
.empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 60vh;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 加载中 */
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx 0;
}
.loading text {
font-size: 28rpx;
color: #999;
}

@ -0,0 +1,189 @@
// pages/login/login.js
Page({
data: {
canIUseGetUserProfile: false,
userInfo: null,
hasUserInfo: false,
heroHighlights: ['AI 图片搜索', '招领进度提醒', '一键发布'],
benefits: [
{
icon: '/images/search_selected_new.png',
title: 'AI 智能以图搜图',
desc: '上传实物照片即可匹配校园范围内的招领记录'
},
{
icon: '/images/message_selected_new.png',
title: '即时认领消息',
desc: '有人认领或留言时,第一时间在消息中心提醒'
},
{
icon: '/images/publish_selected_new.png',
title: '多端同步发布',
desc: '小程序、网页同时发布与更新,资料自动同步'
}
],
supportChannels: [
{ label: '客服微信', value: 'lostfound_helper' },
{ label: '邮箱', value: 'support@lostfound.com' },
{ label: '服务热线', value: '400-000-0000' }
]
},
onLoad: function() {
// 获取全局数据
const app = getApp();
this.setData({
canIUseGetUserProfile: app.globalData.canIUseGetUserProfile,
hasUserInfo: app.globalData.hasUserInfo,
userInfo: app.globalData.userInfo
});
// 如果已经登录,直接跳转到首页
if (this.data.hasUserInfo) {
wx.switchTab({
url: '/pages/index/index'
});
}
},
// 处理微信登录
onGetUserInfo: function(e) {
const app = getApp();
if (e.detail.userInfo) {
// 存储用户信息到全局变量
app.globalData.userInfo = e.detail.userInfo;
app.globalData.hasUserInfo = true;
// 更新页面数据
this.setData({
userInfo: e.detail.userInfo,
hasUserInfo: true
});
// 调用云函数或者API进行登录验证
this.loginToServer();
} else {
// 用户拒绝授权
wx.showToast({
title: '登录失败,请授权后重试',
icon: 'none'
});
}
},
// 使用getUserProfile接口获取用户信息推荐使用
getUserProfile: function() {
const that = this;
wx.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
const app = getApp();
// 存储用户信息到全局变量
app.globalData.userInfo = res.userInfo;
app.globalData.hasUserInfo = true;
// 更新页面数据
that.setData({
userInfo: res.userInfo,
hasUserInfo: true
});
// 调用云函数或者API进行登录验证
that.loginToServer();
},
fail: (err) => {
console.log('获取用户信息失败', err);
wx.showToast({
title: '登录失败,请授权后重试',
icon: 'none'
});
}
});
},
// 登录到服务器(开发环境简化实现)
loginToServer: function() {
wx.login({
success: (res) => {
if (res.code) {
console.log('获取登录凭证成功', res.code);
// 开发环境:完全模拟登录过程,不发起实际的网络请求
// 模拟网络延迟
setTimeout(() => {
const mockResult = {
code: 200,
message: '登录成功',
data: {
token: 'mock_token_' + Date.now(),
userId: 'mock_user_id',
userInfo: this.data.userInfo
}
};
console.log('开发环境:模拟登录成功', mockResult);
// 存储登录态
wx.setStorageSync('token', mockResult.data.token);
// 跳转到首页
wx.switchTab({
url: '/pages/index/index'
});
}, 800);
} else {
console.error('登录失败:' + res.errMsg);
wx.showToast({
title: '登录失败,请重试',
icon: 'none'
});
}
},
fail: (err) => {
console.error('获取登录凭证失败', err);
wx.showToast({
title: '登录失败,请重试',
icon: 'none'
});
}
});
},
// 点击登录按钮
handleLogin: function() {
if (this.data.hasUserInfo) {
wx.switchTab({
url: '/pages/index/index'
});
return;
}
if (this.data.canIUseGetUserProfile) {
// 使用新版接口
this.getUserProfile();
} else {
// 使用旧版接口
this.onGetUserInfo();
}
},
skipLogin: function() {
wx.switchTab({
url: '/pages/index/index'
});
},
showPrivacyPolicy: function() {
wx.navigateTo({
url: '/pages/privacy/privacy'
});
},
showUserAgreement: function() {
wx.navigateTo({
url: '/pages/agreement/agreement'
});
}
});

@ -0,0 +1,67 @@
<!-- pages/login/login.wxml -->
<view class="login-page">
<view class="hero">
<view class="hero-text">
<text class="badge">AI 图片识别 · 实时通知</text>
<text class="hero-title">失物招领 · 一键登录</text>
<text class="hero-subtitle">连接全校同学,让物品更快回到主人身边</text>
<view class="hero-tags">
<view class="tag" wx:for="{{heroHighlights}}" wx:key="*this">{{item}}</view>
</view>
</view>
</view>
<view class="card login-card">
<view class="user-preview">
<image class="avatar" src="{{userInfo && userInfo.avatarUrl ? userInfo.avatarUrl : '/images/default_avatar.svg'}}" mode="aspectFill"></image>
<view class="preview-text">
<text class="welcome">{{hasUserInfo ? '欢迎回来' : '嗨,同学'}}</text>
<text class="hint">{{hasUserInfo ? '已完成登录,可直接前往首页' : '登录后可同步发布记录、接收认领消息'}}</text>
</view>
</view>
<button class="primary-btn"
bindtap="handleLogin">
<image class="btn-icon" src="/images/user_new.png" mode="aspectFit"></image>
<text>{{hasUserInfo ? '进入首页' : '微信一键登录'}}</text>
</button>
<button class="ghost-btn" bindtap="skipLogin">暂不登录,先逛逛</button>
</view>
<view class="card info-card">
<view class="info-header">
<text class="info-title">登录即可体验</text>
<text class="info-desc">智能匹配、招领进度随时掌握</text>
</view>
<view class="benefit-list">
<view class="benefit-item" wx:for="{{benefits}}" wx:key="title">
<image class="benefit-icon" src="{{item.icon}}" mode="aspectFit"></image>
<view class="benefit-text">
<text class="benefit-title">{{item.title}}</text>
<text class="benefit-desc">{{item.desc}}</text>
</view>
</view>
</view>
</view>
<view class="card support-card">
<view class="support-header">
<text class="support-title">遇到问题?联系小助手</text>
<text class="support-desc">我们随时在线,帮你确认物品状态</text>
</view>
<view class="support-list">
<view class="support-item" wx:for="{{supportChannels}}" wx:key="label">
<view class="support-label">{{item.label}}</view>
<text class="support-value">{{item.value}}</text>
</view>
</view>
</view>
<view class="agreement">
<text>登录即表示您同意</text>
<text class="link" bindtap="showPrivacyPolicy">《隐私政策》</text>
<text>和</text>
<text class="link" bindtap="showUserAgreement">《用户协议》</text>
</view>
</view>

@ -0,0 +1,235 @@
/* pages/login/login.wxss */
.login-page {
min-height: 100vh;
background: linear-gradient(180deg, #eff7ff 0%, #fdfdfd 55%, #ffffff 100%);
padding: 72rpx 48rpx 120rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 32rpx;
}
.hero {
background: linear-gradient(135deg, #0078ff 0%, #00c6ff 90%);
border-radius: 36rpx;
padding: 56rpx;
color: #fff;
position: relative;
overflow: hidden;
}
.hero::after {
content: '';
position: absolute;
width: 320rpx;
height: 320rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.15);
right: -80rpx;
top: -60rpx;
}
.hero-text {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.badge {
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.18);
width: fit-content;
}
.hero-title {
font-size: 48rpx;
font-weight: 600;
}
.hero-subtitle {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.85);
}
.hero-tags {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 12rpx;
}
.tag {
font-size: 24rpx;
padding: 8rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.18);
}
.card {
background: #fff;
border-radius: 32rpx;
padding: 48rpx;
box-shadow: 0 20rpx 60rpx rgba(15, 85, 132, 0.08);
}
.login-card {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.user-preview {
display: flex;
align-items: center;
gap: 24rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: #f2f4f7;
border: 4rpx solid #eef3ff;
}
.welcome {
font-size: 36rpx;
font-weight: 600;
color: #1f2b3d;
}
.hint {
font-size: 26rpx;
color: #6a7689;
margin-top: 8rpx;
}
.primary-btn {
height: 100rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #07c160 0%, #06ad87 100%);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
}
.primary-btn::after {
border: none;
}
.btn-icon {
width: 48rpx;
height: 48rpx;
margin-right: 16rpx;
}
.ghost-btn {
height: 96rpx;
border-radius: 999rpx;
border: 2rpx solid #d6e4ff;
color: #3977ff;
background: transparent;
}
.ghost-btn::after {
border: none;
}
.info-card,
.support-card {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.info-header,
.support-header {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-title,
.support-title {
font-size: 34rpx;
font-weight: 600;
color: #1f2b3d;
}
.info-desc,
.support-desc {
font-size: 26rpx;
color: #6a7689;
}
.benefit-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.benefit-item {
display: flex;
gap: 24rpx;
padding: 20rpx;
border-radius: 24rpx;
background: #f7fbff;
}
.benefit-icon {
width: 80rpx;
height: 80rpx;
}
.benefit-title {
font-size: 30rpx;
font-weight: 600;
color: #1f2b3d;
}
.benefit-desc {
font-size: 26rpx;
color: #6a7689;
margin-top: 8rpx;
}
.support-list {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.support-item {
display: flex;
justify-content: space-between;
font-size: 28rpx;
color: #1f2b3d;
padding-bottom: 12rpx;
border-bottom: 2rpx dashed #eef1f6;
}
.support-label {
color: #6a7689;
}
.support-value {
font-weight: 600;
}
.agreement {
font-size: 24rpx;
color: #8a94a6;
text-align: center;
margin-bottom: 40rpx;
}
.link {
color: #2d6bff;
}

@ -0,0 +1,166 @@
// pages/match/match.js
Page({
data: {
matchedItems: [], // 匹配的物品列表
loading: true, // 是否正在加载
hasMore: true, // 是否还有更多匹配结果
pageNum: 1, // 当前页码
matchType: 'lost' // 匹配类型,'lost'表示匹配到的失物,'found'表示匹配到的招领
},
onLoad: function() {
// 检查用户是否已登录
this.checkLoginStatus();
// 加载匹配的物品列表
this.loadMatchedItems();
},
// 检查登录状态(简化版,不强制要求登录)
checkLoginStatus: function() {
const app = getApp();
// 不强制要求登录,使用模拟数据继续运行
// 如果用户未登录,我们仍然允许浏览匹配结果,只是使用默认的模拟数据
if (!app.globalData.hasUserInfo) {
console.log('用户未登录,使用模拟数据显示匹配结果');
// 如果需要,可以设置一个默认的用户信息
if (!app.globalData.userInfo) {
app.globalData.userInfo = { nickName: '访客', avatarUrl: '/images/default_avatar.svg' };
}
// 确保智能匹配功能默认启用
if (!app.globalData.settings) {
app.globalData.settings = { enableSmartMatch: true };
} else if (!app.globalData.settings.enableSmartMatch) {
app.globalData.settings.enableSmartMatch = true;
}
}
},
// 切换匹配类型
switchMatchType: function(e) {
const type = e.currentTarget.dataset.type;
this.setData({
matchType: type,
pageNum: 1,
hasMore: true
});
this.loadMatchedItems();
},
// 加载匹配的物品列表
loadMatchedItems: function() {
if (this.data.loading || !this.data.hasMore) return;
this.setData({ loading: true });
const app = getApp();
// 检查智能匹配是否启用
if (!app.globalData.settings || !app.globalData.settings.enableSmartMatch) {
this.setData({
matchedItems: [],
loading: false,
hasMore: false
});
wx.showToast({
title: '智能匹配功能未启用,请在设置中开启',
icon: 'none',
duration: 2000
});
return;
}
// 使用本地智能匹配功能
app.getSmartMatches(this.data.matchType, this.data.pageNum, (res) => {
if (res.items) {
const newItems = res.items;
// 如果是第一页,直接替换数据;否则追加数据
if (this.data.pageNum === 1) {
this.setData({
matchedItems: newItems,
pageNum: 2,
hasMore: newItems.length === 10
});
} else {
this.setData({
matchedItems: [...this.data.matchedItems, ...newItems],
pageNum: this.data.pageNum + 1,
hasMore: newItems.length === 10
});
}
}
this.setData({ loading: false });
});
},
// 加载更多匹配结果
onReachBottom: function() {
this.loadMatchedItems();
},
// 下拉刷新
onPullDownRefresh: function() {
this.setData({
pageNum: 1,
hasMore: true
});
this.loadMatchedItems();
},
// 跳转到物品详情页
goToDetail: function(e) {
const itemId = e.currentTarget.dataset.id;
const type = e.currentTarget.dataset.type;
wx.navigateTo({
url: `/pages/detail/detail?id=${itemId}&type=${type}`
});
},
// 跳转到设置页面
goToSettings: function() {
wx.navigateTo({
url: '/pages/settings/settings'
});
},
// 忽略匹配
ignoreMatch: function(e) {
const { id, index } = e.currentTarget.dataset;
wx.showModal({
title: '确认忽略',
content: '确定要忽略这个匹配吗?忽略后将不再显示。',
success: (res) => {
if (res.confirm) {
this.submitIgnore(id, index);
}
}
});
},
// 提交忽略请求
submitIgnore: function(id, index) {
const app = getApp();
// 使用本地忽略功能
app.ignoreMatch(id, (res) => {
if (res.success) {
// 从列表中移除该匹配项
const newMatchedItems = [...this.data.matchedItems];
newMatchedItems.splice(index, 1);
this.setData({
matchedItems: newMatchedItems
});
wx.showToast({
title: '已忽略',
icon: 'success'
});
}
});
}
});

@ -0,0 +1,66 @@
<!-- pages/match/match.wxml -->
<view class="container">
<!-- 顶部标题 -->
<view class="header">
<text class="title">智能匹配</text>
<view class="settings-btn" bindtap="goToSettings">
<image src="/images/settings.svg" mode="aspectFit"></image>
</view>
</view>
<!-- 匹配类型切换 -->
<view class="match-type">
<view class="type-tab {{matchType === 'lost' ? 'active' : ''}}" data-type="lost" bindtap="switchMatchType">
<text>匹配到的失物</text>
</view>
<view class="type-tab {{matchType === 'found' ? 'active' : ''}}" data-type="found" bindtap="switchMatchType">
<text>匹配到的招领</text>
</view>
</view>
<!-- 匹配物品列表 -->
<scroll-view class="match-list" scroll-y>
<view class="match-item"
wx:for="{{matchedItems}}"
wx:key="id"
data-id="{{item.id}}"
data-type="{{item.type}}"
data-index="{{index}}">
<!-- 匹配度提示 -->
<view class="match-degree" style="background-color: {{item.matchDegree >= 80 ? '#4CAF50' : item.matchDegree >= 60 ? '#2196F3' : '#FF9800'}};">
<text>匹配度:{{item.matchDegree}}%</text>
</view>
<!-- 物品信息卡片 -->
<view class="item-card" bindtap="goToDetail">
<image class="item-image" wx:if="{{item.images && item.images.length > 0}}" src="{{item.images[0]}}" mode="aspectFill"></image>
<view class="item-info">
<text class="item-title">{{item.title}}</text>
<text class="item-desc">{{item.description}}</text>
<view class="item-meta">
<text class="item-time">{{item.time}}</text>
<text class="item-location">{{item.location}}</text>
</view>
<text class="item-type">{{item.type === 'lost' ? '失物' : '招领'}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button class="ignore-btn" type="default" bindtap="ignoreMatch" data-id="{{item.id}}" data-index="{{index}}">忽略</button>
</view>
</view>
<!-- 空状态 -->
<view class="empty" wx:if="{{matchedItems.length === 0 && !loading}}">
<image class="empty-icon" src="/images/no_match.svg" mode="aspectFit"></image>
<text class="empty-text">暂无匹配的物品</text>
<text class="empty-subtext">系统会根据您发布的信息自动匹配相关物品</text>
</view>
<!-- 加载中 -->
<view class="loading" wx:if="{{loading && matchedItems.length > 0}}">
<text>加载中...</text>
</view>
</scroll-view>
</view>

@ -0,0 +1,219 @@
/* pages/match/match.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 顶部标题 */
.header {
padding: 20rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
flex: 1;
text-align: center;
}
.settings-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.settings-btn image {
width: 40rpx;
height: 40rpx;
}
/* 匹配类型切换 */
.match-type {
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.type-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
}
.type-tab.active {
color: #2196F3;
}
.type-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #2196F3;
}
/* 匹配物品列表 */
.match-list {
flex: 1;
padding: 20rpx;
}
/* 匹配项 */
.match-item {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
/* 匹配度提示 */
.match-degree {
padding: 16rpx 24rpx;
background-color: #e3f2fd;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.2);
}
.match-degree text {
font-size: 24rpx;
color: #2196F3;
font-weight: bold;
}
/* 物品卡片 */
.item-card {
display: flex;
padding: 24rpx;
}
.item-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
margin-right: 20rpx;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-desc {
font-size: 28rpx;
color: #666;
line-height: 40rpx;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-meta {
display: flex;
justify-content: space-between;
margin-bottom: 8rpx;
}
.item-time, .item-location {
font-size: 24rpx;
color: #999;
}
.item-type {
font-size: 24rpx;
color: #fff;
background-color: #2196F3;
padding: 4rpx 16rpx;
border-radius: 16rpx;
align-self: flex-start;
}
/* 操作按钮 */
.action-buttons {
display: flex;
padding: 16rpx 24rpx;
background-color: #f9f9f9;
border-top: 1rpx solid #eee;
}
.ignore-btn {
flex: 1;
height: 72rpx;
font-size: 28rpx;
background-color: #fff;
color: #666;
border: 1rpx solid #ddd;
border-radius: 8rpx;
line-height: 72rpx;
}
/* 空状态 */
.empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 60vh;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.empty-subtext {
font-size: 24rpx;
color: #999;
text-align: center;
padding: 0 60rpx;
}
/* 加载中 */
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx 0;
}
.loading text {
font-size: 28rpx;
color: #999;
}

@ -0,0 +1,403 @@
// pages/message/message.js
Page({
data: {
messages: [], // 消息列表
unreadCount: 0, // 未读消息数量
loading: false, // 是否正在加载
hasMore: false, // 没有更多消息
pageNum: 1, // 当前页码
messageType: 'all' // 消息类型,'all'全部,'unread'未读,'system'系统消息,'match'匹配消息
},
onLoad: function() {
// 使用异步方式初始化,避免阻塞页面加载
setTimeout(() => {
// 检查用户是否已登录
this.checkLoginStatus();
// 加载消息列表
this.loadMessages();
}, 50);
},
onShow: function() {
// 页面显示时,刷新消息列表
this.setData({
pageNum: 1,
hasMore: false
});
this.loadMessages();
},
// 检查登录状态(简化版,不强制要求登录)
checkLoginStatus: function() {
const app = getApp();
// 不强制要求登录,使用模拟数据继续运行
// 如果用户未登录,我们仍然允许浏览消息页面,只是使用默认的模拟数据
if (!app.globalData.hasUserInfo) {
console.log('用户未登录,使用模拟数据显示消息');
// 如果需要,可以设置一个默认的用户信息
if (!app.globalData.userInfo) {
app.globalData.userInfo = { nickName: '访客', avatarUrl: '/images/default_avatar.svg' };
}
}
// 确保消息列表已初始化
if (!app.globalData.pendingClaimMessages) {
// 尝试从本地存储加载
try {
const storedMessages = wx.getStorageSync('pendingClaimMessages');
if (storedMessages && Array.isArray(storedMessages)) {
app.globalData.pendingClaimMessages = storedMessages;
} else {
app.globalData.pendingClaimMessages = [];
}
} catch (e) {
console.error('加载消息失败:', e);
app.globalData.pendingClaimMessages = [];
}
}
},
// 切换消息类型
switchMessageType: function(e) {
const type = e.currentTarget.dataset.type;
this.setData({
messageType: type,
pageNum: 1,
hasMore: false
});
this.loadMessages();
},
// 加载消息列表
loadMessages: function() {
if (this.data.loading) return;
this.setData({ loading: true });
// 获取全局应用实例
const app = getApp();
// 确保消息列表已初始化(从本地存储加载)
if (!app.globalData.pendingClaimMessages) {
try {
const storedMessages = wx.getStorageSync('pendingClaimMessages');
if (storedMessages && Array.isArray(storedMessages)) {
app.globalData.pendingClaimMessages = storedMessages;
console.log('从本地存储加载消息:', storedMessages.length, '条');
} else {
app.globalData.pendingClaimMessages = [];
console.log('本地存储中没有消息,初始化空数组');
}
} catch (e) {
console.error('加载消息失败:', e);
app.globalData.pendingClaimMessages = [];
}
}
// 从全局数据中获取消息列表
let messages = [];
if (app && app.globalData && app.globalData.pendingClaimMessages) {
messages = [...app.globalData.pendingClaimMessages];
console.log('当前消息总数:', messages.length);
// 按时间戳倒序排序(最新的在前)
messages.sort((a, b) => {
const timeA = a.timestamp || 0;
const timeB = b.timestamp || 0;
return timeB - timeA; // 倒序
});
// 格式化时间显示
messages = messages.map(msg => {
// 如果消息已经有格式化时间,直接使用;否则使用时间戳格式化
if (!msg.time && msg.timestamp) {
msg.time = app.formatMessageTime ? app.formatMessageTime(new Date(msg.timestamp)) : '刚刚';
} else if (!msg.time) {
msg.time = '刚刚';
}
return msg;
});
} else {
console.log('全局数据中没有消息列表');
}
// 根据消息类型过滤
let filteredMessages = messages;
if (this.data.messageType === 'unread') {
// 未读消息status为unread或pending或isRead为false
filteredMessages = messages.filter(msg =>
msg.status === 'unread' || msg.status === 'pending' || !msg.isRead
);
console.log('未读消息数量:', filteredMessages.length);
} else if (this.data.messageType !== 'all') {
// 按类型过滤
filteredMessages = messages.filter(msg => msg.type === this.data.messageType);
console.log('过滤后消息数量:', filteredMessages.length, '类型:', this.data.messageType);
} else {
console.log('显示全部消息:', filteredMessages.length);
}
// 计算未读消息数量(所有消息中的未读数)
const unreadCount = messages.filter(msg =>
msg.status === 'unread' || msg.status === 'pending' || !msg.isRead
).length;
console.log('未读消息总数:', unreadCount);
console.log('过滤后的消息列表:', filteredMessages);
setTimeout(() => {
this.setData({
messages: filteredMessages,
unreadCount: unreadCount,
loading: false,
hasMore: false // 没有更多消息
});
// 更新tabBar角标
if (app && app.updateTabBarBadge) {
app.updateTabBarBadge();
}
}, 300); // 轻微延迟,保持良好的用户体验
},
// 加载更多消息
onReachBottom: function() {
this.loadMessages();
},
// 下拉刷新
onPullDownRefresh: function() {
this.setData({
pageNum: 1,
hasMore: false
});
this.loadMessages();
// 停止下拉刷新动画
wx.stopPullDownRefresh();
},
// 标记消息为已读
markAsRead: function(e) {
const { id } = e.currentTarget.dataset;
const app = getApp();
if (app && app.globalData && app.globalData.pendingClaimMessages) {
const messageIndex = app.globalData.pendingClaimMessages.findIndex(msg => msg.id === id);
if (messageIndex !== -1) {
app.globalData.pendingClaimMessages[messageIndex].status = 'read';
app.globalData.pendingClaimMessages[messageIndex].isRead = true;
// 持久化存储
try {
wx.setStorageSync('pendingClaimMessages', app.globalData.pendingClaimMessages);
} catch (e) {
console.error('保存消息状态失败:', e);
}
// 更新tabBar角标
if (app.updateTabBarBadge) {
app.updateTabBarBadge();
}
// 刷新消息列表
this.loadMessages();
}
}
},
// 点击消息
onMessageClick: function(e) {
const { id, itemId, itemType } = e.currentTarget.dataset;
// 标记消息为已读
this.markAsRead(e);
// 如果是匹配消息,跳转到匹配页面
const app = getApp();
const message = app.globalData.pendingClaimMessages.find(msg => msg.id === id);
if (message && message.type === 'match') {
// 跳转到匹配页面
wx.switchTab({
url: '/pages/match/match'
});
return;
}
// 如果消息关联了物品,跳转到物品详情页,并传递来源标志
if (itemId && itemType) {
wx.navigateTo({
url: `/pages/detail/detail?id=${itemId}&type=${itemType}&fromNotification=true`
});
}
},
// 点击匹配项卡片
onMatchItemClick: function(e) {
const { matchId, matchType } = e.currentTarget.dataset;
// 注意:已使用 catchtap 阻止事件冒泡,不需要手动调用 stopPropagation
// 微信小程序的事件对象不支持 stopPropagation 方法
// 跳转到匹配项详情页
if (matchId && matchType) {
wx.navigateTo({
url: `/pages/detail/detail?id=${matchId}&type=${matchType}&fromMatch=true`
});
}
},
// 匹配项图片加载错误处理
onMatchImageError: function(e) {
const { matchId } = e.currentTarget.dataset;
console.log('匹配项图片加载失败:', matchId);
// 图片加载失败时,使用默认图片显示
// WXML中已经设置了默认图片这里只需要记录日志
},
// 一键已读
markAllAsRead: function() {
const app = getApp();
if (app && app.globalData && app.globalData.pendingClaimMessages) {
app.globalData.pendingClaimMessages.forEach(msg => {
msg.status = 'read';
msg.isRead = true;
});
// 持久化存储
try {
wx.setStorageSync('pendingClaimMessages', app.globalData.pendingClaimMessages);
} catch (e) {
console.error('保存消息状态失败:', e);
}
// 更新tabBar角标
if (app.updateTabBarBadge) {
app.updateTabBarBadge();
}
// 刷新消息列表
this.loadMessages();
wx.showToast({
title: '已全部标记为已读',
icon: 'success',
duration: 1500
});
}
},
// 删除消息
deleteMessage: function(e) {
const { id } = e.currentTarget.dataset;
const app = getApp();
wx.showModal({
title: '确认删除',
content: '确定要删除这条消息吗?删除后将无法恢复。',
success: (res) => {
if (res.confirm) {
if (app && app.globalData && app.globalData.pendingClaimMessages) {
const messageIndex = app.globalData.pendingClaimMessages.findIndex(msg => msg.id === id);
if (messageIndex !== -1) {
app.globalData.pendingClaimMessages.splice(messageIndex, 1);
// 持久化存储
try {
wx.setStorageSync('pendingClaimMessages', app.globalData.pendingClaimMessages);
} catch (e) {
console.error('保存消息状态失败:', e);
}
// 更新tabBar角标
if (app.updateTabBarBadge) {
app.updateTabBarBadge();
}
// 刷新消息列表
this.loadMessages();
wx.showToast({
title: '消息已删除',
icon: 'success',
duration: 1500
});
}
}
}
}
});
// 阻止事件冒泡
e.stopPropagation();
},
// 清空所有消息
clearAllMessages: function() {
wx.showModal({
title: '确认清空',
content: '确定要清空所有消息吗?此操作无法恢复。',
success: (res) => {
if (res.confirm) {
const app = getApp();
if (app && app.globalData) {
app.globalData.pendingClaimMessages = [];
// 持久化存储
try {
wx.setStorageSync('pendingClaimMessages', []);
} catch (e) {
console.error('清空消息失败:', e);
}
// 更新tabBar角标
if (app.updateTabBarBadge) {
app.updateTabBarBadge();
}
// 刷新消息列表
this.loadMessages();
wx.showToast({
title: '已清空所有消息',
icon: 'success',
duration: 1500
});
}
}
}
});
},
// 测试创建消息(用于调试)
createTestMessage: function() {
const app = getApp();
if (app && app.createSystemMessage) {
app.createSystemMessage(
'system',
'测试消息',
'这是一条测试消息,用于验证消息通知功能是否正常工作。',
null,
null
);
// 刷新消息列表
this.loadMessages();
wx.showToast({
title: '测试消息已创建',
icon: 'success',
duration: 1500
});
} else {
wx.showToast({
title: '创建消息失败',
icon: 'none',
duration: 1500
});
}
}
});

@ -0,0 +1,106 @@
<!-- pages/message/message.wxml -->
<view class="container">
<!-- 顶部操作栏 -->
<view class="header">
<text class="title">消息通知</text>
<view class="header-actions">
<button class="read-all-btn" size="mini" bindtap="markAllAsRead" wx:if="{{unreadCount > 0}}">一键已读</button>
<button class="clear-all-btn" size="mini" bindtap="clearAllMessages" wx:if="{{messages.length > 0}}">清空</button>
</view>
</view>
<!-- 消息类型切换 -->
<view class="message-type">
<scroll-view scroll-x>
<view class="type-tab {{messageType === 'all' ? 'active' : ''}}" data-type="all" bindtap="switchMessageType">
<text>全部消息</text>
<view class="badge" wx:if="{{unreadCount > 0}}">{{unreadCount}}</view>
</view>
<view class="type-tab {{messageType === 'unread' ? 'active' : ''}}" data-type="unread" bindtap="switchMessageType">
<text>未读消息</text>
<view class="badge" wx:if="{{unreadCount > 0}}">{{unreadCount}}</view>
</view>
<view class="type-tab {{messageType === 'system' ? 'active' : ''}}" data-type="system" bindtap="switchMessageType">
<text>系统消息</text>
</view>
<view class="type-tab {{messageType === 'match' ? 'active' : ''}}" data-type="match" bindtap="switchMessageType">
<text>匹配通知</text>
</view>
</scroll-view>
</view>
<!-- 消息列表 -->
<scroll-view class="message-list" scroll-y>
<view class="message-item {{item.isRead || item.status === 'read' ? '' : 'unread'}}"
wx:for="{{messages}}"
wx:key="id">
<view class="message-content-wrapper"
data-id="{{item.id}}"
data-item-id="{{item.itemId}}"
data-item-type="{{item.itemType}}"
bindtap="onMessageClick">
<view class="message-icon">
<image src="{{item.type === 'system' ? '/images/system.svg' : (item.type === 'match' ? '/images/match.svg' : (item.type === 'claim' ? '/images/message.svg' : '/images/message.svg'))}}" mode="aspectFit"></image>
</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{item.title}}</text>
<text class="message-time">{{item.time || '刚刚'}}</text>
</view>
<text class="message-desc">{{item.content}}</text>
<!-- 匹配物品列表(仅匹配消息显示) -->
<view class="match-items-list" wx:if="{{item.type === 'match' && item.matchItems && item.matchItems.length > 0}}">
<view class="match-item-card"
wx:for="{{item.matchItems}}"
wx:key="id"
wx:for-item="matchItem"
wx:for-index="idx"
data-match-id="{{matchItem.id}}"
data-match-type="{{matchItem.type}}"
catchtap="onMatchItemClick">
<image class="match-item-image"
src="{{matchItem.images && matchItem.images.length > 0 ? matchItem.images[0] : '/images/default_item.png'}}"
mode="aspectFill"
binderror="onMatchImageError"
data-index="{{idx}}"
data-match-id="{{matchItem.id}}"></image>
<view class="match-item-info">
<text class="match-item-title">{{matchItem.title}}</text>
<text class="match-item-desc" wx:if="{{matchItem.description}}">{{matchItem.description}}</text>
<view class="match-item-meta">
<text class="match-item-location" wx:if="{{matchItem.location}}">{{matchItem.location}}</text>
<text class="match-item-degree">匹配度: {{matchItem.matchDegree}}%</text>
</view>
</view>
</view>
<text class="match-items-more" wx:if="{{item.matchCount > item.matchItems.length}}">
还有 {{item.matchCount - item.matchItems.length}} 个匹配项,点击查看全部
</text>
</view>
<view class="message-tags" wx:if="{{item.itemType || item.matchCount}}">
<text class="message-tag" wx:if="{{item.itemType}}">{{item.itemType === 'lost' ? '失物' : '招领'}}</text>
<text class="message-tag match-tag" wx:if="{{item.matchCount}}">匹配{{item.matchCount}}项</text>
</view>
</view>
<view class="unread-dot" wx:if="{{!item.isRead && item.status !== 'read'}}"></view>
</view>
<view class="message-delete" data-id="{{item.id}}" bindtap="deleteMessage">
<text>删除</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty" wx:if="{{messages.length === 0 && !loading}}">
<image class="empty-icon" src="/images/no_message.svg" mode="aspectFit"></image>
<text class="empty-text">暂无消息</text>
<button class="test-btn" size="mini" bindtap="createTestMessage">创建测试消息</button>
</view>
<!-- 加载中 -->
<view class="loading" wx:if="{{loading && messages.length > 0}}">
<text>加载中...</text>
</view>
</scroll-view>
</view>

@ -0,0 +1,311 @@
/* pages/message/message.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 顶部操作栏 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.read-all-btn {
font-size: 24rpx;
color: #2196F3;
background-color: transparent;
line-height: normal;
padding: 0;
margin: 0;
}
/* 消息类型切换 */
.message-type {
background-color: #fff;
border-bottom: 1rpx solid #eee;
padding: 0 20rpx;
}
.message-type scroll-view {
white-space: nowrap;
}
.type-tab {
display: inline-block;
padding: 24rpx 30rpx;
font-size: 28rpx;
color: #666;
position: relative;
}
.type-tab.active {
color: #2196F3;
}
.type-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #2196F3;
}
.badge {
position: absolute;
top: 12rpx;
right: 12rpx;
min-width: 32rpx;
height: 32rpx;
background-color: #f44336;
color: #fff;
font-size: 20rpx;
text-align: center;
line-height: 32rpx;
border-radius: 16rpx;
padding: 0 8rpx;
}
/* 消息列表 */
.message-list {
flex: 1;
padding: 20rpx;
}
/* 消息项 */
.message-item {
display: flex;
align-items: flex-start;
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.message-item.unread {
background-color: #f8f9fa;
}
.message-icon {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
display: flex;
justify-content: center;
align-items: center;
}
.message-icon image {
width: 50rpx;
height: 50rpx;
}
.message-content {
flex: 1;
position: relative;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.message-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.message-time {
font-size: 24rpx;
color: #999;
margin-left: 20rpx;
}
.message-desc {
font-size: 28rpx;
color: #666;
line-height: 40rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 8rpx;
}
.message-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 8rpx;
}
.message-tag {
font-size: 24rpx;
color: #fff;
background-color: #2196F3;
padding: 4rpx 16rpx;
border-radius: 16rpx;
}
.message-tag.match-tag {
background-color: #ff9800;
}
/* 匹配物品列表 */
.match-items-list {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #eee;
}
.match-item-card {
display: flex;
align-items: flex-start;
padding: 16rpx;
margin-bottom: 12rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
border: 1rpx solid #e9ecef;
}
.match-item-card:active {
background-color: #e9ecef;
}
.match-item-image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
margin-right: 16rpx;
flex-shrink: 0;
background-color: #fff;
}
.match-item-info {
flex: 1;
display: flex;
flex-direction: column;
}
.match-item-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.match-item-desc {
font-size: 24rpx;
color: #666;
line-height: 36rpx;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.match-item-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8rpx;
}
.match-item-location {
font-size: 22rpx;
color: #999;
}
.match-item-degree {
font-size: 22rpx;
color: #ff9800;
font-weight: bold;
}
.match-items-more {
display: block;
text-align: center;
font-size: 24rpx;
color: #2196F3;
padding: 12rpx 0;
margin-top: 8rpx;
}
.unread-dot {
width: 16rpx;
height: 16rpx;
background-color: #f44336;
border-radius: 50%;
margin-left: 20rpx;
margin-top: 30rpx;
}
/* 空状态 */
.empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 60vh;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 30rpx;
}
.test-btn {
margin-top: 30rpx;
background-color: #2196F3;
color: #fff;
border-radius: 8rpx;
padding: 12rpx 40rpx;
font-size: 28rpx;
}
/* 加载中 */
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx 0;
}
.loading text {
font-size: 28rpx;
color: #999;
}

@ -0,0 +1,92 @@
// pages/privacy/privacy.js
Page({
data: {
privacyContent: ''
},
onLoad: function() {
// 加载隐私政策内容
this.loadPrivacyPolicy();
},
// 加载隐私政策内容
loadPrivacyPolicy: function() {
// 在实际项目中,这里应该从服务器获取隐私政策内容
// 为了演示,这里使用静态内容
const privacyContent = `失物招领小程序隐私政策
更新日期2024年1月1日
欢迎使用失物招领小程序以下简称"我们"我们非常重视您的隐私保护并会尽全力保护您的个人信息安全为了让您能够安心地使用我们的服务我们向您说明我们的隐私政策帮助您了解我们如何收集使用存储和保护您的个人信息以及您可以如何管理这些信息
我们收集的信息
1.1 您提供的信息
当您使用我们的服务时我们可能会收集您主动提供的个人信息例如
- 您在注册或登录时提供的微信账号信息如昵称头像等
- 您在发布失物或招领信息时提供的内容如物品描述图片联系方式等
- 您在与我们客服沟通时提供的信息
1.2 我们自动收集的信息
当您使用我们的服务时我们可能会自动收集一些信息例如
- 设备信息包括设备型号操作系统版本设备标识符等
- 位置信息当您使用位置相关功能时我们可能会收集您的位置信息
- 使用信息包括您使用我们服务的时间频率方式等
我们如何使用收集的信息
2.1 提供服务
我们使用收集的信息来提供维护和改进我们的服务例如
- 处理您的发布请求
- 为您匹配相关的失物或招领信息
- 向您发送通知
2.2 安全保障
我们使用收集的信息来确保我们服务的安全性例如
- 验证您的身份
- 防止欺诈行为
- 保护用户免受骚扰
2.3 其他用途
在获得您的同意的情况下我们可能会将您的信息用于其他目的
我们如何保护您的信息
我们采取了各种安全措施来保护您的个人信息包括
- 加密存储和传输
- 访问控制
- 定期安全审计
信息共享
我们不会向第三方出售您的个人信息在以下情况下我们可能会共享您的信息
- 获得您的明确同意
- 遵守法律法规的要求
- 保护我们的用户服务或合法权益
您的权利
您有权访问更正删除您的个人信息也有权限制或反对我们对您个人信息的处理您可以通过小程序内的设置功能或联系我们的客服来行使这些权利
隐私政策的更新
我们可能会不时更新我们的隐私政策当我们对隐私政策进行重大更改时我们会通过适当的方式通知您
联系我们
如果您对我们的隐私政策有任何问题或建议请通过以下方式联系我们
- 小程序内客服功能
- 电子邮件support@shiwuzhaoling.com
感谢您使用失物招领小程序`;
this.setData({
privacyContent: privacyContent
});
}
});

@ -0,0 +1,16 @@
<!-- pages/privacy/privacy.wxml -->
<view class="container">
<!-- 顶部标题 -->
<view class="header">
<text class="title">隐私政策</text>
</view>
<!-- 隐私政策内容 -->
<scroll-view class="content" scroll-y>
<view class="privacy-content">
<text class="content-text">
{{privacyContent}}
</text>
</view>
</scroll-view>
</view>

@ -0,0 +1,46 @@
/* pages/privacy/privacy.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 顶部标题 */
.header {
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
/* 内容区域 */
.content {
flex: 1;
padding: 30rpx;
}
.privacy-content {
background-color: #fff;
padding: 40rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.content-text {
font-size: 28rpx;
line-height: 48rpx;
color: #333;
white-space: pre-line;
}
/* 添加段落间距 */
.content-text text {
display: block;
margin-bottom: 20rpx;
}

@ -0,0 +1,786 @@
// pages/publish/publish.js
Page({
data: {
publishType: 'lost', // 发布类型,'lost'表示失物,'found'表示招领
title: '', // 标题
description: '', // 描述
location: '', // 地点
time: '', // 时间
contactName: '', // 联系人
contactPhone: '', // 联系电话
images: [], // 上传的图片列表(自动清理无效路径)
maxImages: 9, // 最多上传的图片数量
loading: false // 是否正在提交
},
// 安全设置图片列表(自动清理无效路径)
setImagesSafe: function(images) {
const cleanedImages = this.cleanImagePaths(images || []);
this.setData({
images: cleanedImages
});
return cleanedImages;
},
onLoad: function() {
// 检查用户是否已登录
this.checkLoginStatus();
// 页面加载时立即清理无效的图片路径(强制清理)
const currentImages = this.data.images || [];
const cleanedImages = this.cleanImagePaths(currentImages);
// 强制更新,确保数据始终是干净的
if (cleanedImages.length !== currentImages.length ||
JSON.stringify(cleanedImages) !== JSON.stringify(currentImages)) {
console.log('onLoad: 检测到', currentImages.length - cleanedImages.length, '个无效图片路径,已清理');
this.setData({
images: cleanedImages
});
} else {
// 即使看起来相同,也使用安全设置方法确保清理
this.setImagesSafe(currentImages);
}
},
onShow: function() {
// 页面显示时,强制清理无效的图片路径(总是执行清理,确保数据干净)
// 使用安全设置方法,确保每次都清理
this.setImagesSafe(this.data.images || []);
},
onReady: function() {
// 页面渲染完成后,再次清理无效路径(确保渲染时数据是干净的)
// 使用安全设置方法,确保每次都清理
this.setImagesSafe(this.data.images || []);
},
// 检查登录状态并使用云开发登录(带降级逻辑)
checkLoginStatus: function() {
const app = getApp();
try {
// 先检查是否已有本地存储的openid
const openid = wx.getStorageSync('openid');
if (openid) {
console.log('使用本地缓存的openid:', openid);
app.globalData.hasUserInfo = true;
if (!app.globalData.userInfo) {
app.globalData.userInfo = {
nickName: '用户' + openid.substring(0, 8),
avatarUrl: '/images/default_avatar.svg'
};
}
return;
}
// 检查是否应该强制使用模拟登录(调试模式或没有部署云函数)
if (app.globalData.isDebug) {
console.log('调试模式下,直接使用模拟登录');
this.simulateLogin(app);
return;
}
// 检查微信云开发是否可用
if (wx.cloud && typeof wx.cloud.callFunction === 'function') {
console.log('尝试使用微信云开发登录');
try {
wx.cloud.callFunction({
name: 'login',
success: res => {
console.log('云函数登录成功openid:', res.result.openid);
// 保存openid到本地存储
wx.setStorageSync('openid', res.result.openid);
app.globalData.hasUserInfo = true;
// 获取用户信息
wx.getUserProfile({
desc: '用于完善用户资料',
success: (userInfoRes) => {
console.log('获取用户信息成功:', userInfoRes.userInfo);
// 保存用户信息到全局
app.globalData.userInfo = userInfoRes.userInfo;
},
fail: () => {
// 如果用户拒绝授权,仍然使用基础登录信息
console.log('用户拒绝授权,使用基础登录信息');
if (!app.globalData.userInfo) {
app.globalData.userInfo = {
nickName: '用户' + res.result.openid.substring(0, 8),
avatarUrl: '/images/default_avatar.svg'
};
}
}
});
},
fail: err => {
console.error('云函数登录失败:', err);
// 无论什么错误,都降级到模拟登录
this.simulateLogin(app);
}
});
} catch (callError) {
console.error('调用云函数过程出错:', callError);
// 捕获所有可能的错误,确保降级到模拟登录
this.simulateLogin(app);
}
} else {
// 云开发不可用时降级到模拟登录
console.log('云开发不可用,使用模拟登录');
this.simulateLogin(app);
}
} catch (error) {
console.error('登录过程出错:', error);
// 出错时降级到模拟登录
this.simulateLogin(app);
}
},
// 模拟登录(降级方案)
simulateLogin: function(app) {
console.log('使用本地模拟登录');
// 生成模拟openid
const mockOpenid = 'local_user_' + Date.now();
// 存储到本地缓存
wx.setStorageSync('openid', mockOpenid);
app.globalData.hasUserInfo = true;
if (!app.globalData.userInfo) {
app.globalData.userInfo = {
nickName: '用户' + mockOpenid.substring(0, 8),
avatarUrl: '/images/default_avatar.svg'
};
}
// 确保有token
if (!wx.getStorageSync('token')) {
wx.setStorageSync('token', 'mock_token_' + Date.now());
}
console.log('模拟登录成功');
},
// 切换发布类型
switchPublishType: function(e) {
const type = e.currentTarget.dataset.type;
this.setData({
publishType: type
});
},
// 监听输入框内容变化
onInput: function(e) {
const { field } = e.currentTarget.dataset;
this.setData({
[field]: e.detail.value
});
},
// 选择地点
chooseLocation: function() {
wx.chooseLocation({
success: (res) => {
this.setData({
location: res.name
});
},
fail: (err) => {
console.error('选择地点失败', err);
// 如果用户取消选择,不显示提示
if (err.errMsg !== 'chooseLocation:fail cancel') {
wx.showToast({
title: '选择地点失败',
icon: 'none'
});
}
}
});
},
// 选择时间
chooseTime: function() {
// 基础库 2.15.0 不支持 wx.showDatePicker使用更兼容的方式
wx.showModal({
title: '选择日期',
editable: true,
placeholderText: '请输入日期格式YYYY-MM-DD',
success: (res) => {
if (res.cancel) return;
// 简单验证日期格式
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
if (datePattern.test(res.content)) {
this.setData({
time: res.content
});
} else {
wx.showToast({
title: '日期格式不正确',
icon: 'none'
});
}
}
});
},
// 上传图片(支持云存储和本地路径两种模式)
uploadImage: function(tempFilePath, callback) {
const app = getApp();
console.log('开始处理图片上传:', tempFilePath);
// 验证临时文件路径格式
if (!tempFilePath || typeof tempFilePath !== 'string') {
console.error('无效的临时文件路径:', tempFilePath);
callback(new Error('无效的临时文件路径'), null);
return;
}
// 确保临时文件路径格式正确(微信小程序的临时文件路径通常以 wxfile:// 开头)
// 如果路径不是以 wxfile://、http://、https:// 开头,且不是以 / 开头的本地资源路径,则可能是无效路径
if (!tempFilePath.startsWith('wxfile://') &&
!tempFilePath.startsWith('http://') &&
!tempFilePath.startsWith('https://') &&
!tempFilePath.startsWith('/')) {
console.warn('临时文件路径格式可能不正确:', tempFilePath);
// 仍然尝试使用,但添加警告
}
// 检查是否支持云开发(优先使用云存储,即使调试模式也尝试)
// 只有在云开发环境已验证为无效时才不使用云存储
const shouldUseCloud = wx.cloud &&
typeof wx.cloud.uploadFile === 'function' &&
app.globalData.cloudEnvValidated !== false; // 环境未验证或已验证为有效时使用
if (shouldUseCloud) {
// 尝试使用云存储上传
console.log('尝试使用云存储上传图片');
this.uploadImageToCloud(tempFilePath, callback);
} else {
// 降级处理:直接使用本地临时路径
if (app.globalData.cloudEnvValidated === false) {
console.log('云开发环境已确认为无效,使用本地临时文件路径');
} else if (!wx.cloud || typeof wx.cloud.uploadFile !== 'function') {
console.log('云开发功能不可用,使用本地临时文件路径');
} else {
console.log('使用本地临时文件路径(调试模式或环境未验证)');
}
// 直接返回临时文件路径这样WXML可以直接使用
callback(null, tempFilePath);
}
},
// 上传图片到云存储
uploadImageToCloud: function(tempFilePath, callback) {
const app = getApp();
// 生成上传文件名
const fileName = 'item_images/' + Date.now() + '_' + Math.floor(Math.random() * 1000) + '.jpg';
console.log('开始上传图片到云存储:', fileName);
wx.cloud.uploadFile({
cloudPath: fileName,
filePath: tempFilePath,
success: res => {
console.log('图片上传成功文件ID:', res.fileID);
// 标记云环境为有效
app.globalData.cloudEnvValidated = true;
// 返回上传后的文件ID
callback(null, res.fileID);
},
fail: err => {
console.error('云存储上传失败:', err);
// 检查是否是环境不存在的错误
const isEnvError = err.errCode === -501000 || (err.errMsg && err.errMsg.includes('env not exists'));
if (isEnvError) {
console.warn('云开发环境不存在,标记为无效并切换到本地路径');
// 标记环境为无效,避免后续重复尝试
app.globalData.cloudEnvValidated = false;
app.globalData.db = null;
}
// 降级到本地临时文件路径
console.log('使用本地临时文件路径作为降级方案');
callback(null, tempFilePath);
}
});
},
// 验证图片路径是否有效
isValidImagePath: function(path) {
if (!path || typeof path !== 'string') {
return false;
}
// 特别检查 /pages/publish/local_image_ 格式(最严格的检查)
if (path.indexOf('/pages/publish/local_image_') !== -1) {
return false;
}
// 有效路径格式wxfile://、http://、https://、cloud:// 或以 /images/ 开头的本地资源
// 排除任何包含 local_image_ 的路径(除非是有效的网络路径)
if (path.indexOf('local_image_') !== -1 &&
path.indexOf('http://') !== 0 &&
path.indexOf('https://') !== 0 &&
path.indexOf('cloud://') !== 0) {
return false;
}
// 排除 /pages/ 开头的路径(这些不是有效的图片路径)
if (path.indexOf('/pages/') === 0) {
return false;
}
// 检查是否是有效的路径格式
return path.indexOf('wxfile://') === 0 ||
path.indexOf('http://') === 0 ||
path.indexOf('https://') === 0 ||
path.indexOf('cloud://') === 0 ||
path.indexOf('/images/') === 0 ||
(path.indexOf('/') === 0 && path.indexOf('/pages/') !== 0);
},
// 选择多张图片
chooseImages: function() {
const that = this;
// 计算还可以选择的图片数量
const currentCount = this.data.images.length || 0;
const maxCount = 9;
const remainingCount = maxCount - currentCount;
if (remainingCount <= 0) {
wx.showToast({
title: '最多只能上传9张图片',
icon: 'none'
});
return;
}
// 调用微信图片选择API
wx.chooseImage({
count: remainingCount,
sizeType: ['compressed'], // 只选择压缩后的图片
sourceType: ['album', 'camera'], // 可以从相册和相机选择
success: res => {
console.log('选择图片成功,数量:', res.tempFilePaths.length);
// 验证所有临时文件路径,过滤掉无效路径
const validTempPaths = res.tempFilePaths.filter(path => {
const isValid = that.isValidImagePath(path);
if (!isValid) {
console.warn('检测到无效的临时文件路径,已过滤:', path);
}
return isValid;
});
if (validTempPaths.length === 0) {
wx.showToast({
title: '没有有效的图片',
icon: 'none'
});
return;
}
// 合并现有图片和新的有效临时路径
const existingImages = (that.data.images || []).filter(img =>
that.isValidImagePath(img)
);
const tempImageList = [...existingImages, ...validTempPaths];
// 先更新临时图片列表到界面上,让用户看到选择的图片
// 使用安全设置方法,自动清理无效路径
that.setImagesSafe(tempImageList);
// 显示加载提示
wx.showLoading({
title: '正在处理图片...',
mask: true
});
// 处理每张图片,确保所有用户都能正常上传
that.uploadImages(tempImageList, 0, [], function(err, uploadedImages) {
wx.hideLoading();
if (err) {
console.error('处理图片失败:', err);
wx.showToast({
title: '部分图片处理失败',
icon: 'none'
});
// 即使部分失败,也使用已成功处理的图片
if (uploadedImages && uploadedImages.length > 0) {
// 使用安全设置方法,自动清理无效路径
that.setImagesSafe(uploadedImages);
} else {
// 如果没有有效的图片,清空列表
that.setData({
images: []
});
}
return;
}
console.log('所有图片处理完成,最终图片数量:', uploadedImages.length);
// 使用安全设置方法,自动清理无效路径
const cleanedImages = that.setImagesSafe(uploadedImages || []);
// 如果有无效路径被清理,提示用户
if (cleanedImages.length < (uploadedImages || []).length) {
console.warn('部分图片路径无效,已自动清理');
}
});
},
fail: err => {
console.error('选择图片失败:', err);
wx.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
// 递归处理多张图片
uploadImages: function(imageList, index, uploadedImages, callback) {
if (index >= imageList.length) {
// 所有图片处理完成
callback(null, uploadedImages);
return;
}
const that = this;
const imagePath = imageList[index];
console.log('处理第', index + 1, '/', imageList.length, '张图片,路径:', imagePath);
// 严格验证原始路径是否有效
if (!that.isValidImagePath(imagePath)) {
console.error('处理图片失败:无效的图片路径(索引:', index, ':', imagePath);
// 跳过无效路径,不添加到列表中
that.uploadImages(imageList, index + 1, uploadedImages, callback);
return;
}
// 使用改进的uploadImage方法支持云存储和本地路径
this.uploadImage(imagePath, function(err, processedPath) {
if (err) {
console.error('处理图片失败(索引:', index, ':', err);
// 对于单张图片处理失败,检查原始路径是否仍然有效
if (that.isValidImagePath(imagePath)) {
console.log('使用原始临时文件路径作为降级方案:', imagePath);
uploadedImages.push(imagePath);
} else {
console.warn('原始路径也无效,跳过该图片');
}
} else {
// 严格验证处理后的路径是否有效
if (that.isValidImagePath(processedPath)) {
// 将处理成功的图片路径添加到列表
uploadedImages.push(processedPath);
} else {
console.warn('处理后的路径格式无效:', processedPath);
// 如果处理后的路径无效,尝试使用原始路径
if (that.isValidImagePath(imagePath)) {
console.log('使用原始路径作为降级方案:', imagePath);
uploadedImages.push(imagePath);
} else {
console.warn('原始路径也无效,跳过该图片');
}
}
}
// 继续处理下一张图片
that.uploadImages(imageList, index + 1, uploadedImages, callback);
});
},
// 删除图片
deleteImage: function(e) {
const { index } = e.currentTarget.dataset;
const newImages = [...this.data.images];
newImages.splice(index, 1);
// 使用安全设置方法,自动清理无效路径
this.setImagesSafe(newImages);
},
// 处理图片路径用于显示(确保路径有效)
getImagePath: function(imagePath) {
if (!imagePath || typeof imagePath !== 'string') {
return '/images/empty.png';
}
// 检查是否是无效的 local_image_ 格式路径(包括 /pages/publish/local_image_ 格式)
if (imagePath.includes('/pages/publish/local_image_') || imagePath.startsWith('/pages/publish/local_image_')) {
console.warn('publish.js - 检测到无效的 /pages/publish/local_image_ 路径,使用默认图片:', imagePath);
return '/images/empty.png';
}
// 如果是 local_image_ 格式的无效路径,使用默认图片
if (imagePath.includes('local_image_') &&
!imagePath.startsWith('wxfile://') &&
!imagePath.startsWith('http://') &&
!imagePath.startsWith('https://') &&
!imagePath.startsWith('/images/')) {
console.warn('publish.js - 检测到无效的local_image_路径使用默认图片:', imagePath);
return '/images/empty.png';
}
// 检查是否是 /pages/ 开头的无效路径
if (imagePath.startsWith('/pages/') && !imagePath.startsWith('/images/')) {
console.warn('publish.js - 检测到无效的 /pages/ 路径,使用默认图片:', imagePath);
return '/images/empty.png';
}
return imagePath;
},
// 清理图片路径,移除无效的 local_image_ 格式路径
cleanImagePaths: function(imagePaths) {
if (!imagePaths || !Array.isArray(imagePaths)) {
return [];
}
// 使用统一的验证函数,过滤掉所有无效路径
const cleaned = imagePaths
.filter(path => {
// 直接过滤不需要map
const isValid = this.isValidImagePath(path);
if (!isValid && path) {
console.warn('publish.js - 清理:过滤无效路径:', path);
// 特别记录 /pages/publish/local_image_ 格式的路径
if (typeof path === 'string' && path.indexOf('/pages/publish/local_image_') !== -1) {
console.error('⚠️ 检测到严重的无效路径格式:', path);
}
}
return isValid;
});
// 返回清理后的有效路径数组
return cleaned;
},
// 图片加载错误处理
onImageError: function(e) {
const { index } = e.currentTarget.dataset;
const imagePath = this.data.images[index];
console.error('图片加载失败,索引:', index, '路径:', imagePath);
// 如果图片加载失败,立即替换为默认图片或移除
const newImages = [...this.data.images];
if (newImages[index] && newImages[index] !== '/images/empty.png') {
const currentPath = newImages[index];
// 检查是否是无效路径
if (!this.isValidImagePath(currentPath)) {
console.warn('检测到无效图片路径,移除:', currentPath);
// 移除无效路径
newImages.splice(index, 1);
} else {
// 即使是有效格式的路径,如果加载失败(可能是临时文件已过期),也移除
console.warn('图片路径格式正确但加载失败,可能是临时文件已过期,移除');
newImages.splice(index, 1);
}
// 使用安全设置方法,确保移除后再次清理
this.setImagesSafe(newImages);
}
},
// 提交发布信息
submitPublish: function() {
const that = this;
const { publishType, title, description, location, time, contactName, contactPhone, images } = this.data;
// 表单验证
if (!this.validateForm()) return;
// 显示加载提示
wx.showLoading({
title: '发布中...',
mask: true
});
// 清理图片路径,确保没有无效路径
const cleanedImages = this.cleanImagePaths(images || []);
// 准备发布数据
const publishData = {
title: title.trim(),
description: description.trim(),
location: location.trim(),
time: time,
contactName: contactName.trim(),
contactPhone: contactPhone.trim(),
images: cleanedImages // 使用清理后的图片路径
};
console.log('准备发布物品数据:', publishData);
console.log('包含图片数量:', publishData.images.length);
console.log('图片路径:', publishData.images);
const app = getApp();
// 确保app实例中有allPublishedItems数组用于存储所有用户发布的物品
if (!app.globalData.allPublishedItems) {
app.globalData.allPublishedItems = [];
}
// 根据类型调用不同的发布方法
if (publishType === 'lost') {
app.publishLostItem(publishData, function(res) {
wx.hideLoading();
if (res && res.success) {
console.log('失物信息发布成功');
wx.showToast({
title: '发布成功',
icon: 'success',
duration: 2000,
success: function() {
// 在app.globalData中设置标记表示有新发布的物品
const app = getApp();
app.globalData.hasNewPublishedItem = true;
// 存储发布类型,以便返回首页时自动切换到对应的标签
app.globalData.lastPublishedType = publishType;
// 延迟跳转到首页
setTimeout(function() {
wx.switchTab({
url: '/pages/index/index'
});
}, 1500);
}
});
} else {
console.error('失物信息发布失败:', res);
wx.showToast({
title: res && res.message || '发布失败',
icon: 'none'
});
}
});
} else {
app.publishFoundItem(publishData, function(res) {
wx.hideLoading();
if (res && res.success) {
console.log('招领信息发布成功');
wx.showToast({
title: '发布成功',
icon: 'success',
duration: 2000,
success: function() {
// 在app.globalData中设置标记表示有新发布的物品
const app = getApp();
app.globalData.hasNewPublishedItem = true;
// 存储发布类型,以便返回首页时自动切换到对应的标签
app.globalData.lastPublishedType = publishType;
// 延迟跳转到首页
setTimeout(function() {
wx.switchTab({
url: '/pages/index/index'
});
}, 1500);
}
});
} else {
console.error('招领信息发布失败:', res);
wx.showToast({
title: res && res.message || '发布失败',
icon: 'none'
});
}
});
}
},
// 处理发布结果
handlePublishResult: function(res) {
this.setData({ loading: false });
if (res && res.success) {
wx.showToast({
title: '发布成功',
icon: 'success',
duration: 2000,
success: () => {
setTimeout(() => {
// 在app.globalData中设置标记表示有新发布的物品
const app = getApp();
app.globalData.hasNewPublishedItem = true;
// 存储发布类型,以便返回首页时自动切换到对应的标签
app.globalData.lastPublishedType = this.data.publishType;
// 跳转到首页
wx.switchTab({
url: '/pages/index/index'
});
}, 1500);
}
});
} else {
wx.showToast({
title: '发布失败,请重试',
icon: 'none'
});
}
},
// 表单验证
validateForm: function() {
if (!this.data.title.trim()) {
wx.showToast({ title: '请输入标题', icon: 'none' });
return false;
}
if (!this.data.description.trim()) {
wx.showToast({ title: '请输入描述', icon: 'none' });
return false;
}
if (!this.data.location.trim()) {
wx.showToast({ title: '请选择地点', icon: 'none' });
return false;
}
if (!this.data.time.trim()) {
wx.showToast({ title: '请选择时间', icon: 'none' });
return false;
}
if (!this.data.contactName.trim()) {
wx.showToast({ title: '请输入联系人', icon: 'none' });
return false;
}
if (!this.data.contactPhone.trim()) {
wx.showToast({ title: '请输入联系电话', icon: 'none' });
return false;
}
// 简单的手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(this.data.contactPhone)) {
wx.showToast({ title: '请输入正确的手机号码', icon: 'none' });
return false;
}
return true;
}
});

@ -0,0 +1,99 @@
<!-- pages/publish/publish.wxml -->
<wxs module="imageUtils" src="./publish.wxs"></wxs>
<view class="container">
<!-- 顶部发布类型切换 -->
<view class="publish-type">
<view class="type-tab {{publishType === 'lost' ? 'active' : ''}}" data-type="lost" bindtap="switchPublishType">
<text>发布失物</text>
</view>
<view class="type-tab {{publishType === 'found' ? 'active' : ''}}" data-type="found" bindtap="switchPublishType">
<text>发布招领</text>
</view>
</view>
<!-- 表单区域 -->
<form class="publish-form">
<!-- 标题输入 -->
<view class="form-group">
<text class="form-label">标题 *</text>
<input class="form-input"
placeholder="请输入物品名称或简短描述"
value="{{title}}"
data-field="title"
bindinput="onInput"></input>
</view>
<!-- 详细描述 -->
<view class="form-group">
<text class="form-label">详细描述 *</text>
<textarea class="form-textarea"
placeholder="请详细描述物品特征、丢失/捡到经过等信息"
value="{{description}}"
data-field="description"
bindinput="onInput"
auto-height></textarea>
</view>
<!-- 地点选择 -->
<view class="form-group">
<text class="form-label">地点 *</text>
<view class="location-input" bindtap="chooseLocation">
<text wx:if="{{location}}" class="location-text">{{location}}</text>
<text wx:else class="location-placeholder">请选择丢失/捡到地点</text>
<image class="location-icon" src="/images/match.png"></image>
</view>
</view>
<!-- 时间选择 -->
<view class="form-group">
<text class="form-label">时间 *</text>
<view class="time-input" bindtap="chooseTime">
<text wx:if="{{time}}" class="time-text">{{time}}</text>
<text wx:else class="time-placeholder">请选择丢失/捡到时间</text>
<image class="time-icon" src="/images/match.svg"></image>
</view>
</view>
<!-- 联系人信息 -->
<view class="form-group">
<text class="form-label">联系人 *</text>
<input class="form-input"
placeholder="请输入联系人姓名"
value="{{contactName}}"
data-field="contactName"
bindinput="onInput"></input>
</view>
<view class="form-group">
<text class="form-label">联系电话 *</text>
<input class="form-input"
placeholder="请输入手机号码"
value="{{contactPhone}}"
data-field="contactPhone"
bindinput="onInput"
type="number"></input>
</view>
<!-- 图片上传 -->
<view class="form-group">
<text class="form-label">上传图片</text>
<view class="image-upload">
<view class="image-item" wx:for="{{imageUtils.filterValidImages(images)}}" wx:key="index">
<image class="image-preview" src="{{item}}" mode="aspectFill" binderror="onImageError" data-index="{{index}}" lazy-load="{{true}}"></image>
<view class="image-delete" data-index="{{index}}" bindtap="deleteImage">
<text>×</text>
</view>
</view>
<view class="image-upload-btn" bindtap="chooseImages" wx:if="{{images.length < maxImages}}">
<text>+</text>
</view>
</view>
<text class="image-tip">最多上传{{maxImages}}张图片</text>
</view>
<!-- 提交按钮 -->
<button class="submit-button" type="primary" bindtap="submitPublish" loading="{{loading}}">
{{publishType === 'lost' ? '发布失物信息' : '发布招领信息'}}
</button>
</form>
</view>

@ -0,0 +1,67 @@
// pages/publish/publish.wxs
// 图片路径验证函数WXS 语法,不能使用 ES6
function isValidImagePath(path) {
if (!path) {
return false;
}
var pathStr = path + '';
// 特别检查 /pages/publish/local_image_ 格式(最严格的检查)
if (pathStr.indexOf('/pages/publish/local_image_') !== -1) {
return false;
}
// 排除任何包含 local_image_ 的路径(除非是有效的网络路径)
if (pathStr.indexOf('local_image_') !== -1) {
// 只有在以 http://、https://、cloud:// 开头时才认为是有效的
if (pathStr.indexOf('http://') !== 0 &&
pathStr.indexOf('https://') !== 0 &&
pathStr.indexOf('cloud://') !== 0) {
return false;
}
}
// 排除 /pages/ 开头的路径(这些不是有效的图片路径)
if (pathStr.indexOf('/pages/') === 0) {
return false;
}
// 检查是否是有效的路径格式
return pathStr.indexOf('wxfile://') === 0 ||
pathStr.indexOf('http://') === 0 ||
pathStr.indexOf('https://') === 0 ||
pathStr.indexOf('cloud://') === 0 ||
pathStr.indexOf('/images/') === 0 ||
(pathStr.indexOf('/') === 0 && pathStr.indexOf('/pages/') !== 0);
}
// 获取安全的图片路径
function getSafeImagePath(path) {
if (isValidImagePath(path)) {
return path;
}
return '/images/empty.png';
}
// 过滤图片数组,只返回有效路径
function filterValidImages(images) {
if (!images || !images.length) {
return [];
}
var result = [];
for (var i = 0; i < images.length; i++) {
if (isValidImagePath(images[i])) {
result.push(images[i]);
}
}
return result;
}
module.exports = {
isValidImagePath: isValidImagePath,
getSafeImagePath: getSafeImagePath,
filterValidImages: filterValidImages
};

@ -0,0 +1,166 @@
/* pages/publish/publish.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
box-sizing: border-box;
}
/* 发布类型切换 */
.publish-type {
display: flex;
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.type-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
}
.type-tab.active {
background-color: #2196F3;
color: #fff;
}
/* 表单样式 */
.publish-form {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
flex: 1;
overflow-y: auto;
}
.form-group {
margin-bottom: 30rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
display: block;
}
.form-input {
width: 100%;
height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.form-textarea {
width: 100%;
min-height: 160rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
line-height: 1.5;
box-sizing: border-box;
}
/* 地点和时间选择器 */
.location-input, .time-input {
width: 100%;
height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.location-text, .time-text {
font-size: 28rpx;
color: #333;
}
.location-placeholder, .time-placeholder {
font-size: 28rpx;
color: #999;
}
.location-icon, .time-icon {
width: 32rpx;
height: 32rpx;
color: #999;
}
/* 图片上传区域 */
.image-upload {
display: flex;
flex-wrap: wrap;
margin-bottom: 12rpx;
}
.image-item {
width: 160rpx;
height: 160rpx;
margin-right: 20rpx;
margin-bottom: 20rpx;
position: relative;
}
.image-preview {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.image-delete {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 32rpx;
}
.image-upload-btn {
width: 160rpx;
height: 160rpx;
border: 1rpx dashed #ddd;
border-radius: 8rpx;
display: flex;
justify-content: center;
align-items: center;
color: #999;
font-size: 64rpx;
}
.image-tip {
font-size: 24rpx;
color: #999;
}
/* 提交按钮 */
.submit-button {
width: 100%;
height: 96rpx;
font-size: 32rpx;
background-color: #2196F3;
color: #fff;
border-radius: 48rpx;
margin-top: 40rpx;
line-height: 96rpx;
}

@ -0,0 +1,664 @@
// pages/search/search.js
Page({
data: {
keyword: '', // 搜索关键词
searchResults: [], // 搜索结果
loading: false, // 是否正在搜索
hasMore: true, // 是否还有更多结果
pageNum: 1, // 当前页码
searchType: 'text', // 搜索类型,'text'表示文字搜索,'image'表示图片搜索
imagePath: '', // 上传的图片路径
searchProgress: '' // 搜索进度提示
},
onLoad: function() {
// 检查用户是否已登录
this.checkLoginStatus();
},
// 检查登录状态(简化版,不强制要求登录)
checkLoginStatus: function() {
const app = getApp();
// 不强制要求登录,使用模拟数据继续运行
// 如果用户未登录,我们仍然允许使用搜索功能,只是使用默认的模拟数据
if (!app.globalData.hasUserInfo) {
console.log('用户未登录,使用模拟数据进行搜索');
// 如果需要,可以设置一个默认的用户信息
if (!app.globalData.userInfo) {
app.globalData.userInfo = { nickName: '访客', avatarUrl: '/images/default_avatar.svg' };
}
}
},
// 监听输入框内容变化
onInput: function(e) {
this.setData({
keyword: e.detail.value
});
},
// 切换搜索类型
switchSearchType: function(e) {
const type = e.currentTarget.dataset.type;
this.setData({
searchType: type,
searchResults: [],
pageNum: 1,
hasMore: true
});
},
// 执行文字搜索
searchByText: function() {
if (!this.data.keyword.trim()) {
wx.showToast({
title: '请输入搜索关键词',
icon: 'none'
});
return;
}
this.setData({
loading: true,
searchResults: [],
pageNum: 1,
hasMore: true
});
const app = getApp();
app.searchItemsByText(this.data.keyword, (res) => {
if (res && res.items) {
this.processSearchResults(res.items, false);
this.setData({
hasMore: res.items.length === 10
});
} else {
this.setData({ loading: false });
wx.showToast({
title: '搜索失败,请重试',
icon: 'none'
});
}
});
},
// 处理搜索结果
processSearchResults: function(results, append = false) {
console.log('处理搜索结果,总数:', results.length);
// 处理每个搜索结果项,确保有正确的图片和发布者标识
const processedResults = results.map(item => {
const parseSimilarityValue = () => {
if (typeof item.similarityValue === 'number') {
return item.similarityValue;
}
if (typeof item.similarity === 'number') {
return item.similarity;
}
if (typeof item.similarity === 'string') {
const cleaned = item.similarity.replace('%', '').trim();
const parsed = parseFloat(cleaned);
if (!isNaN(parsed)) {
return item.similarity.includes('%') ? parsed / 100 : parsed;
}
}
return undefined;
};
const normalizedSimilarity = parseSimilarityValue();
const similarityDisplay = normalizedSimilarity !== undefined
? (normalizedSimilarity * 100).toFixed(1) + '%'
: (typeof item.similarity === 'string' ? item.similarity : undefined);
// 确保每个item都有images字段且为数组
if (!item.images || !Array.isArray(item.images) || item.images.length === 0) {
item.images = [item.type === 'lost' ? '/images/lost.png' : '/images/found.png'];
}
// 处理图片路径,确保路径格式正确
const processedImages = item.images.map(img => {
if (typeof img === 'string') {
// 检查是否是cloud://路径,需要转换
if (img.startsWith('cloud://')) {
// cloud://路径需要异步转换,这里先保留,稍后转换
return img;
}
// 检查是否是有效的网络图片URL或本地路径
if (img.startsWith('http://') || img.startsWith('https://') || img.startsWith('/')) {
return img;
} else {
// 尝试作为相对路径处理
return '/' + img;
}
}
return item.type === 'lost' ? '/images/lost.png' : '/images/found.png';
});
// 标记当前物品是否为用户自己发布的
const currentOpenId = wx.getStorageSync('openid') || 'local_user';
const isUserPublished = item._openid === currentOpenId || item.isUserPublished === true;
return {
...item,
images: processedImages,
isUserPublished: isUserPublished,
// 统一保留数值相似度和展示字段
similarityValue: normalizedSimilarity,
similarity: similarityDisplay !== undefined ? similarityDisplay : item.similarity
};
});
// 根据相似度或发布时间排序
const shouldSortBySimilarity = processedResults.some(item => item.isAISimilarity || typeof item.similarityValue === 'number');
const sortedResults = processedResults.sort((a, b) => {
if (shouldSortBySimilarity) {
const diff = (b.similarityValue || 0) - (a.similarityValue || 0);
if (diff !== 0) {
return diff;
}
}
const dateA = new Date(a.publishTime || a.createTime || 0);
const dateB = new Date(b.publishTime || b.createTime || 0);
return dateB - dateA;
});
console.log('处理后的搜索结果,包含其他用户物品:', sortedResults.some(item => !item.isUserPublished));
// 转换所有cloud://路径为临时URL
this.convertAllCloudImages(sortedResults, (convertedResults) => {
// 更新数据
this.setData({
searchResults: append ? [...this.data.searchResults, ...convertedResults] : convertedResults,
loading: false,
hasResults: convertedResults.length > 0
});
});
},
// 转换所有搜索结果中的cloud://路径
convertAllCloudImages: function(results, callback) {
const app = getApp();
const cloudPaths = [];
const pathToIndexMap = {}; // 记录路径对应的结果索引和图片索引
// 收集所有需要转换的cloud://路径
results.forEach((item, itemIndex) => {
if (item.images && Array.isArray(item.images)) {
item.images.forEach((img, imgIndex) => {
if (img && img.startsWith('cloud://')) {
if (!pathToIndexMap[img]) {
pathToIndexMap[img] = [];
cloudPaths.push(img);
}
pathToIndexMap[img].push({ itemIndex, imgIndex });
}
});
}
});
// 如果没有cloud://路径,直接返回
if (cloudPaths.length === 0) {
callback(results);
return;
}
console.log('发现', cloudPaths.length, '个云存储路径需要转换');
// 批量转换
if (wx.cloud && typeof wx.cloud.getTempFileURL === 'function') {
wx.cloud.getTempFileURL({
fileList: cloudPaths,
success: (res) => {
const urlMap = {};
if (res.fileList) {
res.fileList.forEach((file) => {
if (file.tempFileURL) {
urlMap[file.fileID] = file.tempFileURL;
// 更新app.js中的缓存
if (app.cloudUrlCache) {
app.cloudUrlCache[file.fileID] = file.tempFileURL;
} else {
app.cloudUrlCache = {};
app.cloudUrlCache[file.fileID] = file.tempFileURL;
}
}
});
}
// 替换所有cloud://路径
const convertedResults = results.map((item, itemIndex) => {
const convertedItem = { ...item };
if (convertedItem.images && Array.isArray(convertedItem.images)) {
convertedItem.images = convertedItem.images.map((img, imgIndex) => {
if (img && img.startsWith('cloud://')) {
const convertedUrl = urlMap[img];
if (convertedUrl) {
console.log('云存储路径转换成功:', img, '->', convertedUrl);
return convertedUrl;
} else {
console.warn('云存储路径转换失败未找到对应URL:', img);
// 转换失败时,使用默认图片
return item.type === 'lost' ? '/images/lost.png' : '/images/found.png';
}
}
return img;
});
}
return convertedItem;
});
callback(convertedResults);
},
fail: (err) => {
console.error('批量转换云存储路径失败:', err);
// 转换失败时使用默认图片替换cloud://路径
const fallbackResults = results.map(item => {
const fallbackItem = { ...item };
if (fallbackItem.images && Array.isArray(fallbackItem.images)) {
fallbackItem.images = fallbackItem.images.map(img => {
if (img && img.startsWith('cloud://')) {
return item.type === 'lost' ? '/images/lost.png' : '/images/found.png';
}
return img;
});
}
return fallbackItem;
});
callback(fallbackResults);
}
});
} else {
console.warn('不支持云开发API无法转换cloud://路径');
// 不支持时使用默认图片替换cloud://路径
const fallbackResults = results.map(item => {
const fallbackItem = { ...item };
if (fallbackItem.images && Array.isArray(fallbackItem.images)) {
fallbackItem.images = fallbackItem.images.map(img => {
if (img && img.startsWith('cloud://')) {
return item.type === 'lost' ? '/images/lost.png' : '/images/found.png';
}
return img;
});
}
return fallbackItem;
});
callback(fallbackResults);
}
},
// 点击搜索按钮
onSearch: function() {
if (this.data.searchType === 'text') {
this.searchByText();
} else if (this.data.searchType === 'image' && this.data.imagePath) {
this.searchByImage();
} else {
wx.showToast({
title: '请上传图片',
icon: 'none'
});
}
},
// 选择图片(改进版:添加图片预处理)
chooseImage: function() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
maxDuration: 30,
camera: 'back',
success: (res) => {
if (res.tempFiles && res.tempFiles.length > 0) {
const tempFilePath = res.tempFiles[0].tempFilePath;
// 压缩图片以提高搜索效率
wx.compressImage({
src: tempFilePath,
quality: 80, // 压缩质量80%质量已经足够
success: (compressRes) => {
console.log('图片压缩成功,原始大小:', res.tempFiles[0].size, '压缩后:', compressRes.tempFilePath);
this.setData({
imagePath: compressRes.tempFilePath,
searchResults: [],
pageNum: 1,
hasMore: true
});
},
fail: (err) => {
console.warn('图片压缩失败,使用原图:', err);
// 压缩失败时使用原图
this.setData({
imagePath: tempFilePath,
searchResults: [],
pageNum: 1,
hasMore: true
});
}
});
}
},
fail: (err) => {
console.error('选择图片失败', err);
wx.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
// 执行图片搜索(方案一:使用云函数)
searchByImage: function() {
if (!this.data.imagePath) {
wx.showToast({
title: '请先上传图片',
icon: 'none'
});
return;
}
this.setData({
loading: true,
searchResults: [],
pageNum: 1,
hasMore: true,
searchProgress: '正在上传图片...'
});
const that = this;
const imagePath = this.data.imagePath;
// 显示搜索进度
wx.showLoading({
title: '搜索中...',
mask: true
});
// 方案一:先上传图片到云存储,然后调用云函数
this.uploadImageAndSearch(imagePath, (res) => {
wx.hideLoading();
if (res && res.success && res.results) {
console.log('云函数搜索返回结果数量:', res.results.length);
// 处理搜索结果
const app = getApp();
const processedResults = res.results.map(item => {
// 使用app.js中的processItemImages函数处理图片路径
const processedItem = app.processItemImages(item);
// 确保每个item都有images字段且为数组
if (!processedItem.images || !Array.isArray(processedItem.images) || processedItem.images.length === 0) {
processedItem.images = [processedItem.type === 'lost' ? '/images/lost.png' : '/images/found.png'];
}
return {
...processedItem,
similarity: (item.similarity * 100).toFixed(1) + '%', // 转换为百分比显示
similarityValue: item.similarity, // 保留原始数值用于排序
isUserPublished: false, // 从云数据库获取的都是其他用户的物品
isAISimilarity: true
};
});
// 按相似度排序(确保顺序正确)
processedResults.sort((a, b) => (b.similarityValue || 0) - (a.similarityValue || 0));
that.processSearchResults(processedResults, false);
that.setData({
hasMore: false, // 云函数返回的是最终结果
searchProgress: ''
});
} else {
console.error('云函数搜索失败:', res);
wx.showToast({
title: res.error || '搜索失败,请重试',
icon: 'none',
duration: 2000
});
that.setData({
loading: false,
searchProgress: ''
});
// 降级到本地搜索
console.log('降级到本地搜索');
const app = getApp();
app.searchItemsByImage(imagePath, (localRes) => {
if (localRes && localRes.items) {
console.log('本地搜索返回结果数量:', localRes.items.length);
// 处理本地搜索结果的相似度格式
const processedLocalResults = localRes.items.map(item => {
const processedItem = app.processItemImages(item);
// 确保每个item都有images字段且为数组
if (!processedItem.images || !Array.isArray(processedItem.images) || processedItem.images.length === 0) {
processedItem.images = [processedItem.type === 'lost' ? '/images/lost.png' : '/images/found.png'];
}
// 处理相似度:确保格式正确
let similarityDisplay = '0%';
if (item.similarity !== undefined && item.similarity !== null) {
if (typeof item.similarity === 'number') {
similarityDisplay = (item.similarity * 100).toFixed(1) + '%';
} else if (typeof item.similarity === 'string') {
similarityDisplay = item.similarity;
}
}
return {
...processedItem,
similarity: similarityDisplay,
similarityValue: typeof item.similarity === 'number' ? item.similarity : (item.similarityValue || 0),
isAISimilarity: true
};
});
that.processSearchResults(processedLocalResults, false);
that.setData({
hasMore: localRes.hasMore || false,
searchProgress: ''
});
} else {
that.setData({
loading: false,
searchProgress: ''
});
}
});
}
});
},
// 上传图片并调用云函数搜索
uploadImageAndSearch: function(imagePath, callback) {
console.log('开始上传图片到云存储并搜索');
// 1. 先上传图片到云存储
const fileName = 'search_images/' + Date.now() + '_' + Math.floor(Math.random() * 1000) + '.jpg';
wx.cloud.uploadFile({
cloudPath: fileName,
filePath: imagePath,
success: (uploadRes) => {
console.log('图片上传成功文件ID:', uploadRes.fileID);
// 2. 调用云函数进行搜索
wx.cloud.callFunction({
name: 'imageSearch',
data: {
imageFileID: uploadRes.fileID
},
success: (cloudRes) => {
console.log('云函数调用成功:', cloudRes.result);
// 删除临时上传的搜索图片(可选)
wx.cloud.deleteFile({
fileList: [uploadRes.fileID],
success: () => {
console.log('临时搜索图片已删除');
},
fail: (err) => {
console.warn('删除临时图片失败:', err);
}
});
callback(cloudRes.result);
},
fail: (err) => {
console.error('云函数调用失败:', err);
callback({
success: false,
error: err.errMsg || '云函数调用失败'
});
}
});
},
fail: (err) => {
console.error('图片上传失败:', err);
callback({
success: false,
error: err.errMsg || '图片上传失败'
});
}
});
},
// 加载更多搜索结果(改进版)
loadMore: function() {
if (this.data.loading || !this.data.hasMore) return;
this.setData({ loading: true });
const app = getApp();
const pageNum = this.data.pageNum + 1;
if (this.data.searchType === 'text') {
app.searchItemsByText(this.data.keyword, pageNum, (res) => {
if (res && res.items) {
this.processSearchResults(res.items, true);
this.setData({
pageNum: pageNum,
hasMore: res.items.length === 10
});
} else {
this.setData({ loading: false });
}
});
} else if (this.data.searchType === 'image') {
// 图片搜索加载更多
app.searchItemsByImage(this.data.imagePath, pageNum, (res) => {
if (res && res.items) {
// 处理搜索结果,确保图片路径正确
const processedResults = res.items.map(item => {
const processedItem = app.processItemImages(item);
if (!processedItem.images || !Array.isArray(processedItem.images) || processedItem.images.length === 0) {
processedItem.images = [processedItem.type === 'lost' ? '/images/lost.png' : '/images/found.png'];
}
// 处理相似度:确保格式正确
let similarityDisplay = '0%';
if (item.similarity !== undefined && item.similarity !== null) {
if (typeof item.similarity === 'number') {
similarityDisplay = (item.similarity * 100).toFixed(1) + '%';
} else if (typeof item.similarity === 'string') {
similarityDisplay = item.similarity;
}
}
return {
...processedItem,
similarity: similarityDisplay,
similarityValue: typeof item.similarity === 'number' ? item.similarity : (item.similarityValue || 0),
isAISimilarity: true
};
});
this.processSearchResults(processedResults, true);
this.setData({
pageNum: pageNum,
hasMore: res.hasMore
});
} else {
this.setData({ loading: false });
}
});
}
},
// 跳转到物品详情页
goToDetail: function(e) {
const itemId = e.currentTarget.dataset.id;
const type = e.currentTarget.dataset.type;
wx.navigateTo({
url: `/pages/detail/detail?id=${itemId}&type=${type}`
});
},
// 预览已选择的图片
previewSelectedImage: function() {
if (this.data.imagePath) {
wx.previewImage({
current: this.data.imagePath,
urls: [this.data.imagePath]
});
}
},
// 清除图片
clearImage: function() {
this.setData({
imagePath: '',
searchResults: []
});
},
// 点击键盘上的搜索按钮
onConfirm: function() {
if (this.data.searchType === 'text') {
this.searchByText();
}
},
// 图片加载错误处理
onImageError: function(e) {
const index = e.currentTarget.dataset.index;
const item = this.data.searchResults[index];
if (item && item.images && item.images[0]) {
const imagePath = item.images[0];
console.warn('搜索页面图片加载失败,索引:', index, '路径:', imagePath);
// 如果是cloud://路径,尝试重新转换
if (imagePath.startsWith('cloud://')) {
const app = getApp();
app.convertCloudPathToUrl(imagePath, (convertedUrl) => {
if (convertedUrl && convertedUrl !== imagePath) {
// 更新图片路径
const updatedResults = [...this.data.searchResults];
updatedResults[index].images[0] = convertedUrl;
this.setData({
searchResults: updatedResults
});
} else {
// 转换失败,使用默认图片
const updatedResults = [...this.data.searchResults];
updatedResults[index].images[0] = item.type === 'lost' ? '/images/lost.png' : '/images/found.png';
this.setData({
searchResults: updatedResults
});
}
});
} else {
// 非cloud://路径加载失败,使用默认图片
const updatedResults = [...this.data.searchResults];
updatedResults[index].images[0] = item.type === 'lost' ? '/images/lost.png' : '/images/found.png';
this.setData({
searchResults: updatedResults
});
}
}
}
});

@ -0,0 +1,50 @@
<!-- pages/search/search.wxml -->
<view class="container">
<!-- 文字搜索框 -->
<view class="search-bar">
<view class="search-input-container">
<image class="search-icon" src="/images/search.png"></image>
<input class="search-input"
placeholder="输入物品名称、特征等关键词"
value="{{keyword}}"
bindinput="onInput"
bindconfirm="onConfirm"
focus="{{true}}"></input>
</view>
<button class="search-button" type="primary" bindtap="onSearch" loading="{{loading}}">搜索</button>
</view>
<!-- 搜索结果区域 -->
<view class="results-area" wx:if="{{searchResults.length > 0}}">
<view class="results-header">
<text>搜索结果 ({{searchResults.length}})</text>
</view>
<view class="results-list">
<view class="item-card" wx:for="{{searchResults}}" wx:key="id" data-id="{{item.id}}" data-type="{{item.type}}" bindtap="goToDetail">
<image class="item-image" wx:if="{{item.images && item.images.length > 0}}" src="{{item.images[0]}}" mode="aspectFill" binderror="onImageError" data-index="{{index}}"></image>
<view class="item-info">
<text class="item-title">{{item.title}}</text>
<text class="item-desc">{{item.description}}</text>
<view class="item-meta">
<text class="item-time">{{item.time}}</text>
<text class="item-location">{{item.location}}</text>
</view>
<view class="item-info-bottom">
<text class="item-type">{{item.type === 'lost' ? '失物' : '招领'}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty" wx:if="{{(searchResults.length === 0 && !loading)}}">
<image class="empty-icon" src="/images/empty.png" mode="aspectFit"></image>
<text class="empty-text">请输入关键词进行搜索</text>
</view>
<!-- 加载中 -->
<view class="loading" wx:if="{{loading}}">
<text>搜索中...</text>
</view>
</view>

@ -0,0 +1,312 @@
/* pages/search/search.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
box-sizing: border-box;
}
/* 搜索类型切换 */
.search-type {
display: flex;
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.type-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
}
.type-tab.active {
background-color: #2196F3;
color: #fff;
}
/* 文字搜索框 */
.search-bar {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.search-input-container {
flex: 1;
display: flex;
align-items: center;
height: 72rpx;
background-color: #fff;
border-radius: 36rpx;
padding: 0 30rpx;
margin-right: 20rpx;
}
.search-icon {
width: 32rpx;
height: 32rpx;
margin-right: 16rpx;
}
.search-input {
flex: 1;
height: 100%;
font-size: 28rpx;
}
/* 搜索按钮 - 增大尺寸(统一应用于文字和图片搜索)*/
.search-button {
width: 200rpx;
height: 90rpx;
font-size: 28rpx;
line-height: 90rpx;
background-color: #2196F3;
margin-bottom: 20rpx;
border-radius: 12rpx;
}
/* 图片搜索区域 */
.image-search-area {
display: flex;
flex-direction: column;
}
.image-container {
width: 100%;
height: 400rpx;
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
position: relative;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border: 2rpx dashed #ddd;
}
.selected-image {
width: 100%;
height: 100%;
}
.upload-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #999;
font-size: 28rpx;
background-color: #fafafa;
}
.upload-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 20rpx;
opacity: 0.5;
}
.upload-hint {
font-size: 24rpx;
color: #ccc;
margin-top: 10rpx;
}
.button-group {
display: flex;
gap: 20rpx;
margin-bottom: 20rpx;
}
.button-group .search-button {
flex: 1;
margin-bottom: 0;
}
.search-hint {
background-color: #e3f2fd;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
font-size: 24rpx;
color: #1976d2;
line-height: 1.6;
}
.search-progress {
text-align: center;
padding: 20rpx;
font-size: 24rpx;
color: #666;
background-color: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.clear-button {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 40rpx;
height: 40rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 32rpx;
}
/* 搜索按钮 - 增大尺寸 */
.search-button {
width: 200rpx;
height: 90rpx;
font-size: 28rpx;
line-height: 90rpx;
background-color: #2196F3;
margin-bottom: 20rpx;
border-radius: 12rpx;
}
/* 搜索结果区域 */
.results-area {
flex: 1;
overflow-y: auto;
}
.results-header {
padding: 20rpx 0;
font-size: 28rpx;
color: #333;
border-bottom: 1rpx solid #eee;
margin-bottom: 20rpx;
}
.results-list {
/* 结果列表样式 */
}
/* 物品卡片 */
.item-card {
display: flex;
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 24rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.item-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
margin-right: 20rpx;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.item-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-desc {
font-size: 28rpx;
color: #666;
line-height: 40rpx;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-meta {
display: flex;
justify-content: space-between;
margin-bottom: 8rpx;
}
.item-time, .item-location {
font-size: 24rpx;
color: #999;
}
.item-type {
font-size: 24rpx;
color: #fff;
background-color: #2196F3;
padding: 4rpx 16rpx;
border-radius: 16rpx;
align-self: flex-start;
}
/* 物品底部信息容器 */
.item-info-bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8rpx;
width: 100%;
}
/* 相似度分数样式 */
.similarity-score {
font-size: 24rpx;
color: #4CAF50;
font-weight: bold;
}
/* 空状态 */
.empty {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 加载中 */
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0;
}
.loading text {
font-size: 28rpx;
color: #999;
}

@ -0,0 +1,393 @@
// pages/settings/settings.js
Page({
data: {
userInfo: null,
hasUserInfo: false,
appVersion: '1.0.0',
settings: {
receiveNotifications: true,
enableSmartMatch: true,
allowLocationAccess: true
},
cloudStatus: 'unknown', // 'success', 'warning', 'error', 'unknown'
cloudStatusText: '未检测',
cloudEnvId: 'cloud1-4gtth1kue3bec7ef',
cloudTestResult: ''
},
onLoad: function() {
// 检查用户登录状态
this.checkLoginStatus();
// 优先从全局存储中获取设置
const globalSettings = wx.getStorageSync('userSettings');
const localSettings = wx.getStorageSync('settings') || {};
// 合并设置,全局设置优先
const settings = { ...localSettings, ...globalSettings,
allowLocationAccess: (globalSettings && globalSettings.allowLocationAccess !== undefined) ? globalSettings.allowLocationAccess :
(localSettings && localSettings.allowLocationAccess !== undefined) ? localSettings.allowLocationAccess :
this.data.settings.allowLocationAccess,
enableSmartMatch: (globalSettings && globalSettings.enableSmartMatch !== undefined) ?
globalSettings.enableSmartMatch :
(localSettings && localSettings.enableSmartMatch !== undefined) ?
localSettings.enableSmartMatch :
this.data.settings.enableSmartMatch
};
// 从本地存储读取设置
this.loadSettings();
// 更新全局设置
const app = getApp();
if (app.updateSettings) {
app.updateSettings(settings);
}
// 检查云开发环境状态
this.checkCloudStatus();
},
onShow: function() {
// 每次页面显示时,刷新用户信息
this.checkLoginStatus();
// 刷新云开发状态
this.checkCloudStatus();
},
// 检查登录状态(简化版)
checkLoginStatus: function() {
const app = getApp();
// 获取用户信息或使用默认值
const userInfo = app.globalData && app.globalData.userInfo ?
app.globalData.userInfo :
{ nickName: '失物招领用户', avatarUrl: '/images/default_avatar.svg' };
this.setData({
userInfo: userInfo,
hasUserInfo: !!app.globalData?.hasUserInfo || true // 默认认为已登录,使用模拟数据
});
},
// 从本地存储读取设置
loadSettings: function() {
const receiveNotifications = wx.getStorageSync('receiveNotifications');
const enableSmartMatch = wx.getStorageSync('enableSmartMatch');
const allowLocationAccess = wx.getStorageSync('allowLocationAccess');
// 如果本地存储有值,则使用存储的值;否则使用默认值
this.setData({
settings: {
receiveNotifications: receiveNotifications !== undefined ? receiveNotifications : this.data.settings.receiveNotifications,
enableSmartMatch: enableSmartMatch !== undefined ? enableSmartMatch : this.data.settings.enableSmartMatch,
allowLocationAccess: allowLocationAccess !== undefined ? allowLocationAccess : this.data.settings.allowLocationAccess
}
});
},
// 保存设置到本地存储
saveSettings: function() {
wx.setStorageSync('receiveNotifications', this.data.settings.receiveNotifications);
wx.setStorageSync('enableSmartMatch', this.data.settings.enableSmartMatch);
wx.setStorageSync('allowLocationAccess', this.data.settings.allowLocationAccess);
// 同时更新全局存储确保app.js能够读取到
wx.setStorageSync('userSettings', this.data.settings);
// 同步设置到服务器
this.syncSettingsToServer();
},
// 同步设置到服务器(本地模拟版本)
syncSettingsToServer: function() {
// 使用本地模拟操作代替网络请求
setTimeout(() => {
console.log('设置同步成功(本地模拟)');
// 更新全局设置
const app = getApp();
if (app.globalData) {
app.globalData.userSettings = this.data.settings;
}
}, 200);
},
// 切换通知开关
toggleNotifications: function(e) {
const enabled = e.detail.value;
this.setData({
'settings.receiveNotifications': enabled
});
// 保存设置
this.saveSettings();
// 如果开启通知,请求订阅消息授权
if (enabled) {
this.requestNotificationPermission();
}
},
// 请求订阅消息授权
requestNotificationPermission: function() {
wx.requestSubscribeMessage({
tmplIds: ['your_template_id_for_notification'], // 替换为你的模板ID
success: (res) => {
console.log('订阅消息授权结果', res);
},
fail: (err) => {
console.error('请求订阅消息授权失败', err);
}
});
},
// 切换智能匹配开关
toggleSmartMatch: function(e) {
const enabled = e.detail.value;
this.setData({
'settings.enableSmartMatch': enabled
});
// 保存设置
this.saveSettings();
// 更新全局设置
const app = getApp();
app.updateSettings({ enableSmartMatch: enabled }, () => {
// 设置保存成功后的提示
wx.showToast({
title: enabled ? '智能匹配已开启' : '智能匹配已关闭',
icon: 'none',
duration: 1500
});
});
},
// 切换位置权限开关
toggleLocationAccess: function(e) {
const enabled = e.detail.value;
this.setData({
'settings.allowLocationAccess': enabled
});
// 保存设置
this.saveSettings();
// 如果开启位置权限,请求位置授权
if (enabled) {
this.requestLocationPermission();
}
},
// 请求位置授权
requestLocationPermission: function() {
wx.authorize({
scope: 'scope.userLocation',
success: () => {
console.log('位置授权成功');
},
fail: () => {
console.error('位置授权失败');
wx.showModal({
title: '需要位置权限',
content: '开启位置权限后,您可以更准确地获取附近的失物招领信息',
success: (res) => {
if (res.confirm) {
wx.openSetting({
success: (res) => {
console.log('设置结果', res);
}
});
}
}
});
}
});
},
// 跳转到编辑个人资料页面
editUserProfile: function() {
wx.navigateTo({
url: '/pages/profile/profile'
});
},
// 跳转到隐私政策页面
viewPrivacyPolicy: function() {
wx.navigateTo({
url: '/pages/privacy/privacy'
});
},
// 跳转到用户协议页面
viewUserAgreement: function() {
wx.navigateTo({
url: '/pages/agreement/agreement'
});
},
// 清除缓存
clearCache: function() {
wx.showModal({
title: '清除缓存',
content: '确定要清除所有缓存吗?',
success: (res) => {
if (res.confirm) {
wx.clearStorageSync();
wx.showToast({
title: '缓存已清除',
icon: 'success'
});
}
}
});
},
// 关于我们
aboutUs: function() {
wx.navigateTo({
url: '/pages/about/about'
});
},
// 检查云开发环境状态
checkCloudStatus: function() {
const app = getApp();
const db = app.globalData.db;
const validated = app.globalData.cloudEnvValidated;
let status = 'unknown';
let statusText = '未检测';
if (!db) {
status = 'error';
statusText = '不可用';
} else if (validated === true) {
status = 'success';
statusText = '正常';
} else if (validated === false) {
status = 'error';
statusText = '配置错误';
} else {
status = 'warning';
statusText = '未验证';
}
this.setData({
cloudStatus: status,
cloudStatusText: statusText
});
},
// 测试云开发连接
testCloudConnection: function() {
wx.showLoading({
title: '测试中...',
mask: true
});
const app = getApp();
const db = app.globalData.db;
if (!db) {
wx.hideLoading();
this.setData({
cloudTestResult: '❌ 云数据库引用不存在\n请检查云开发环境配置',
cloudStatus: 'error',
cloudStatusText: '不可用'
});
return;
}
// 测试数据库连接
db.collection('items').limit(1).get({
success: (res) => {
wx.hideLoading();
const result = `✅ 连接成功!\n\n环境ID: ${this.data.cloudEnvId}\n数据库访问: 正常\n返回数据: ${res.data.length}\n\n跨设备数据共享已启用`;
this.setData({
cloudTestResult: result,
cloudStatus: 'success',
cloudStatusText: '正常'
});
// 更新全局验证状态
app.globalData.cloudEnvValidated = true;
wx.showToast({
title: '连接成功',
icon: 'success',
duration: 2000
});
},
fail: (err) => {
wx.hideLoading();
const isEnvError = err.errCode === -501000 || (err.errMsg && err.errMsg.includes('env not exists'));
let result = '';
let status = 'error';
let statusText = '配置错误';
if (isEnvError) {
result = `❌ 环境不存在\n\n错误: ${err.errMsg || err}\n\n请检查:\n1. 环境ID是否正确\n2. 是否在云开发控制台创建了环境\n3. 小程序AppID是否已关联云开发环境`;
} else {
result = `⚠️ 连接失败\n\n错误: ${err.errMsg || err}\n\n可能是权限问题,请检查数据库集合权限设置`;
status = 'warning';
statusText = '权限问题';
}
this.setData({
cloudTestResult: result,
cloudStatus: status,
cloudStatusText: statusText
});
if (isEnvError) {
app.globalData.cloudEnvValidated = false;
app.globalData.db = null;
}
wx.showToast({
title: '连接失败',
icon: 'none',
duration: 3000
});
}
});
},
// 退出登录
logout: function() {
wx.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
// 清除用户信息和token
const app = getApp();
if (app.globalData) {
app.globalData.userInfo = null;
app.globalData.hasUserInfo = false;
}
// 移除token但保留设置
try {
const settings = wx.getStorageSync('userSettings');
wx.clearStorageSync();
// 重新保存设置
if (settings) {
wx.setStorageSync('userSettings', settings);
}
} catch (e) {
console.error('清除存储失败', e);
}
// 返回首页
wx.switchTab({
url: '/pages/index/index'
});
}
}
});
}
});

@ -0,0 +1,107 @@
<!-- pages/settings/settings.wxml -->
<view class="container">
<!-- 顶部标题 -->
<view class="header">
<text class="title">设置</text>
</view>
<!-- 个人设置 -->
<view class="setting-section">
<view class="section-title">个人设置</view>
<view class="setting-item" bindtap="editUserProfile">
<text class="setting-label">编辑个人资料</text>
<view class="setting-arrow">
<text class="arrow">→</text>
</view>
</view>
</view>
<!-- 功能设置 -->
<view class="setting-section">
<view class="section-title">功能设置</view>
<view class="setting-item">
<text class="setting-label">接收通知</text>
<switch checked="{{settings.receiveNotifications}}" bindchange="toggleNotifications"></switch>
</view>
<view class="setting-item">
<text class="setting-label">智能匹配</text>
<switch checked="{{settings.enableSmartMatch}}" bindchange="toggleSmartMatch"></switch>
</view>
<view class="setting-item">
<text class="setting-label">位置权限</text>
<switch checked="{{settings.allowLocationAccess}}" bindchange="toggleLocationAccess"></switch>
</view>
</view>
<!-- 云开发环境状态 -->
<view class="setting-section">
<view class="section-title">云开发环境</view>
<view class="setting-item">
<text class="setting-label">环境状态</text>
<text class="cloud-status {{cloudStatus === 'success' ? 'success' : cloudStatus === 'warning' ? 'warning' : 'error'}}">
{{cloudStatusText}}
</text>
</view>
<view class="setting-item" wx:if="{{cloudEnvId}}">
<text class="setting-label">环境ID</text>
<text class="cloud-env-id">{{cloudEnvId}}</text>
</view>
<view class="setting-item">
<button class="test-cloud-btn" type="primary" size="mini" bindtap="testCloudConnection">测试连接</button>
</view>
<view class="cloud-test-result" wx:if="{{cloudTestResult}}">
<text class="result-text">{{cloudTestResult}}</text>
</view>
</view>
<!-- 隐私设置 -->
<view class="setting-section">
<view class="section-title">隐私设置</view>
<view class="setting-item" bindtap="viewPrivacyPolicy">
<text class="setting-label">隐私政策</text>
<view class="setting-arrow">
<text class="arrow">→</text>
</view>
</view>
<view class="setting-item" bindtap="viewUserAgreement">
<text class="setting-label">用户协议</text>
<view class="setting-arrow">
<text class="arrow">→</text>
</view>
</view>
</view>
<!-- 其他设置 -->
<view class="setting-section">
<view class="section-title">其他</view>
<view class="setting-item" bindtap="clearCache">
<text class="setting-label">清除缓存</text>
<view class="setting-arrow">
<text class="arrow">→</text>
</view>
</view>
<view class="setting-item" bindtap="aboutUs">
<text class="setting-label">关于我们</text>
<view class="setting-arrow">
<text class="arrow">→</text>
</view>
</view>
</view>
<!-- 版本信息 -->
<view class="version-info">
<text>当前版本v{{appVersion}}</text>
</view>
<!-- 退出登录按钮 -->
<view class="logout-section">
<button class="logout-btn" type="warn" bindtap="logout">退出登录</button>
</view>
</view>

@ -0,0 +1,133 @@
/* pages/settings/settings.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 顶部标题 */
.header {
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
/* 设置分组 */
.setting-section {
margin-top: 20rpx;
background-color: #fff;
}
.section-title {
padding: 20rpx 30rpx;
font-size: 24rpx;
color: #999;
background-color: #f9f9f9;
}
/* 设置项 */
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
}
.setting-label {
font-size: 28rpx;
color: #333;
}
.setting-arrow {
color: #ddd;
}
.arrow {
font-size: 28rpx;
}
/* 开关样式 */
switch {
transform: scale(0.8);
}
/* 版本信息 */
.version-info {
text-align: center;
padding: 40rpx 0;
color: #999;
font-size: 24rpx;
}
/* 退出登录按钮 */
.logout-section {
padding: 30rpx;
}
.logout-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
font-size: 28rpx;
border-radius: 8rpx;
}
/* 最后一个设置项没有下边框 */
.setting-section:last-child .setting-item {
border-bottom: none;
}
/* 云开发状态样式 */
.cloud-status {
font-size: 24rpx;
padding: 6rpx 12rpx;
border-radius: 4rpx;
font-weight: bold;
}
.cloud-status.success {
color: #4CAF50;
background-color: #E8F5E9;
}
.cloud-status.warning {
color: #FF9800;
background-color: #FFF3E0;
}
.cloud-status.error {
color: #F44336;
background-color: #FFEBEE;
}
.cloud-env-id {
font-size: 24rpx;
color: #666;
font-family: monospace;
}
.test-cloud-btn {
margin-top: 10rpx;
}
.cloud-test-result {
padding: 20rpx 30rpx;
margin-top: 10rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
.result-text {
font-size: 24rpx;
color: #333;
line-height: 1.6;
white-space: pre-wrap;
}

@ -0,0 +1,459 @@
// pages/user/user.js
Page({
data: {
userInfo: {}, // 用户信息
myItems: [], // 我发布的物品
myCollections: [], // 我的收藏
currentTab: 'published', // 当前选中的标签
loading: false, // 是否正在加载
pageNum: 1, // 当前页码
hasMore: true // 是否还有更多数据
},
onLoad: function() {
// 检查用户登录状态
this.checkLoginStatus();
// 确保用户信息初始化正确
if (!this.data.userInfo.nickName && !this.data.userInfo.avatarUrl) {
this.setData({
userInfo: {
nickName: '失物招领用户',
avatarUrl: '/images/default_avatar.svg'
}
});
}
// 初始化页面,不依赖登录状态也能显示内容
this.loadMyItems();
},
onShow: function() {
// 更新消息角标
const app = getApp();
if (app && app.updateTabBarBadge) {
app.updateTabBarBadge();
}
// 每次页面显示时,检查登录状态和刷新数据
this.checkLoginStatus();
// 重新加载当前标签页数据
if (this.data.currentTab === 'published') {
this.loadMyItems();
} else {
this.loadMyCollections();
}
},
// 检查登录状态(简化版,不依赖实际登录)
checkLoginStatus: function() {
const app = getApp();
// 设置登录状态或使用默认用户信息
if (app.globalData.userInfo) {
this.setData({
userInfo: app.globalData.userInfo
});
} else {
// 模拟用户已登录状态,使用默认用户信息
this.setData({
userInfo: {
nickName: '失物招领用户',
avatarUrl: '/images/default_avatar.svg'
}
});
}
return true;
},
// 获取用户信息(简化版,不依赖实际登录)
getUserInfo: function(e) {
if (e.detail.userInfo) {
const app = getApp();
// 设置全局用户信息
app.globalData.userInfo = e.detail.userInfo;
// 设置页面用户信息
this.setData({
userInfo: e.detail.userInfo
});
// 模拟登录成功
wx.setStorageSync('token', 'mock_token');
// 重新加载数据
this.loadMyItems();
}
},
// 调用getUserProfile获取用户信息简化版
getUserProfile: function() {
const that = this;
try {
wx.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
const app = getApp();
// 设置全局用户信息
app.globalData.userInfo = res.userInfo;
// 设置页面用户信息
that.setData({
userInfo: res.userInfo
});
// 模拟登录成功
wx.setStorageSync('token', 'mock_token');
// 重新加载数据
that.loadMyItems();
},
fail: () => {
console.log('用户取消授权');
}
});
} catch (e) {
// 如果wx.getUserProfile不可用使用默认信息
console.log('获取用户信息接口不可用,使用默认信息');
}
},
// 登录到服务器简化版使用模拟token
loginToServer: function() {
// 模拟登录成功直接设置token
wx.setStorageSync('token', 'mock_token');
// 加载用户数据
this.loadMyItems();
},
// 切换标签
switchTab: function(e) {
const tab = e.currentTarget.dataset.tab;
if (tab === this.data.currentTab) return;
this.setData({
currentTab: tab,
pageNum: 1,
hasMore: true
});
if (tab === 'published') {
this.loadMyItems();
} else if (tab === 'collections') {
this.loadMyCollections();
}
},
// 加载我发布的物品
loadMyItems: function() {
if (this.data.loading || !this.data.hasMore) return;
this.setData({ loading: true });
const app = getApp();
const db = app.globalData && app.globalData.db;
const useCloudDB = db && wx.cloud && app.globalData.cloudEnvValidated !== false;
const openid = wx.getStorageSync('openid') || 'local_user';
const pageSize = 10;
const applyPaging = (allItems) => {
// 将完整列表缓存到全局,供其他地方使用
app.globalData.userPublishedItems = allItems;
const startIndex = (this.data.pageNum - 1) * pageSize;
const endIndex = startIndex + pageSize;
const newItems = allItems.slice(startIndex, endIndex);
if (this.data.pageNum === 1) {
this.setData({
myItems: newItems,
pageNum: 2,
hasMore: newItems.length === pageSize
});
} else {
this.setData({
myItems: [...this.data.myItems, ...newItems],
pageNum: this.data.pageNum + 1,
hasMore: newItems.length === pageSize
});
}
this.setData({ loading: false });
};
// 优先从云数据库获取“我发布的”记录
if (useCloudDB) {
db.collection('items')
.where({ _openid: openid })
.orderBy('publishTime', 'desc')
.get({
success: res => {
const items = (res.data || []).map(item => ({
id: item._id,
type: item.type,
title: item.title,
description: item.description,
location: item.location,
time: item.time || item.publishTime || item.createTime,
images: item.images || [],
isUserPublished: true,
status: item.status || 'pending'
}));
applyPaging(items);
},
fail: () => {
// 失败则退回到仅使用内存中的已发布数据
const localItems = (app.globalData && app.globalData.userPublishedItems) || [];
applyPaging(localItems);
}
});
} else {
// 云数据库不可用,仅使用内存中的已发布数据
const localItems = (app.globalData && app.globalData.userPublishedItems) || [];
applyPaging(localItems);
}
},
// 加载我的收藏
loadMyCollections: function() {
if (this.data.loading || !this.data.hasMore) return;
this.setData({ loading: true });
// 使用本地模拟数据代替网络请求
setTimeout(() => {
// 从全局数据中获取用户收藏的物品
let collections = [];
const app = getApp();
if (app.globalData && app.globalData.userCollections) {
collections = app.globalData.userCollections;
} else {
// 如果没有收藏,生成一些模拟数据
collections = this.generateMockCollections();
}
// 分页处理
const pageSize = 10;
const startIndex = (this.data.pageNum - 1) * pageSize;
const endIndex = startIndex + pageSize;
const newItems = collections.slice(startIndex, endIndex);
if (this.data.pageNum === 1) {
this.setData({
myCollections: newItems,
pageNum: 2,
hasMore: newItems.length === 10
});
} else {
this.setData({
myCollections: [...this.data.myCollections, ...newItems],
pageNum: this.data.pageNum + 1,
hasMore: newItems.length === 10
});
}
this.setData({ loading: false });
}, 600);
},
// 生成模拟的收藏数据
generateMockCollections: function() {
const mockCollections = [
{
id: 'mock_collect_1',
type: 'lost',
title: '丢失的书包',
description: '蓝色书包,内有笔记本和文具',
location: '教学楼B区',
time: new Date(Date.now() - 43200000).toISOString().split('T')[0],
images: ['/images/match.png'],
isUserPublished: false,
publisher: '张三'
},
{
id: 'mock_collect_2',
type: 'found',
title: '捡到的学生证',
description: '2020级学号20200001',
location: '餐厅',
time: new Date(Date.now() - 604800000).toISOString().split('T')[0],
images: ['/images/found.png'],
isUserPublished: false,
publisher: '李四'
}
];
// 将模拟数据保存到全局
const app = getApp();
if (!app.globalData) app.globalData = {};
app.globalData.userCollections = mockCollections;
return mockCollections;
},
// 跳转到物品详情页
goToDetail: function(e) {
const itemId = e.currentTarget.dataset.id;
const type = e.currentTarget.dataset.type;
wx.navigateTo({
url: `/pages/detail/detail?id=${itemId}&type=${type}`
});
},
// 删除发布的物品
deleteItem: function(e) {
const id = e.currentTarget.dataset.id;
const index = e.currentTarget.dataset.index;
wx.showModal({
title: '确认删除',
content: '确定要删除该物品吗?删除后无法恢复。',
success: res => {
if (res.confirm) {
// 使用本地模拟操作代替网络请求
setTimeout(() => {
wx.showToast({
title: '删除成功',
icon: 'success'
});
// 更新物品列表
const updatedItems = [...this.data.myItems];
updatedItems.splice(index, 1);
this.setData({
myItems: updatedItems
});
// 更新全局数据
const app = getApp();
if (app.globalData && app.globalData.userPublishedItems) {
app.globalData.userPublishedItems = updatedItems;
}
}, 300);
}
}
});
},
// 取消收藏
cancelCollection: function(e) {
const id = e.currentTarget.dataset.id;
const index = e.currentTarget.dataset.index;
wx.showModal({
title: '确认取消收藏',
content: '确定要取消收藏该物品吗?',
success: res => {
if (res.confirm) {
// 使用本地模拟操作代替网络请求
setTimeout(() => {
wx.showToast({
title: '取消收藏成功',
icon: 'success'
});
// 从收藏列表中移除该物品
const newCollections = [...this.data.myCollections];
newCollections.splice(index, 1);
this.setData({
myCollections: newCollections
});
// 更新全局数据
const app = getApp();
if (app.globalData && app.globalData.userCollections) {
app.globalData.userCollections = newCollections;
}
}, 300);
}
}
});
},
// 跳转到设置页面
navigateToSettings: function() {
wx.navigateTo({
url: '/pages/settings/settings'
});
},
// 跳转到关于我们页面
navigateToAbout: function() {
wx.navigateTo({
url: '/pages/about/about'
});
},
// 跳转到智能匹配页面
navigateToMatch: function() {
wx.navigateTo({
url: '/pages/match/match'
});
},
// 跳转到智能匹配页面
navigateToMatch: function() {
wx.navigateTo({
url: '/pages/match/match'
});
},
// 跳转到物品详情页
navigateToDetail: function(e) {
const { id, type } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/detail/detail?id=${id}&type=${type}`
});
},
// 跳转到发布页面
navigateToPublish: function() {
wx.navigateTo({
url: '/pages/publish/publish'
});
},
// 跳转到首页
navigateToHome: function() {
wx.switchTab({
url: '/pages/index/index'
});
},
// 加载更多
onReachBottom: function() {
if (this.data.currentTab === 'published') {
this.loadMyItems();
} else if (this.data.currentTab === 'collections') {
this.loadMyCollections();
}
},
// 下拉刷新
onPullDownRefresh: function() {
this.setData({
pageNum: 1,
hasMore: true
});
if (this.data.currentTab === 'published') {
this.loadMyItems();
} else if (this.data.currentTab === 'collections') {
this.loadMyCollections();
}
// 停止下拉刷新动画
wx.stopPullDownRefresh();
}
});

@ -0,0 +1,112 @@
<view class="container">
<!-- 用户信息区域 -->
<view class="user-info-section">
<image class="avatar" src="{{userInfo.avatar || '/images/default_avatar.svg'}}"></image>
<view class="user-details">
<view class="user-name">{{userInfo.nickName || '游客用户'}}</view>
<view class="user-id">ID: {{userInfo.openId || '未登录'}}</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-section">
<view class="menu-item" bindtap="navigateToSettings">
<image class="menu-icon" src="/images/settings.svg"></image>
<text class="menu-text">设置</text>
<image class="menu-arrow" src="/images/website.png"></image>
</view>
<view class="menu-item" bindtap="navigateToMatch">
<image class="menu-icon" src="/images/match.svg"></image>
<text class="menu-text">智能匹配</text>
<image class="menu-arrow" src="/images/website.png"></image>
</view>
<view class="menu-item" bindtap="navigateToAbout">
<image class="menu-icon" src="/images/system.svg"></image>
<text class="menu-text">关于我们</text>
<image class="menu-arrow" src="/images/website.png"></image>
</view>
</view>
<!-- 我的发布和收藏标签页 -->
<view class="tabs">
<view class="tab {{currentTab === 'published' ? 'active' : ''}}" bindtap="switchTab" data-tab="published">
<text>我发布的</text>
</view>
<view class="tab {{currentTab === 'collections' ? 'active' : ''}}" bindtap="switchTab" data-tab="collections">
<text>我的收藏</text>
</view>
</view>
<!-- 我发布的物品列表 -->
<view class="content" wx:if="{{currentTab === 'published'}}">
<view class="item-list" wx:if="{{myItems.length > 0}}">
<view class="item" wx:for="{{myItems}}" wx:key="id">
<view class="item-left">
<image class="item-image" src="{{item.images[0] || '/images/empty.png'}}"></image>
<view class="item-info">
<view class="item-title">
<text class="item-type {{item.type === 'lost' ? 'lost' : 'found'}}">{{item.type === 'lost' ? '丢失' : '捡到'}}</text>
<text>{{item.title}}</text>
</view>
<view class="item-location">{{item.location}}</view>
<view class="item-time">{{item.time}}</view>
<view class="item-status" wx:if="{{item.status === 'matched'}}">
<text style="color: #52c41a;">✓ 已匹配</text>
</view>
</view>
</view>
<view class="item-actions">
<view class="action-button delete" bindtap="deleteItem" data-id="{{item.id}}" data-index="{{index}}">
<text>删除</text>
</view>
</view>
</view>
</view>
<view class="empty-state" wx:else>
<image src="/images/empty.png"></image>
<text>您还没有发布任何物品</text>
<button class="publish-button" bindtap="navigateToPublish">立即发布</button>
</view>
<view class="loading-more" wx:if="{{hasMore && loading}}">
<text>加载中...</text>
</view>
<view class="no-more" wx:if="{{!hasMore && myItems.length > 0}}">
<text>没有更多了</text>
</view>
</view>
<!-- 我的收藏列表 -->
<view class="content" wx:if="{{currentTab === 'collections'}}">
<view class="item-list" wx:if="{{myCollections.length > 0}}">
<view class="item" wx:for="{{myCollections}}" wx:key="id">
<view class="item-left" bindtap="navigateToDetail" data-id="{{item.id}}" data-type="{{item.type}}">
<image class="item-image" src="{{item.images[0] || '/images/empty.png'}}"></image>
<view class="item-info">
<view class="item-title">
<text class="item-type {{item.type === 'lost' ? 'lost' : 'found'}}">{{item.type === 'lost' ? '丢失' : '捡到'}}</text>
<text>{{item.title}}</text>
</view>
<view class="item-location">{{item.location}}</view>
<view class="item-time">{{item.time}}</view>
</view>
</view>
<view class="item-actions">
<view class="action-button cancel" bindtap="cancelCollection" data-id="{{item.id}}" data-index="{{index}}">
<text>取消收藏</text>
</view>
</view>
</view>
</view>
<view class="empty-state" wx:else>
<image src="/images/empty.png"></image>
<text>您还没有收藏任何物品</text>
<button class="publish-button" bindtap="navigateToHome">去首页查看</button>
</view>
<view class="loading-more" wx:if="{{hasMore && loading}}">
<text>加载中...</text>
</view>
<view class="no-more" wx:if="{{!hasMore && myCollections.length > 0}}">
<text>没有更多了</text>
</view>
</view>
</view>

@ -0,0 +1,247 @@
/* 页面容器 */
.container {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 用户信息区域 */
.user-info-section {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.user-details {
flex: 1;
}
.user-name {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.user-id {
font-size: 24rpx;
color: #999;
}
/* 功能菜单 */
.menu-section {
background-color: #fff;
margin-top: 16rpx;
padding: 0 30rpx;
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-icon {
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.menu-text {
flex: 1;
font-size: 32rpx;
}
.menu-arrow {
width: 20rpx;
height: 32rpx;
opacity: 0.5;
}
/* 标签页 */
.tabs {
display: flex;
background-color: #fff;
margin-top: 16rpx;
border-bottom: 1rpx solid #eee;
}
.tab {
flex: 1;
text-align: center;
padding: 30rpx 0;
font-size: 32rpx;
position: relative;
}
.tab.active {
color: #1aad19;
}
.tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 4rpx;
background-color: #1aad19;
}
/* 内容区域 */
.content {
background-color: #fff;
min-height: 500rpx;
}
/* 物品列表 */
.item-list {
padding: 0 30rpx;
}
.item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.item:last-child {
border-bottom: none;
}
.item-left {
display: flex;
flex: 1;
}
.item-image {
width: 140rpx;
height: 140rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.item-info {
flex: 1;
}
.item-title {
display: flex;
align-items: center;
font-size: 32rpx;
margin-bottom: 10rpx;
font-weight: 500;
}
.item-type {
font-size: 20rpx;
padding: 2rpx 12rpx;
border-radius: 12rpx;
margin-right: 10rpx;
color: #fff;
}
.item-type.lost {
background-color: #ff6b6b;
}
.item-type.found {
background-color: #51cf66;
}
.item-location {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
}
.item-time {
font-size: 24rpx;
color: #999;
}
.item-status {
margin-top: 10rpx;
}
.item-actions {
margin-left: 20rpx;
}
.action-button {
padding: 15rpx 30rpx;
border-radius: 20rpx;
font-size: 26rpx;
}
.action-button.delete {
background-color: #fff1f0;
color: #ff4d4f;
border: 1rpx solid #ffccc7;
}
.action-button.cancel {
background-color: #f0f0f0;
color: #666;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-state image {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.6;
}
.empty-state text {
font-size: 28rpx;
color: #999;
margin-bottom: 40rpx;
}
.publish-button {
background-color: #1aad19;
color: #fff;
font-size: 28rpx;
padding: 0 60rpx;
line-height: 70rpx;
border-radius: 35rpx;
}
/* 加载状态 */
.loading-more {
text-align: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #999;
}
.no-more {
text-align: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #ccc;
}

@ -0,0 +1,71 @@
// pages/webview/webview.js
Page({
data: {
url: ''
},
onLoad: function(options) {
// 从页面参数中获取URL
if (options && options.url) {
// 解码URL因为在跳转时我们使用了encodeURIComponent
const decodedUrl = decodeURIComponent(options.url);
// 验证URL是否以http或https开头
if (decodedUrl.startsWith('http://') || decodedUrl.startsWith('https://')) {
this.setData({
url: decodedUrl
});
} else {
// 如果URL不是以http或https开头显示错误提示
wx.showToast({
title: '无效的URL',
icon: 'none'
});
// 返回上一页
setTimeout(() => {
wx.navigateBack();
}, 1500);
}
} else {
// 如果没有URL参数显示错误提示
wx.showToast({
title: '缺少URL参数',
icon: 'none'
});
// 返回上一页
setTimeout(() => {
wx.navigateBack();
}, 1500);
}
},
// 监听网页加载完成
onWebViewLoaded: function() {
console.log('网页加载完成');
},
// 监听网页加载失败
onWebViewError: function(e) {
console.error('网页加载失败', e.detail);
wx.showToast({
title: '网页加载失败',
icon: 'none'
});
},
// 返回上一页
navigateBack: function() {
wx.navigateBack();
},
// 禁止转发
onShareAppMessage: function() {
return {
title: '',
path: '',
imageUrl: ''
};
}
});

@ -0,0 +1,19 @@
<!-- pages/webview/webview.wxml -->
<view class="container">
<!-- 顶部标题栏 -->
<view class="header">
<view class="back-button" bindtap="navigateBack">
<text class="back-icon">←</text>
</view>
<text class="title">外部网页</text>
<view class="right-space"></view>
</view>
<!-- web-view组件用于显示网页内容 -->
<web-view
src="{{url}}"
bindload="onWebViewLoaded"
binderror="onWebViewError"
class="web-view"
></web-view>
</view>

@ -0,0 +1,54 @@
/* pages/webview/webview.wxss */
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #fff;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
height: 88rpx;
padding: 0 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
}
.back-button {
width: 88rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 36rpx;
color: #333;
}
.title {
flex: 1;
font-size: 36rpx;
font-weight: bold;
color: #333;
text-align: center;
}
.right-space {
width: 88rpx;
}
/* web-view组件样式 */
.web-view {
flex: 1;
margin-top: 88rpx;
height: calc(100vh - 88rpx);
}

@ -0,0 +1,96 @@
{
"description": "失物招领小程序",
"packOptions": {
"ignore": [
{
"value": "node_modules",
"type": "folder"
},
{
"value": ".md",
"type": "suffix"
},
{
"value": ".gitignore",
"type": "file"
}
],
"include": []
},
"setting": {
"urlCheck": true,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": true,
"coverView": true,
"nodeModules": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"useIsolateContext": true,
"userConfirmedUseMultipleSubpackages": true,
"showES6CompileOption": false,
"useCompilerPlugins": [
"typescript"
],
"enableEngineNative": false,
"useNativeESM": true,
"useNewFeatureProcess": true,
"noStatusBar": false,
"useSignalingChannel": false,
"removeSelectorReservedWord": true,
"compileWorklet": false,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"condition": false,
"swc": false,
"disableSWC": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"compileType": "miniprogram",
"cloudfunctionRoot": "cloudfunctions/",
"libVersion": "3.10.1",
"appid": "wx85e3844e6e547c51",
"projectname": "shiwuzhaol",
"isGameTourist": false,
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
"condition": {
"search": {
"current": -1,
"list": []
},
"conversation": {
"current": -1,
"list": []
},
"game": {
"current": -1,
"list": []
},
"miniprogram": {
"current": 0,
"list": [
{
"id": 0,
"name": "首页",
"pathName": "pages/index/index",
"query": ""
}
]
}
},
"editorSetting": {}
}

@ -0,0 +1,24 @@
{
"libVersion": "3.10.1",
"projectname": "sw",
"condition": {},
"setting": {
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"useApiHostProcess": true,
"showShadowRootInWxmlPanel": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"compileHotReLoad": true,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false,
"useIsolateContext": true
}
}

@ -0,0 +1,198 @@
// MD5加密工具适用于微信小程序
(function (factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = factory();
} else {
window.md5 = factory();
}
}(function () {
'use strict';
// 整数加法处理2^32溢出
function safeAdd(x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
// 32位数左移
function bitRotateLeft(num, cnt) {
return (num << cnt) | (num >>> (32 - cnt));
}
function md5cmn(q, a, b, x, s, t) {
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
}
function md5ff(a, b, c, d, x, s, t) {
return md5cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5gg(a, b, c, d, x, s, t) {
return md5cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5hh(a, b, c, d, x, s, t) {
return md5cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5ii(a, b, c, d, x, s, t) {
return md5cmn(c ^ (b | (~d)), a, b, x, s, t);
}
// 计算MD5主函数
function binlMD5(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << (len % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
var i;
var olda;
var oldb;
var oldc;
var oldd;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
for (i = 0; i < x.length; i += 16) {
olda = a;
oldb = b;
oldc = c;
oldd = d;
a = md5ff(a, b, c, d, x[i], 7, -680876936);
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
b = md5gg(b, c, d, a, x[i], 20, -373897302);
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
d = md5hh(d, a, b, c, x[i], 11, -358537222);
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5ii(a, b, c, d, x[i], 6, -198630844);
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safeAdd(a, olda);
b = safeAdd(b, oldb);
c = safeAdd(c, oldc);
d = safeAdd(d, oldd);
}
return [a, b, c, d];
}
function binl2rstr(input) {
var i;
var output = '';
var length32 = input.length * 32;
for (i = 0; i < length32; i += 8) {
output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF);
}
return output;
}
function rstr2binl(input) {
var i;
var output = [];
output[(input.length >> 2) - 1] = undefined;
for (i = 0; i < output.length; i += 1) {
output[i] = 0;
}
var length8 = input.length * 8;
for (i = 0; i < length8; i += 8) {
output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32);
}
return output;
}
function rstrMD5(s) {
return binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
}
function rstr2hex(input) {
var hexTab = '0123456789abcdef';
var output = '';
var x;
var i;
for (i = 0; i < input.length; i += 1) {
x = input.charCodeAt(i);
output += hexTab.charAt((x >>> 4) & 0x0F) + hexTab.charAt(x & 0x0F);
}
return output;
}
function str2rstrUTF8(input) {
return unescape(encodeURIComponent(input));
}
function rawMD5(s) {
return rstrMD5(str2rstrUTF8(s));
}
function hexMD5(s) {
return rstr2hex(rawMD5(s));
}
return hexMD5;
}));

@ -0,0 +1,176 @@
// MobileNet特征提取器使用TensorFlow.js
let model = null;
let modelLoading = false;
let modelLoadPromise = null;
// 加载MobileNet模型
async function loadModel() {
if (model) {
return model;
}
if (modelLoading && modelLoadPromise) {
return modelLoadPromise;
}
modelLoading = true;
modelLoadPromise = new Promise(async (resolve, reject) => {
try {
// 从CDN加载模型避免增加包体积
const modelUrl = 'https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v2_1.0_224/model.json';
if (typeof tf === 'undefined') {
console.warn('TensorFlow.js未加载');
reject(new Error('TensorFlow.js未加载'));
return;
}
model = await tf.loadLayersModel(modelUrl);
modelLoading = false;
resolve(model);
} catch (error) {
console.error('模型加载失败');
modelLoading = false;
modelLoadPromise = null;
reject(error);
}
});
return modelLoadPromise;
}
// 提取图片特征向量
async function extractFeatures(imagePath) {
try {
const mobilenetModel = await loadModel();
const imgTensor = await loadAndPreprocessImage(imagePath);
// 提取特征向量
const prediction = mobilenetModel.predict(imgTensor);
const features = await prediction.data();
// 清理内存
imgTensor.dispose();
prediction.dispose();
// 转换为数组并归一化
const featuresArray = Array.from(features);
const normalizedFeatures = normalizeFeatures(featuresArray);
return normalizedFeatures;
} catch (error) {
console.error('特征提取失败');
throw error;
}
}
/**
* 加载并预处理图片
* @param {string} imagePath - 图片路径
* @returns {Promise<tf.Tensor>} 预处理后的图片Tensor
*/
function loadAndPreprocessImage(imagePath) {
return new Promise((resolve, reject) => {
// 使用wx.getImageInfo获取图片信息
wx.getImageInfo({
src: imagePath,
success: (res) => {
const width = res.width;
const height = res.height;
// 创建canvas
try {
const canvas = wx.createOffscreenCanvas({
type: '2d',
width: 224, // MobileNet输入尺寸
height: 224
});
const ctx = canvas.getContext('2d');
// 创建图片对象
const img = canvas.createImage();
img.onload = () => {
// 绘制图片缩放到224x224
ctx.drawImage(img, 0, 0, 224, 224);
// 获取像素数据
const imageData = ctx.getImageData(0, 0, 224, 224);
// 转换为Tensor
// 注意小程序中需要使用tf.browser.fromPixels
const tensor = tf.browser.fromPixels(imageData)
.resizeNearestNeighbor([224, 224])
.toFloat()
.div(255.0) // 归一化到0-1
.expandDims(0); // 添加batch维度
resolve(tensor);
};
img.onerror = (err) => {
reject(new Error('图片加载失败: ' + err));
};
img.src = imagePath;
} catch (error) {
// 如果createOffscreenCanvas不支持使用备用方案
console.warn('createOffscreenCanvas不支持使用备用方案');
reject(error);
}
},
fail: (err) => {
reject(new Error('获取图片信息失败: ' + err));
}
});
});
}
/**
* 归一化特征向量L2归一化
* @param {Array<number>} features - 原始特征向量
* @returns {Array<number>} 归一化后的特征向量
*/
function normalizeFeatures(features) {
// 计算L2范数
const norm = Math.sqrt(features.reduce((sum, val) => sum + val * val, 0));
// 如果范数为0返回原向量
if (norm === 0) {
return features;
}
// L2归一化
return features.map(val => val / norm);
}
/**
* 检查是否支持MobileNet
* @returns {boolean}
*/
function isSupported() {
// 检查TensorFlow.js是否加载
if (typeof tf === 'undefined') {
return false;
}
// 检查是否支持createOffscreenCanvas
if (typeof wx === 'undefined' || !wx.createOffscreenCanvas) {
return false;
}
return true;
}
module.exports = {
extractFeatures,
loadModel,
isSupported
};
Loading…
Cancel
Save