diff --git a/shiwuzhaol22/README.md b/shiwuzhaol22/README.md
new file mode 100644
index 0000000..c4233d2
--- /dev/null
+++ b/shiwuzhaol22/README.md
@@ -0,0 +1,136 @@
+# 失物招领小程序
+
+## 项目介绍
+这是一个基于微信小程序开发的失物招领平台,旨在帮助用户快速发布失物信息、招领信息,并通过AI图像识别技术实现智能匹配,提高物品找回率。
+
+## 功能特性
+
+### 核心功能
+- **用户认证**:基于微信小程序登录,支持模拟登录(调试模式)
+- **物品发布**:支持发布失物信息和招领信息,包括标题、描述、地点、时间、联系方式等
+- **图片管理**:支持上传多张图片(最多9张),自动清理无效图片路径
+- **智能匹配**:基于腾讯AI和MobileNet模型的图像特征提取,实现以图搜图功能
+- **消息通知**:匹配成功后发送消息通知相关用户
+- **物品认领**:支持在线提交认领申请
+- **个人中心**:管理用户发布的物品信息和消息
+
+### 用户界面
+- **首页**:展示失物和招领信息,支持标签切换
+- **搜索页**:支持关键词搜索和图片搜索
+- **发布页**:统一的物品发布界面,支持选择发布类型
+- **详情页**:展示物品详细信息
+- **消息页**:接收系统通知和匹配消息
+- **个人中心**:管理个人信息和设置
+
+## 技术栈
+
+### 前端技术
+- **微信小程序原生开发**:使用WXML、WXSS、JavaScript
+- **数据存储**:微信本地存储(wx.setStorageSync)
+- **云开发**:微信云函数、云数据库、云存储
+
+### AI技术
+- **图像特征提取**:
+ - 腾讯AI接口
+ - MobileNet模型(TensorFlow.js)作为降级方案
+- **相似度计算**:余弦相似度算法
+
+### 工具库
+- **MD5加密**:用于腾讯AI签名
+- **TensorFlow.js**:用于本地图像特征提取(降级方案)
+
+## 项目结构
+
+```
+shiwuzhaol/
+├── app.js # 小程序入口文件
+├── app.json # 全局配置文件
+├── app.wxss # 全局样式文件
+├── cloudfunctions/ # 云函数目录
+│ ├── imageSearch/ # 图片搜索云函数
+│ ├── login/ # 登录云函数
+│ └── tencentAI/ # 腾讯AI调用云函数
+├── images/ # 图片资源目录
+├── pages/ # 页面目录
+│ ├── index/ # 首页
+│ ├── login/ # 登录页
+│ ├── search/ # 搜索页
+│ ├── publish/ # 发布页
+│ ├── detail/ # 详情页
+│ ├── message/ # 消息页
+│ ├── user/ # 个人中心
+│ ├── claim/ # 认领页
+│ ├── match/ # 匹配页
+│ └── settings/ # 设置页
+└── utils/ # 工具函数目录
+ ├── md5.js # MD5加密库
+ └── mobilenetFeatureExtractor.js # MobileNet特征提取器
+```
+
+## 安装部署
+
+### 前置条件
+- 微信开发者工具
+- 微信小程序账号
+- 微信云开发环境(可选,用于完整功能)
+
+### 安装步骤
+1. 克隆或下载项目代码
+2. 使用微信开发者工具导入项目
+3. 在项目设置中配置小程序AppID
+4. (可选)开通并配置云开发环境
+5. (可选)部署云函数到云开发环境
+
+### 云函数部署
+1. 右键点击`cloudfunctions`目录下的各云函数文件夹
+2. 选择"上传并部署:云端安装依赖"
+3. 确保所有云函数部署成功
+
+## 使用说明
+
+### 调试模式
+项目默认启用调试模式(`app.globalData.isDebug = true`),在此模式下:
+- 使用本地模拟数据而非云数据库
+- 支持模拟登录,无需真实的微信登录
+- 图片搜索使用本地降级方案
+
+### 配置说明
+- **腾讯AI配置**:在`cloudfunctions/imageSearch/index.js`中配置腾讯AI的SECRET_ID和SECRET_KEY
+- **MobileNet模型**:默认从CDN加载,可改为本地模型文件
+- **权限配置**:已在app.json中配置了相机、相册、位置等权限
+
+## 功能亮点
+
+### 智能图像匹配
+- 优先使用腾讯AI进行高精度图像特征提取
+- 支持MobileNet模型作为降级方案,确保离线可用
+- 使用余弦相似度算法计算图片相似度
+- 实现特征向量缓存,提高匹配效率
+
+### 健壮性设计
+- 多重图片路径验证和清理机制
+- 云函数调用失败时的降级处理
+- 完善的错误处理和日志记录
+- 数据持久化和缓存管理
+
+### 用户体验优化
+- 简洁直观的界面设计
+- 流畅的页面切换和数据加载
+- 实时消息通知和未读消息标记
+- 支持下拉刷新和上拉加载更多
+
+## 注意事项
+
+1. **腾讯AI密钥安全**:在生产环境中,请妥善保管SECRET_ID和SECRET_KEY
+2. **权限配置**:确保在小程序后台配置了相应的API域名白名单
+3. **调试模式**:上线前请关闭调试模式
+4. **存储管理**:定期清理云存储中的临时文件和无效图片
+5. **性能优化**:对于大量图片匹配场景,建议优化特征提取和相似度计算算法
+
+## License
+
+MIT License
+
+## 联系我们
+
+如有问题或建议,请通过小程序内的"关于我们"页面联系开发者。
\ No newline at end of file
diff --git a/shiwuzhaol22/project.config.json b/shiwuzhaol22/project.config.json
new file mode 100644
index 0000000..51c3427
--- /dev/null
+++ b/shiwuzhaol22/project.config.json
@@ -0,0 +1,39 @@
+{
+ "description": "失物招领小程序",
+ "miniprogramRoot": "shiwuzhaol/",
+ "cloudfunctionRoot": "shiwuzhaol/cloudfunctions/",
+ "packOptions": {
+ "ignore": [],
+ "include": []
+ },
+ "setting": {
+ "es6": true,
+ "postcss": true,
+ "minified": true,
+ "uglifyFileName": false,
+ "enhance": true,
+ "minifyWXML": true,
+ "packNpmRelationList": [],
+ "babelSetting": {
+ "ignore": [],
+ "disablePlugins": [],
+ "outputPath": ""
+ },
+ "compileWorklet": false,
+ "uploadWithSourceMap": true,
+ "packNpmManually": false,
+ "minifyWXSS": true,
+ "localPlugins": false,
+ "disableUseStrict": false,
+ "useCompilerPlugins": false,
+ "condition": false,
+ "swc": false,
+ "disableSWC": true
+ },
+ "compileType": "miniprogram",
+ "libVersion": "3.11.2",
+ "appid": "wx85e3844e6e547c51",
+ "projectname": "shiwuzhaol",
+ "simulatorPluginLibVersion": {},
+ "editorSetting": {}
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/project.private.config.json b/shiwuzhaol22/project.private.config.json
new file mode 100644
index 0000000..6ff49cc
--- /dev/null
+++ b/shiwuzhaol22/project.private.config.json
@@ -0,0 +1,22 @@
+{
+ "libVersion": "3.11.2",
+ "projectname": "shiwuzhaol11",
+ "setting": {
+ "urlCheck": true,
+ "coverView": true,
+ "lazyloadPlaceholderEnable": false,
+ "skylineRenderEnable": false,
+ "preloadBackgroundData": false,
+ "autoAudits": false,
+ "showShadowRootInWxmlPanel": true,
+ "compileHotReLoad": true,
+ "useApiHook": true,
+ "useApiHostProcess": true,
+ "useStaticServer": false,
+ "useLanDebug": false,
+ "showES6CompileOption": false,
+ "checkInvalidKey": true,
+ "ignoreDevUnusedFiles": true,
+ "bigPackageSizeSupport": false
+ }
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/app.js b/shiwuzhaol22/shiwuzhaol/app.js
new file mode 100644
index 0000000..fd4a162
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/app.js
@@ -0,0 +1,5619 @@
+//app.js
+// 引入MD5库(用于腾讯AI签名)
+const md5 = require('./utils/md5.js');
+
+App({
+ globalData: {
+ userInfo: null,
+ hasUserInfo: false,
+ canIUseGetUserProfile: false,
+ userPublishedItems: [], // 存储用户自己发布的物品信息
+ allPublishedItems: [], // 存储所有用户发布的物品信息(用于模拟多用户场景)
+ localImages: {}, // 存储本地模拟的图片路径映射(用于跨用户访问)
+ similarityCache: {}, // 存储图片相似度计算结果的缓存
+ itemFeaturesCache: {}, // 存储物品图片的AI特征向量缓存
+ aiLabelsCache: {}, // 存储AI识别标签的缓存(用于智能匹配)
+ settings: wx.getStorageSync('userSettings') || { enableSmartMatch: true, allowLocationAccess: false }, // 用户设置
+ ignoredMatches: wx.getStorageSync('ignoredMatches') || [], // 已忽略的匹配项
+ db: null, // 云数据库引用
+ cloudEnvValidated: undefined, // 云环境验证状态:undefined=未验证, true=已验证为有效, false=已验证为无效
+ isDebug: true, // 调试模式
+ hasNewPublishedItem: false, // 标记是否有新发布的物品
+ lastPublishedType: 'all', // 上次发布的物品类型
+ pendingClaimMessages: wx.getStorageSync('pendingClaimMessages') || [], // 消息列表
+ // 移除云存储引用,使用wx.cloud.uploadFile直接操作
+ },
+
+ generateAndPersistFeaturesForItem: function(itemId, imagePath, callback) {
+ const app = this;
+ if (!imagePath) {
+ callback && callback([]);
+ return;
+ }
+
+ this.uploadImageToAI(imagePath, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ if (!app.globalData.itemFeaturesCache) {
+ app.globalData.itemFeaturesCache = {};
+ }
+ const cacheKey = app.generateCacheKey(imagePath);
+ app.globalData.itemFeaturesCache[cacheKey] = features;
+
+ if (app.globalData.db && wx.cloud && itemId) {
+ app.globalData.db.collection('items').doc(itemId).update({
+ data: {
+ imageFeatureVectors: [{
+ fileID: imagePath,
+ features: features,
+ dimension: features.length,
+ source: features.length >= 512 ? 'tencentAI' : 'local',
+ updatedAt: new Date()
+ }]
+ },
+ success: () => {
+ console.log('已持久化发布物品的图片特征', itemId);
+ },
+ fail: (err) => {
+ console.warn('持久化发布物品图片特征失败:', err);
+ }
+ });
+ }
+ callback && callback(features);
+ } else {
+ console.warn('发布时提取图片特征失败:', imagePath);
+ callback && callback([]);
+ }
+ });
+ },
+ onLaunch: function() {
+ // 微信登录(简化版,不进行实际的服务器交互)
+ console.log('小程序启动');
+
+ // 初始化云开发环境
+ this.initCloud();
+
+ // 初始化用户自己发布的物品列表
+ this.globalData.userPublishedItems = [];
+
+ // 检查是否有存储的用户信息
+ this.loadUserInfo();
+
+ // 尝试获取用户信息,但不强制要求授权
+ this.getUserInfo();
+
+ // 初始化消息通知系统,更新tabBar角标
+ this.updateTabBarBadge();
+
+ // 检查是否支持getUserProfile接口
+ if (wx.getUserProfile) {
+ this.globalData.canIUseGetUserProfile = true;
+ }
+
+ // 云开发环境已在上方初始化,此处不再重复初始化
+
+ // 已移除订阅消息授权请求
+
+ // 设置全局错误处理器
+ this.setupGlobalErrorHandler();
+ },
+
+ // 设置全局错误处理器
+ setupGlobalErrorHandler: function() {
+ const app = this;
+
+ wx.onError(function(error) {
+ // 过滤掉不影响功能的警告
+ // reportRealtimeAction 是开发者工具内部的实时日志上报,不支持时不影响功能
+ if (error && typeof error === 'string' && error.includes('reportRealtimeAction')) {
+ // 静默忽略,不输出到控制台
+ return;
+ }
+
+ // 过滤掉 worker 相关的警告
+ if (error && typeof error === 'string' && error.includes('[worker]')) {
+ // 静默忽略 worker 警告
+ return;
+ }
+
+ console.error('小程序全局错误:', error);
+
+ // 检测云开发相关错误
+ if (error && typeof error === 'string' && error.includes('wx.cloud')) {
+ console.log('检测到云开发相关错误,确保使用本地模拟数据模式');
+
+ // 如果全局数据中仍然引用了云数据库,清除它以强制使用降级方案
+ if (app.globalData.db && error.includes('wx.cloud')) {
+ console.log('重置数据库引用,确保使用本地模拟数据');
+ app.globalData.db = null;
+ }
+ }
+
+ // 可以在这里添加错误上报逻辑
+ });
+ },
+
+ // 初始化云开发环境(带错误处理和降级方案)
+ initCloud: function() {
+ try {
+ // 检查微信云开发是否可用
+ if (wx.cloud && typeof wx.cloud.init === 'function') {
+ console.log('尝试初始化云开发环境');
+
+ // 尝试初始化云开发,使用try-catch防止初始化失败
+ try {
+ // 使用您的云开发环境ID
+ // 如果遇到 env not exists 错误,请检查:
+ // 1. 环境ID是否正确(从云开发控制台复制)
+ // 2. 小程序AppID是否已关联云开发环境
+ // 3. 在微信开发者工具中是否正确选择了云开发环境
+ wx.cloud.init({
+ traceUser: true,
+ env: 'cloud1-4gtth1kue3bec7ef' // 环境ID,从云开发控制台获取
+ });
+
+ console.log('云开发环境初始化完成,环境ID:', 'cloud1-4gtth1kue3bec7ef');
+
+ // 获取云数据库引用
+ try {
+ const db = wx.cloud.database();
+ if (db) {
+ // 先设置db引用,但标记为"未验证"
+ this.globalData.db = db;
+ this.globalData.cloudEnvValidated = false; // 标记环境尚未验证
+ console.log('云数据库引用获取成功,将在首次使用时验证环境有效性');
+
+ // 立即尝试一个简单的查询来验证环境是否可用
+ // 使用 setTimeout 延迟执行,避免阻塞初始化
+ setTimeout(() => {
+ this.validateCloudEnvironment();
+ }, 100);
+ } else {
+ console.warn('云数据库获取失败,db对象为null');
+ this.globalData.db = null;
+ }
+ } catch (dbError) {
+ console.warn('获取云数据库引用失败,将使用本地模拟数据:', dbError);
+ this.globalData.db = null;
+ }
+
+ } catch (cloudError) {
+ console.warn('云开发初始化失败,将使用本地模拟数据模式:', cloudError);
+ this.globalData.db = null;
+ }
+ } else {
+ console.log('当前微信版本不支持云开发,将使用本地模拟数据');
+ this.globalData.db = null;
+ }
+
+ // 如果云开发初始化失败或不可用,确保使用本地模拟数据模式
+ if (!this.globalData.db) {
+ console.log('云开发环境不可用,系统将使用本地模拟数据运行');
+ }
+ return false; // 默认使用降级方案,除非明确检测到可用环境
+ } catch (e) {
+ console.warn('云开发初始化过程中出现错误,将使用本地模拟数据:', e);
+ this.globalData.db = null;
+ return false; // 初始化失败,使用降级方案
+ }
+ },
+
+ // 验证云开发环境是否可用
+ validateCloudEnvironment: function() {
+ if (!this.globalData.db) {
+ console.log('⚠️ 云数据库引用不存在,无法验证环境');
+ return;
+ }
+
+ const that = this;
+ console.log('========== 开始验证云开发环境有效性 ==========');
+ console.log('环境ID:', 'cloud1-4gtth1kue3bec7ef');
+
+ // 尝试一个简单的查询来验证环境
+ try {
+ this.globalData.db.collection('items').limit(1).get({
+ success: res => {
+ console.log('✅ 云开发环境验证成功,环境可用');
+ console.log('✅ 可以正常访问云数据库,返回数据量:', res.data.length);
+ that.globalData.cloudEnvValidated = true;
+
+ // 显示成功提示
+ console.log('==========================================');
+ console.log('✅ 云开发环境配置正确!');
+ console.log('✅ 跨设备数据共享已启用');
+ console.log('==========================================');
+ },
+ fail: err => {
+ // 检查是否是环境不存在的错误
+ const isEnvError = err.errCode === -501000 || (err.errMsg && err.errMsg.includes('env not exists'));
+
+ if (isEnvError) {
+ console.error('❌ 云开发环境不存在或未正确配置!');
+ console.error('错误详情:', err.errMsg || err);
+ console.log('==========================================');
+ console.log('⚠️ 配置检查清单:');
+ console.log('1. 检查环境ID是否正确:', 'cloud1-4gtth1kue3bec7ef');
+ console.log('2. 检查是否在云开发控制台创建了环境');
+ console.log('3. 检查小程序AppID是否已关联云开发环境');
+ console.log('4. 检查微信开发者工具中是否正确选择了云开发环境');
+ console.log('5. 查看文档: 云开发环境配置检查清单.md');
+ console.log('==========================================');
+
+ // 清除数据库引用,确保后续不再尝试使用云数据库
+ that.globalData.db = null;
+ that.globalData.cloudEnvValidated = false;
+ } else {
+ console.warn('⚠️ 云开发环境验证失败,但可能是权限问题');
+ console.warn('错误详情:', err.errMsg || err);
+ console.log('提示:可能是数据库集合权限设置问题,请检查 items 集合权限');
+ console.log('建议权限:所有用户可读,仅创建者可写');
+ // 不立即标记为无效,可能是权限问题
+ }
+ }
+ });
+ } catch (error) {
+ console.error('验证云开发环境时发生异常:', error);
+ this.globalData.cloudEnvValidated = false;
+ }
+ },
+
+ // 加载用户信息
+ loadUserInfo: function() {
+ try {
+ const storedUserInfo = wx.getStorageSync('userInfo');
+ if (storedUserInfo) {
+ this.globalData.userInfo = storedUserInfo;
+ this.globalData.hasUserInfo = true;
+ } else {
+ // 设置默认用户信息,避免页面空白
+ this.globalData.userInfo = { nickName: '访客', avatarUrl: '/images/default_avatar.svg' };
+ // 不设置hasUserInfo为true,以便各个页面可以正确判断登录状态
+ }
+ } catch (e) {
+ console.error('读取本地存储失败', e);
+ }
+ },
+
+ // 获取用户信息(简化版,不强制要求授权)
+ getUserInfo: function() {
+ const that = this;
+ try {
+ wx.getSetting({
+ success: res => {
+ // 只有在用户已授权的情况下才获取用户信息
+ if (res.authSetting && res.authSetting['scope.userInfo']) {
+ wx.getUserInfo({
+ success: function(res) {
+ // 可以将 res 发送给后台解码出 unionId
+ that.globalData.userInfo = res.userInfo;
+ that.globalData.hasUserInfo = true;
+
+ // 将用户信息存储到本地,方便下次使用
+ try {
+ wx.setStorageSync('userInfo', res.userInfo);
+ } catch (e) {
+ console.error('存储用户信息失败', e);
+ }
+
+ // 调用全局钩子,通知页面用户信息已更新
+ if (that.userInfoReadyCallback) {
+ that.userInfoReadyCallback(res);
+ }
+ },
+ fail: function(err) {
+ console.error('获取用户信息失败', err);
+ // 失败时不做任何处理,使用默认值
+ }
+ });
+ }
+ // 未授权时不做任何处理,保持默认的用户信息
+ },
+ fail: function(err) {
+ console.error('获取设置失败', err);
+ // 失败时不做任何处理,保持默认的用户信息
+ }
+ });
+ } catch (e) {
+ console.error('getUserInfo 方法执行失败', e);
+ }
+ },
+
+ // 搜索物品(文字)
+ searchItemsByText: function(keyword, pageNum, callback) {
+ // 处理可选参数pageNum
+ if (typeof pageNum === 'function') {
+ callback = pageNum;
+ pageNum = 1;
+ }
+
+ console.log('开始文字搜索');
+
+ // 使用云数据库搜索
+ this.searchItemsFromCloud(keyword, 'all', (items) => {
+ // 分页处理
+ const pageSize = 10;
+ const startIndex = (pageNum - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedItems = items.slice(startIndex, endIndex);
+
+ // 返回与search.js期望格式一致的数据结构
+ const response = {
+ items: paginatedItems,
+ hasMore: endIndex < items.length
+ };
+
+ callback(response);
+ });
+ },
+
+ // 搜索物品(图片)- 完善AI版(支持搜索所有类型的物品:失物和招领)
+ searchItemsByImage: function(imagePath, pageNum, callback) {
+ // 处理可选参数pageNum
+ if (typeof pageNum === 'function') {
+ callback = pageNum;
+ pageNum = 1;
+ }
+
+ console.log('开始AI图片搜索,比较相似度...');
+ console.log('搜索范围:所有类型的物品(失物和招领)');
+
+ // 初始化相似度计算缓存(如果不存在)
+ if (!this.globalData.similarityCache) {
+ this.globalData.similarityCache = {};
+ }
+
+ // 创建缓存键
+ const cacheKey = this.generateCacheKey(imagePath);
+
+ // 检查缓存是否存在
+ if (this.globalData.similarityCache[cacheKey]) {
+ console.log('使用缓存的相似度结果,缓存键:', cacheKey);
+ console.log('缓存结果数量:', this.globalData.similarityCache[cacheKey].length);
+ const cachedItems = this.globalData.similarityCache[cacheKey];
+
+ // 输出缓存结果的相似度分布,用于调试
+ if (cachedItems.length > 0) {
+ const similarities = cachedItems.map(item => item.similarity || 0);
+ console.log('缓存结果相似度分布:', {
+ min: Math.min(...similarities).toFixed(3),
+ max: Math.max(...similarities).toFixed(3),
+ avg: (similarities.reduce((a, b) => a + b, 0) / similarities.length).toFixed(3),
+ unique: new Set(similarities.map(s => s.toFixed(3))).size
+ });
+ }
+
+ // 分页处理(由于只返回4个,第一页返回全部,后续页面为空)
+ const pageSize = 4; // 每页4个
+ const startIndex = (pageNum - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ // 确保缓存的结果已按相似度排序
+ const sortedCachedItems = cachedItems.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
+ const paginatedItems = sortedCachedItems.slice(startIndex, endIndex);
+
+ // 返回与search.js期望格式一致的数据结构
+ const response = {
+ items: paginatedItems,
+ hasMore: endIndex < cachedItems.length
+ };
+
+ callback(response);
+ return;
+ }
+
+ // 收集所有可比较的物品(包括用户发布的和模拟的失物物品)
+ // 注意:搜索时应该包含用户自己发布的物品,这样才能找到自己发布的物品
+ const localItems = this.getAllComparableItems(false); // false表示包含用户自己发布的物品
+
+ console.log('本地物品数量:', localItems.length);
+
+ // 从云数据库获取所有类型的物品(失物和招领)
+ // 流程:1) 获取所有物品(包含图片路径) 2) 提取图片路径 3) 对比图片 4) 根据匹配的图片路径查询物品信息
+ this.getAllItemsFromCloudForSearch((cloudItems) => {
+ console.log('========== AI图片搜索流程 ==========');
+ console.log('步骤1: 从云数据库获取物品,数量:', cloudItems.length);
+
+ // 提取所有云存储图片路径(cloud://)
+ const cloudImagePaths = [];
+ const imageToItemMap = new Map(); // 图片路径 -> 物品信息的映射
+
+ cloudItems.forEach(item => {
+ if (item && item.images && Array.isArray(item.images)) {
+ item.images.forEach(imagePath => {
+ if (imagePath && imagePath.startsWith('cloud://')) {
+ cloudImagePaths.push(imagePath);
+ // 如果同一张图片对应多个物品,保存所有物品
+ if (!imageToItemMap.has(imagePath)) {
+ imageToItemMap.set(imagePath, []);
+ }
+ imageToItemMap.get(imagePath).push(item);
+ }
+ });
+ }
+ });
+
+ console.log('步骤2: 提取云存储图片路径,数量:', cloudImagePaths.length);
+ console.log('步骤3: 开始对比图片相似度...');
+
+ // 合并本地物品和云数据库物品,去重
+ const allItemsMap = new Map();
+
+ // 先添加本地物品
+ localItems.forEach(item => {
+ if (item && item.id && item.images && item.images.length > 0) {
+ allItemsMap.set(item.id, item);
+ }
+ });
+
+ // 再添加云数据库物品(如果ID已存在,则不覆盖,保留本地版本)
+ cloudItems.forEach(item => {
+ if (item && item.id && item.images && item.images.length > 0 && !allItemsMap.has(item.id)) {
+ // 确保云数据库物品有图片特征
+ if (!item.imageFeatures) {
+ const firstImage = item.images && item.images.length > 0
+ ? item.images[0]
+ : '/images/empty.png';
+ item.imageFeatures = null;
+ }
+ allItemsMap.set(item.id, item);
+ }
+ });
+
+ const allItems = Array.from(allItemsMap.values());
+
+ console.log('合并后总物品数量:', allItems.length);
+ console.log('物品类型分布:', {
+ lost: allItems.filter(item => item.type === 'lost').length,
+ found: allItems.filter(item => item.type === 'found').length
+ });
+
+ // 如果物品数量为0,直接返回空结果
+ if (allItems.length === 0) {
+ const response = {
+ items: [],
+ hasMore: false
+ };
+ callback(response);
+ return;
+ }
+
+ // 为目标图片生成特征向量(使用统一的特征提取方法)
+ this.uploadImageToAI(imagePath, (targetFeatures) => {
+ console.log('搜索图片路径:', imagePath);
+ console.log('目标图片特征向量维度:', targetFeatures ? targetFeatures.length : 0);
+ console.log('目标图片特征向量前10个值:', targetFeatures ? targetFeatures.slice(0, 10) : 'null');
+
+ if (!targetFeatures || !Array.isArray(targetFeatures) || targetFeatures.length === 0) {
+ console.error('特征提取失败,返回空结果');
+ callback({ items: [], hasMore: false });
+ return;
+ }
+
+ // 检查特征向量是否有效(不全为0)
+ const targetSum = targetFeatures.reduce((sum, val) => sum + Math.abs(val || 0), 0);
+ if (targetSum === 0) {
+ console.error('目标图片特征向量无效(全为0),返回空结果');
+ console.error('可能原因:1) 特征提取失败 2) 图片无法读取 3) AI服务返回空结果');
+ callback({ items: [], hasMore: false });
+ return;
+ }
+
+ console.log('目标图片特征向量总和:', targetSum.toFixed(3), '(应该大于0)');
+
+ // 检查特征向量是否来自降级方法(基于路径)
+ const targetFirst5 = targetFeatures.slice(0, 5);
+ const isMockFeatures = targetSum < 10 && targetFeatures.length === 128; // 基于路径的特征通常总和较小
+
+ if (isMockFeatures) {
+ console.error('❌ 严重警告:目标图片使用了基于路径的特征提取(降级方法)');
+ console.error('❌ 这会导致不同图片的相似度异常高,因为基于路径的特征无法区分图片内容');
+ console.error('❌ 建议:1) 检查AI服务配置 2) 确认AI服务正常工作 3) 检查云函数是否部署成功');
+ console.error('❌ 当前AI配置 provider:', this.AI_CONFIG.provider);
+ console.error('❌ 当前USE_TENCENT_CLOUD:', this.AI_CONFIG.TENCENT_AI.USE_TENCENT_CLOUD);
+ }
+
+ // 检查特征向量的唯一性(前10维)
+ const targetUniqueHash = targetFeatures.slice(0, 10).map(v => v.toFixed(3)).join(',');
+ console.log('目标图片特征向量唯一性标识(前10维):', targetUniqueHash);
+
+ // 计算每个物品与目标图片的相似度
+ const similarItemsPromises = allItems.map(item => {
+ return new Promise((resolve) => {
+ // 确保物品有图片特征(使用相同的特征提取方法)
+ const needsRefresh = !item.imageFeatures || this.isMockFeatureVector(item.imageFeatures);
+ if (needsRefresh) {
+ const firstImage = item.images && item.images.length > 0
+ ? item.images[0]
+ : '/images/empty.png';
+ // 使用统一的特征提取方法
+ this.getOrGenerateItemFeatures(firstImage, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ item.imageFeatures = features;
+ console.log(`物品 ${item.id} 特征向量维度: ${features.length}, 前5个值:`, features.slice(0, 5));
+ } else {
+ console.warn(`物品 ${item.id} 特征提取失败,使用空特征`);
+ item.imageFeatures = new Array(targetFeatures.length).fill(0);
+ }
+ resolve(item);
+ });
+ } else {
+ // 检查特征向量维度是否匹配
+ if (item.imageFeatures.length !== targetFeatures.length) {
+ console.warn(`物品 ${item.id} 特征向量维度不匹配: ${item.imageFeatures.length} vs ${targetFeatures.length},重新提取`);
+ const firstImage = item.images && item.images.length > 0
+ ? item.images[0]
+ : '/images/empty.png';
+ this.getOrGenerateItemFeatures(firstImage, (features) => {
+ item.imageFeatures = features && Array.isArray(features) ? features : new Array(targetFeatures.length).fill(0);
+ resolve(item);
+ });
+ } else {
+ resolve(item);
+ }
+ }
+ });
+ });
+
+ // 等待所有特征提取完成
+ Promise.all(similarItemsPromises).then((itemsWithFeatures) => {
+ console.log(`特征提取完成,共 ${itemsWithFeatures.length} 个物品`);
+
+ const similarItems = itemsWithFeatures.map(item => {
+ // 确保特征向量维度匹配
+ if (!item.imageFeatures || !Array.isArray(item.imageFeatures) || item.imageFeatures.length === 0) {
+ console.warn(`物品 ${item.id} 特征向量无效,相似度设为0`);
+ return {
+ ...item,
+ similarity: 0,
+ isAISimilarity: true
+ };
+ }
+
+ // 如果维度不匹配,统一处理
+ let itemFeatures = item.imageFeatures;
+ let finalTargetFeatures = targetFeatures;
+
+ if (itemFeatures.length !== targetFeatures.length) {
+ console.log(`物品 ${item.id} 特征向量维度不匹配: ${itemFeatures.length} vs ${targetFeatures.length},进行统一处理`);
+
+ // 统一到较小的维度,避免填充0导致相似度计算偏差
+ const minLength = Math.min(itemFeatures.length, targetFeatures.length);
+ itemFeatures = itemFeatures.slice(0, minLength);
+ finalTargetFeatures = targetFeatures.slice(0, minLength);
+ }
+
+ // 检查特征向量是否有效(不全为0)
+ const targetSum = finalTargetFeatures.reduce((sum, val) => sum + Math.abs(val), 0);
+ const itemSum = itemFeatures.reduce((sum, val) => sum + Math.abs(val), 0);
+
+ if (targetSum === 0 || itemSum === 0) {
+ console.warn(`物品 ${item.id} 特征向量无效(全为0),相似度设为0`, {
+ targetSum: targetSum,
+ itemSum: itemSum,
+ targetFirst5: finalTargetFeatures.slice(0, 5),
+ itemFirst5: itemFeatures.slice(0, 5)
+ });
+ return {
+ ...item,
+ similarity: 0,
+ isAISimilarity: true
+ };
+ }
+
+ // 使用余弦相似度计算(确保维度一致)
+ const similarity = this.calculateCosineSimilarity(finalTargetFeatures, itemFeatures);
+
+ // 检查是否使用了基于路径的特征提取(降级方法)
+ const targetIsMock = targetSum < 10 && targetFeatures.length === 128;
+ const itemIsMock = itemSum < 10 && itemFeatures.length === 128;
+
+ if (targetIsMock || itemIsMock) {
+ console.warn(`⚠️ 警告:检测到基于路径的特征提取(降级方法)`);
+ console.warn(` 目标图片: ${imagePath}, 是否降级=${targetIsMock}`);
+ console.warn(` 物品图片: ${item.images[0]}, 是否降级=${itemIsMock}`);
+ console.warn(` 这会导致不同图片的相似度异常高,因为基于路径的特征无法区分图片内容`);
+
+ // 如果都使用了基于路径的特征,且路径不同,相似度应该很低
+ // 但如果相似度很高,说明路径相似或特征提取有问题
+ if (similarity > 0.5) {
+ console.error(`❌ 严重错误:不同图片路径但相似度异常高(${similarity.toFixed(3)})`);
+ console.error(` 目标图片路径: ${imagePath}`);
+ console.error(` 物品图片路径: ${item.images[0]}`);
+ console.error(` 目标特征总和: ${targetSum.toFixed(3)}`);
+ console.error(` 物品特征总和: ${itemSum.toFixed(3)}`);
+ console.error(` 目标特征前10维: [${finalTargetFeatures.slice(0, 10).map(v => v.toFixed(3)).join(', ')}]`);
+ console.error(` 物品特征前10维: [${itemFeatures.slice(0, 10).map(v => v.toFixed(3)).join(', ')}]`);
+ }
+ }
+
+ // 调试日志:输出详细信息
+ if (similarity > 0.3) {
+ console.log(`🔍 相似度物品: ID=${item.id}, 标题=${item.title}, 类型=${item.type}, 相似度=${similarity.toFixed(3)}, 图片路径=${item.images[0]}`);
+ console.log(` 目标特征前5: [${finalTargetFeatures.slice(0, 5).map(v => v.toFixed(3)).join(', ')}]`);
+ console.log(` 物品特征前5: [${itemFeatures.slice(0, 5).map(v => v.toFixed(3)).join(', ')}]`);
+ console.log(` 目标特征总和: ${targetSum.toFixed(3)}, 物品特征总和: ${itemSum.toFixed(3)}`);
+
+ // 检查特征向量是否相同(用于诊断)
+ const targetStr = finalTargetFeatures.slice(0, 10).map(v => v.toFixed(3)).join(',');
+ const itemStr = itemFeatures.slice(0, 10).map(v => v.toFixed(3)).join(',');
+ if (targetStr === itemStr) {
+ console.warn(`⚠️ 警告:物品 ${item.id} 的特征向量前10维与目标图片完全相同!`);
+ console.warn(` 这可能是因为都使用了基于路径的特征提取,且路径相似`);
+ console.warn(` 目标图片: ${imagePath}`);
+ console.warn(` 物品图片: ${item.images[0]}`);
+ }
+ }
+
+ // 如果相似度异常高(>0.7),输出详细警告并进行严格检查
+ if (similarity > 0.7) {
+ console.error(`❌ 异常高相似度警告: 物品 ${item.id} 相似度 ${similarity.toFixed(3)}`);
+ console.error(` 目标图片: ${imagePath}`);
+ console.error(` 物品图片: ${item.images[0]}`);
+ console.error(` 物品标题: ${item.title}`);
+ console.error(` 目标特征总和: ${targetSum.toFixed(3)}, 物品特征总和: ${itemSum.toFixed(3)}`);
+ console.error(` 目标是否降级: ${targetIsMock}, 物品是否降级: ${itemIsMock}`);
+
+ // 检查特征向量是否相同
+ const targetStr = finalTargetFeatures.slice(0, 20).map(v => v.toFixed(3)).join(',');
+ const itemStr = itemFeatures.slice(0, 20).map(v => v.toFixed(3)).join(',');
+
+ if (targetStr === itemStr) {
+ console.error(`❌ 错误:特征向量前20维完全相同!`);
+ console.error(` 这通常是因为都使用了基于路径的特征提取,且路径相同`);
+ console.error(` 目标特征前20维: [${targetStr}]`);
+ console.error(` 物品特征前20维: [${itemStr}]`);
+
+ // 如果特征向量完全相同,强制将相似度设为0,避免误匹配
+ console.error(` 强制将相似度设为0,避免误匹配`);
+ return {
+ ...item,
+ similarity: 0, // 强制设为0
+ isAISimilarity: true,
+ matchedImagePath: item.images && item.images.length > 0 ? item.images[0] : null,
+ forceZeroReason: '特征向量完全相同'
+ };
+ } else {
+ // 计算差异度
+ let diffCount = 0;
+ let totalDiff = 0;
+ let maxDiff = 0;
+ for (let i = 0; i < Math.min(20, finalTargetFeatures.length, itemFeatures.length); i++) {
+ const diff = Math.abs(finalTargetFeatures[i] - itemFeatures[i]);
+ if (diff > 0.01) {
+ diffCount++;
+ }
+ totalDiff += diff;
+ if (diff > maxDiff) {
+ maxDiff = diff;
+ }
+ }
+ console.error(` 特征向量差异: 前20维中${diffCount}个维度有明显差异,平均差异: ${(totalDiff / 20).toFixed(4)}, 最大差异: ${maxDiff.toFixed(4)}`);
+
+ // 如果差异很小,说明特征向量过于相似
+ if (diffCount < 5 || totalDiff / 20 < 0.1) {
+ console.error(` ⚠️ 警告:特征向量过于相似,这可能导致相似度异常高`);
+ console.error(` 目标特征前10维: [${finalTargetFeatures.slice(0, 10).map(v => v.toFixed(3)).join(', ')}]`);
+ console.error(` 物品特征前10维: [${itemFeatures.slice(0, 10).map(v => v.toFixed(3)).join(', ')}]`);
+
+ // 如果都使用了基于路径的特征提取,且差异很小,降低相似度
+ if (targetIsMock && itemIsMock && totalDiff / 20 < 0.05) {
+ console.error(` ⚠️ 都使用了基于路径的特征提取,且差异很小,降低相似度`);
+ const adjustedSimilarity = Math.max(0, similarity - 0.5); // 降低0.5
+ return {
+ ...item,
+ similarity: adjustedSimilarity,
+ isAISimilarity: true,
+ matchedImagePath: item.images && item.images.length > 0 ? item.images[0] : null,
+ adjustedReason: '基于路径的特征提取且差异很小'
+ };
+ }
+ }
+ }
+
+ console.error(` 请检查:1) 是否使用了正确的AI服务 2) 特征向量是否真的不同 3) 是否都降级到了基于路径的特征提取`);
+ }
+
+ return {
+ ...item,
+ similarity: similarity,
+ isAISimilarity: true,
+ matchedImagePath: item.images && item.images.length > 0 ? item.images[0] : null // 记录匹配到的图片路径
+ };
+ });
+
+ // 按相似度排序(从高到低)
+ similarItems.sort((a, b) => b.similarity - a.similarity);
+
+ console.log('步骤5: 图片对比完成,根据相似度排序结果');
+ console.log('匹配到的图片路径对应的物品信息已包含在结果中');
+
+ // 输出相似度最高的前5个结果,用于调试
+ if (similarItems.length > 0) {
+ console.log('========== 相似度前5名 ==========');
+ similarItems.slice(0, 5).forEach((item, idx) => {
+ const featureSum = item.imageFeatures ? item.imageFeatures.reduce((sum, val) => sum + Math.abs(val || 0), 0) : 0;
+ const isMock = featureSum < 10 && item.imageFeatures && item.imageFeatures.length === 128;
+ console.log(` ${idx + 1}. ID: ${item.id}, 标题: ${item.title}, 类型: ${item.type}, 相似度: ${item.similarity.toFixed(3)}`);
+ console.log(` 图片路径: ${item.images[0]}`);
+ console.log(` 特征向量总和: ${featureSum.toFixed(3)}, 是否降级=${isMock}`);
+ console.log(` 特征向量前5维: [${item.imageFeatures ? item.imageFeatures.slice(0, 5).map(v => v.toFixed(3)).join(', ') : 'N/A'}]`);
+ });
+
+ // 检查前5名是否都使用了基于路径的特征提取
+ const top5UsingMock = similarItems.slice(0, 5).filter(item => {
+ if (!item.imageFeatures || item.imageFeatures.length === 0) return false;
+ const sum = item.imageFeatures.reduce((sum, val) => sum + Math.abs(val || 0), 0);
+ return sum < 10 && item.imageFeatures.length === 128;
+ });
+
+ if (top5UsingMock.length > 0) {
+ console.error(`❌ 严重警告:前5名中有 ${top5UsingMock.length} 个物品使用了基于路径的特征提取(降级方法)`);
+ console.error(` 这会导致不同图片的相似度异常高,因为基于路径的特征无法区分图片内容`);
+ console.error(` 建议:1) 检查AI服务配置 2) 确认AI服务正常工作 3) 检查云函数是否部署成功`);
+ console.error(` 当前AI配置 provider: ${this.AI_CONFIG.provider || '未配置'}`);
+ console.error(` 当前USE_TENCENT_CLOUD: ${this.AI_CONFIG.TENCENT_AI.USE_TENCENT_CLOUD || false}`);
+ }
+
+ // 检查相似度是否都相同(用于诊断问题)
+ const similarities = similarItems.map(item => item.similarity);
+ const uniqueSimilarities = new Set(similarities.map(s => s.toFixed(3)));
+ if (uniqueSimilarities.size === 1) {
+ console.warn('⚠️ 警告:所有物品的相似度都相同!这可能是特征提取或相似度计算的问题。');
+ console.warn(' 相似度值:', similarities[0]);
+ console.warn(' 请检查:1) 特征提取是否正常工作 2) 是否所有图片都降级到了相同的特征提取方法');
+ } else {
+ console.log(`✅ 相似度分布正常,共有 ${uniqueSimilarities.size} 个不同的相似度值`);
+ }
+ }
+
+ // 过滤相似度阈值:根据特征提取方法动态调整
+ // 如果使用了基于路径的特征提取,适当提高阈值以避免误匹配
+ const isUsingMockFeatures = targetSum < 10 && targetFeatures.length === 128;
+
+ // 调整阈值策略:
+ // 1. 如果使用真实AI特征提取,使用较低的阈值(0.1)以获取更多结果
+ // 2. 如果使用基于路径的特征提取,使用较高的阈值(0.8),但仍然允许一些结果
+ // 3. 如果使用增强特征提取(基于内容),使用中等阈值(0.3)
+ let MIN_SIMILARITY_THRESHOLD = 0.1; // 默认阈值
+
+ if (isUsingMockFeatures) {
+ // 基于路径的特征提取:使用较高阈值,但仍然允许一些结果
+ MIN_SIMILARITY_THRESHOLD = 0.8;
+ console.warn('⚠️ 使用基于路径的特征提取,提高相似度阈值到0.8以避免误匹配');
+ console.warn('⚠️ 注意:基于路径的特征无法准确区分图片内容,结果可能不准确');
+ console.warn('⚠️ 强烈建议:配置AI服务以获得准确的图片识别');
+ console.warn('⚠️ 当前AI配置 provider:', this.AI_CONFIG.provider || '未配置');
+ console.warn('⚠️ 当前USE_TENCENT_CLOUD:', this.AI_CONFIG.TENCENT_AI.USE_TENCENT_CLOUD || false);
+ } else if (targetFeatures.length >= 200) {
+ // 增强特征提取(基于内容):使用中等阈值
+ MIN_SIMILARITY_THRESHOLD = 0.3;
+ console.log('✅ 使用增强特征提取(基于内容),阈值: 0.3');
+ } else {
+ // 真实AI特征提取:使用较低阈值
+ MIN_SIMILARITY_THRESHOLD = 0.1;
+ console.log('✅ 使用真实AI特征提取,阈值: 0.1');
+ }
+
+ // 额外检查:如果相似度很高但都使用了基于路径的特征提取,且路径不同,适当降低相似度
+ // 但不完全禁止,因为可能路径相似但图片不同
+ const adjustedSimilarItems = similarItems.map(item => {
+ if (item.similarity > 0.7) {
+ const itemFeatureSum = item.imageFeatures ? item.imageFeatures.reduce((sum, val) => sum + Math.abs(val || 0), 0) : 0;
+ const itemIsMock = itemFeatureSum < 10 && item.imageFeatures && item.imageFeatures.length === 128;
+
+ if (isUsingMockFeatures && itemIsMock && imagePath !== item.images[0]) {
+ // 都使用了基于路径的特征提取,但路径不同
+ // 检查路径是否真的不同(不是同一张图片的不同路径)
+ const pathSimilarity = this.calculatePathSimilarity(imagePath, item.images[0]);
+
+ if (pathSimilarity < 0.5) {
+ // 路径完全不同,降低相似度
+ console.warn(`⚠️ 检测到异常:物品 ${item.id} 使用了基于路径的特征提取,但路径完全不同,相似度却很高(${item.similarity.toFixed(3)})`);
+ console.warn(` 目标图片路径: ${imagePath}`);
+ console.warn(` 物品图片路径: ${item.images[0]}`);
+ console.warn(` 路径相似度: ${pathSimilarity.toFixed(3)}`);
+ console.warn(` 适当降低相似度`);
+ return {
+ ...item,
+ similarity: Math.min(item.similarity, 0.5), // 降低到0.5以下,但仍然允许匹配
+ adjustedReason: '基于路径的特征提取但路径完全不同'
+ };
+ }
+ }
+ }
+ return item;
+ });
+
+ // 重新排序调整后的结果
+ adjustedSimilarItems.sort((a, b) => b.similarity - a.similarity);
+ similarItems = adjustedSimilarItems;
+
+ const filteredItems = similarItems.filter(item => item.similarity >= MIN_SIMILARITY_THRESHOLD);
+
+ console.log(`相似度过滤:原始 ${similarItems.length} 个结果,过滤后 ${filteredItems.length} 个结果(阈值:${MIN_SIMILARITY_THRESHOLD})`);
+
+ // 如果过滤后没有结果,降低阈值或返回相似度较高的结果
+ let finalItems = filteredItems;
+ if (filteredItems.length === 0 && similarItems.length > 0) {
+ console.warn('⚠️ 过滤后没有结果,降低阈值返回相似度较高的结果');
+
+ // 如果使用基于路径的特征提取,降低阈值到0.5
+ // 如果使用其他特征提取,降低阈值到0.05
+ const fallbackThreshold = isUsingMockFeatures ? 0.5 : 0.05;
+ const fallbackItems = similarItems.filter(item => item.similarity >= fallbackThreshold);
+
+ if (fallbackItems.length > 0) {
+ console.log(` 使用降级阈值 ${fallbackThreshold},找到 ${fallbackItems.length} 个结果`);
+ finalItems = fallbackItems.slice(0, 4); // 最多返回4个
+ } else {
+ // 如果还是没有结果,返回相似度最高的前4个(即使低于阈值)
+ console.warn(' 降级阈值后仍无结果,返回相似度最高的前4个');
+ finalItems = similarItems.slice(0, 4);
+ }
+ }
+
+ // 确保按相似度从高到低排序,并只保留前4个
+ finalItems.sort((a, b) => b.similarity - a.similarity);
+ finalItems = finalItems.slice(0, 4);
+
+ console.log(`最终结果数量: ${finalItems.length}(已限制为前4个)`);
+ console.log(`最终结果类型分布:`, {
+ lost: finalItems.filter(item => item.type === 'lost').length,
+ found: finalItems.filter(item => item.type === 'found').length
+ });
+
+ // 输出相似度范围
+ if (finalItems.length > 0) {
+ const similarities = finalItems.map(item => item.similarity);
+ console.log(`相似度范围: ${Math.min(...similarities).toFixed(3)} - ${Math.max(...similarities).toFixed(3)}`);
+ console.log('前4个结果的相似度:', finalItems.map((item, idx) => `${idx + 1}. ${item.similarity.toFixed(3)}`).join(', '));
+ }
+
+ // 返回前4个相似度最高的结果(用于缓存)
+ const highSimilarityItems = finalItems.slice(0, 4);
+
+ // 保存到缓存
+ this.globalData.similarityCache[cacheKey] = highSimilarityItems;
+
+ // 分页处理(由于只返回4个,第一页返回全部,后续页面为空)
+ const pageSize = 4; // 每页4个
+ const startIndex = (pageNum - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedItems = finalItems.slice(startIndex, endIndex);
+
+ // 返回与search.js期望格式一致的数据结构
+ const response = {
+ items: paginatedItems,
+ hasMore: endIndex < highSimilarityItems.length
+ };
+
+ console.log(`返回第 ${pageNum} 页结果,共 ${paginatedItems.length} 个,还有更多:${response.hasMore}`);
+ console.log(`返回结果类型分布:`, {
+ lost: paginatedItems.filter(item => item.type === 'lost').length,
+ found: paginatedItems.filter(item => item.type === 'found').length
+ });
+
+ callback(response);
+ }).catch((err) => {
+ console.error('特征提取或相似度计算失败:', err);
+ console.error('错误详情:', JSON.stringify(err));
+ // 即使出错也返回空结果,而不是什么都不返回
+ callback({ items: [], hasMore: false });
+ });
+ }).catch((err) => {
+ console.error('目标图片特征提取失败:', err);
+ callback({ items: [], hasMore: false });
+ });
+ });
+ },
+
+ // 从云数据库获取所有类型的物品用于搜索(失物和招领)
+ getAllItemsFromCloudForSearch: function(callback) {
+ // 检查环境是否已验证为无效
+ if (this.globalData.cloudEnvValidated === false && this.globalData.db === null) {
+ console.log('云开发环境已确认为无效,跳过云数据库查询');
+ callback([]);
+ return;
+ }
+
+ try {
+ const db = this.globalData.db;
+ const useCloudDB = db && wx.cloud;
+
+ if (!useCloudDB) {
+ console.log('云数据库不可用,跳过云数据库查询');
+ callback([]);
+ return;
+ }
+
+ console.log('从云数据库获取所有类型的物品用于搜索...');
+
+ // 查询所有类型的物品(不限制type)
+ db.collection('items')
+ .orderBy('publishTime', 'desc')
+ .limit(100) // 限制最多100条,避免查询过多数据
+ .get({
+ success: res => {
+ console.log('云数据库查询成功,返回', res.data.length, '条记录');
+
+ // 处理返回的数据,添加id字段和是否为当前用户发布的标记
+ const openid = wx.getStorageSync('openid') || 'anonymous';
+ const items = res.data.map(item => {
+ // 确保物品有图片
+ if (!item.images || !Array.isArray(item.images) || item.images.length === 0) {
+ return null; // 没有图片的物品不参与搜索
+ }
+
+ // 优先使用云端保存的特征向量(真实AI特征)
+ let persistedFeatures = null;
+ if (Array.isArray(item.imageFeatureVectors) && item.imageFeatureVectors.length > 0) {
+ const firstVector = item.imageFeatureVectors[0];
+ if (firstVector && Array.isArray(firstVector.features) && firstVector.features.length > 0) {
+ persistedFeatures = firstVector.features;
+ }
+ }
+
+ return {
+ id: item._id,
+ ...item,
+ isUserPublished: item._openid === openid,
+ // 确保图片特征存在(如果不存在,后续会生成)
+ imageFeatures: persistedFeatures || item.imageFeatures || null
+ };
+ }).filter(Boolean); // 过滤掉null值
+
+ console.log('云数据库有效物品数量:', items.length);
+
+ callback(items);
+ },
+ fail: err => {
+ // 检查是否是环境不存在的错误
+ const isEnvError = err.errCode === -501000 || (err.errMsg && err.errMsg.includes('env not exists'));
+
+ if (isEnvError) {
+ console.warn('云开发环境不存在,跳过云数据库查询');
+ // 清除数据库引用,确保后续不再尝试使用云数据库
+ this.globalData.db = null;
+ this.globalData.cloudEnvValidated = false;
+ } else {
+ console.error('云数据库查询失败:', err.errMsg || err);
+ }
+
+ // 返回空数组
+ callback([]);
+ }
+ });
+ } catch (error) {
+ console.error('获取云数据库物品时出错:', error);
+ callback([]);
+ }
+ },
+
+ // 获取所有可比较的物品(排除用户自己发布的物品,用于匹配)
+ getAllComparableItems: function(excludeUserItems = true) {
+ const items = [];
+
+ // 1. 添加用户发布的物品(如果需要用于其他用途)
+ if (!excludeUserItems) {
+ const userItems = this.globalData.userPublishedItems || [];
+ userItems.forEach(item => {
+ if (item.images && item.images.length > 0) {
+ items.push({
+ id: item.id,
+ type: item.type,
+ title: item.title,
+ description: item.description,
+ location: item.location,
+ time: item.time || item.date || item.publishTime,
+ images: item.images,
+ isUserPublished: true,
+ imageFeatures: item.imageFeatures || null
+ });
+ }
+ });
+ }
+
+ // 2. 添加所有其他用户发布的物品(从allPublishedItems)
+ const allPublishedItems = this.globalData.allPublishedItems || [];
+ allPublishedItems.forEach(item => {
+ // 排除用户自己发布的物品
+ if (!item.isUserPublished && item.images && item.images.length > 0) {
+ items.push({
+ id: item.id,
+ type: item.type,
+ title: item.title,
+ description: item.description,
+ location: item.location,
+ time: item.time || item.date || item.publishTime,
+ images: item.images,
+ isUserPublished: false,
+ imageFeatures: item.imageFeatures || null
+ });
+ }
+ });
+
+ // 3. 如果云数据库已初始化,尝试从数据库获取更多物品
+ // 注意:由于这是同步方法,云数据库查询是异步的,所以这里暂时只使用内存中的数据
+ // 实际应用中可以考虑预先加载或使用异步方式
+
+ // 4. 不再添加模拟数据,只使用真实发布的物品
+ // 如果希望添加模拟数据用于测试,可以取消注释下面的代码
+ /*
+ if (items.length < 3) {
+ const mockLostItems = [
+ {
+ id: 'mock_lost_1',
+ type: 'lost',
+ title: '丢失的手机',
+ description: '在图书馆丢失的黑色智能手机,有明显的保护壳磨损痕迹。',
+ location: '学校图书馆',
+ time: new Date().toISOString().split('T')[0],
+ images: ['/images/lost.png'],
+ isUserPublished: false,
+ imageFeatures: this.generateMockAIFeatures('/images/lost.png')
+ },
+ {
+ id: 'mock_lost_2',
+ type: 'lost',
+ title: '钱包丢失',
+ description: '棕色皮质钱包,内有学生证和银行卡。',
+ location: '校园食堂',
+ time: new Date(Date.now() - 86400000).toISOString().split('T')[0],
+ images: ['/images/found.png'],
+ isUserPublished: false,
+ imageFeatures: this.generateMockAIFeatures('/images/found.png')
+ },
+ {
+ id: 'mock_lost_3',
+ type: 'lost',
+ title: '钥匙串',
+ description: '银色钥匙串,上面有宿舍和自行车钥匙。',
+ location: '教学楼',
+ time: new Date(Date.now() - 172800000).toISOString().split('T')[0],
+ images: ['/images/match.svg'],
+ isUserPublished: false,
+ imageFeatures: this.generateMockAIFeatures('/images/match.svg')
+ }
+ ];
+
+ const mockFoundItems = [
+ {
+ id: 'mock_found_1',
+ type: 'found',
+ title: '捡到钥匙串',
+ description: '在教学楼一楼捡到的钥匙串,有宿舍和自行车钥匙。',
+ location: '教学楼',
+ time: new Date(Date.now() - 86400000).toISOString().split('T')[0],
+ images: ['/images/found.png'],
+ isUserPublished: false,
+ imageFeatures: this.generateMockAIFeatures('/images/found.png')
+ },
+ {
+ id: 'mock_found_2',
+ type: 'found',
+ title: '捡到钱包',
+ description: '在食堂捡到的棕色钱包,内有学生证。',
+ location: '校园食堂',
+ time: new Date(Date.now() - 172800000).toISOString().split('T')[0],
+ images: ['/images/lost.png'],
+ isUserPublished: false,
+ imageFeatures: this.generateMockAIFeatures('/images/lost.png')
+ },
+ {
+ id: 'mock_found_3',
+ type: 'found',
+ title: '捡到手机',
+ description: '在图书馆发现的黑色智能手机。',
+ location: '学校图书馆',
+ time: new Date(Date.now() - 259200000).toISOString().split('T')[0],
+ images: ['/images/match.svg'],
+ isUserPublished: false,
+ imageFeatures: this.generateMockAIFeatures('/images/match.svg')
+ }
+ ];
+
+ // 添加模拟数据
+ items.push(...mockLostItems);
+ items.push(...mockFoundItems);
+ }
+ */
+
+ console.log('getAllComparableItems 返回物品数量:', items.length, '排除用户物品:', excludeUserItems, '(已移除模拟数据)');
+ return items;
+ },
+
+ // AI服务配置(可配置为真实AI服务或开源模型)
+ AI_CONFIG: {
+
+ provider: 'tencent',
+
+
+ // ========== 腾讯AI配置 ==========
+ TENCENT_AI: {
+ // 方式1:腾讯AI开放平台(api.ai.qq.com)- 推荐使用
+ // 访问 https://ai.qq.com/ 注册并创建应用获取
+ APP_ID: '1385619376', // 腾讯AI开放平台 AppID(从控制台获取)
+ APP_KEY: 'AKIDrr9j8LL8oJbgYCesseDZ8WN5a9XlodEb', // 腾讯AI开放平台 AppKey(从控制台获取)
+
+ // 方式2:腾讯云图像识别(使用您控制台的SecretId和SecretKey)
+ // 使用腾讯云控制台的API密钥(SecretId和SecretKey)
+ USE_TENCENT_CLOUD: true, // 设置为true使用腾讯云API(需要服务器端支持)
+ SECRET_ID: 'AKIDU4ScZlvNrGXcUCiw53pQn161mNoKn6FD', // 腾讯云 SecretId(从控制台获取)
+ SECRET_KEY: 'ujOuFPM9qUyISSHzYw9OoO4Qp14f7Rcf', // 腾讯云 SecretKey(创建时保存,如果丢失需重新创建)
+ REGION: 'ap-beijing', // 地域,如:ap-beijing(北京)、ap-shanghai(上海)
+
+ // 腾讯AI开放平台API地址(图像识别)
+ API_URL: 'https://api.ai.qq.com/fcgi-bin/vision/vision_imgidentify',
+ // 腾讯云图像识别API地址
+ CLOUD_API_URL: 'https://iai.tencentcloudapi.com'
+ },
+
+ // ========== 开源模型配置 ==========
+ MOBILENET: {
+ // MobileNet模型URL(可以从CDN加载,避免增加包体积)
+ MODEL_URL: 'https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v2_1.0_224/model.json',
+ // 或者使用本地模型文件(需要先下载模型文件到项目)
+ // MODEL_URL: '/models/mobilenet/model.json',
+ // 特征向量维度(MobileNet输出1000维)
+ FEATURE_DIM: 1000
+ },
+
+ // ========== 自定义AI服务配置 ==========
+ CUSTOM: {
+ API_URL: 'https://api.example.com/ai/image-features',
+ API_KEY: 'YOUR_API_KEY',
+ // 请求头配置
+ headers: {
+ 'Authorization': 'Bearer YOUR_API_KEY',
+ 'Content-Type': 'application/json'
+ }
+ },
+
+ // 使用增强特征提取(本地改进版,类似淘宝以图搜物)
+ // 当真实AI服务失败时,会降级到此方法
+ useEnhancedFeatures: true
+ },
+
+ // 上传图片到AI服务并提取特征(支持真实AI服务和增强本地特征)
+ uploadImageToAI: function(imagePath, callback) {
+ console.log('上传图片到AI服务提取特征:', imagePath);
+
+ // 如果配置了真实AI服务,优先使用
+ if (this.AI_CONFIG.provider) {
+ console.log(`使用${this.AI_CONFIG.provider} AI服务`);
+
+ if (this.AI_CONFIG.provider === 'baidu') {
+ this.callBaiduAI(imagePath, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ callback(features);
+ } else {
+ console.warn('百度AI返回空特征,降级到增强特征提取');
+ this.fallbackToLocalFeatures(imagePath, callback);
+ }
+ });
+ return;
+ } else if (this.AI_CONFIG.provider === 'tencent') {
+ this.callTencentAI(imagePath, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ callback(features);
+ } else {
+ console.warn('腾讯AI返回空特征,降级到增强特征提取');
+ this.fallbackToLocalFeatures(imagePath, callback);
+ }
+ });
+ return;
+ } else if (this.AI_CONFIG.provider === 'mobilenet') {
+ this.callMobileNet(imagePath, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ callback(features);
+ } else {
+ console.warn('MobileNet返回空特征,降级到增强特征提取');
+ this.fallbackToLocalFeatures(imagePath, callback);
+ }
+ });
+ return;
+ } else if (this.AI_CONFIG.provider === 'custom') {
+ this.callCustomAIService(imagePath, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ callback(features);
+ } else {
+ console.warn('自定义AI服务返回空特征,降级到增强特征提取');
+ this.fallbackToLocalFeatures(imagePath, callback);
+ }
+ });
+ return;
+ }
+ }
+
+ // 否则使用增强的本地特征提取(类似淘宝以图搜物)
+ if (this.AI_CONFIG.useEnhancedFeatures) {
+ this.extractEnhancedImageFeatures(imagePath, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ callback(features);
+ } else {
+ console.warn('增强特征提取失败,降级到基础特征提取');
+ this.extractImageContentFeatures(imagePath, (baseFeatures) => {
+ if (baseFeatures && Array.isArray(baseFeatures) && baseFeatures.length > 0) {
+ callback(baseFeatures);
+ } else {
+ console.warn('基础特征提取也失败,放弃该图片的特征');
+ callback([]);
+ }
+ });
+ }
+ });
+ } else {
+ // 使用基础特征提取
+ this.extractImageContentFeatures(imagePath, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ callback(features);
+ } else {
+ console.warn('基础特征提取失败,放弃该图片的特征');
+ callback([]);
+ }
+ });
+ }
+ },
+
+ // 降级到本地特征提取(统一处理)
+ fallbackToLocalFeatures: function(imagePath, callback) {
+ if (this.AI_CONFIG.useEnhancedFeatures) {
+ this.extractEnhancedImageFeatures(imagePath, (enhancedFeatures) => {
+ if (enhancedFeatures && Array.isArray(enhancedFeatures) && enhancedFeatures.length > 0) {
+ callback(enhancedFeatures);
+ } else {
+ console.warn('增强特征提取也失败,降级到基础特征提取');
+ this.extractImageContentFeatures(imagePath, (baseFeatures) => {
+ if (baseFeatures && Array.isArray(baseFeatures) && baseFeatures.length > 0) {
+ callback(baseFeatures);
+ } else {
+ console.warn('基础特征提取也失败,放弃该图片的特征');
+ callback([]);
+ }
+ });
+ }
+ });
+ } else {
+ this.extractImageContentFeatures(imagePath, callback);
+ }
+ },
+
+ // 调用百度AI服务
+ callBaiduAI: function(imagePath, callback) {
+ console.log('调用百度AI服务');
+
+ const config = this.AI_CONFIG.BAIDU_AI;
+ if (!config.API_KEY || !config.SECRET_KEY || config.API_KEY === 'YOUR_BAIDU_API_KEY') {
+ console.error('百度AI配置不完整,请配置API_KEY和SECRET_KEY');
+ callback(null);
+ return;
+ }
+
+ // 先获取access_token
+ this.getBaiduAccessToken((accessToken) => {
+ if (!accessToken) {
+ console.error('获取百度AI access_token失败');
+ callback(null);
+ return;
+ }
+
+ // 读取图片文件
+ wx.getFileSystemManager().readFile({
+ filePath: imagePath,
+ encoding: 'base64',
+ success: (res) => {
+ const base64Image = res.data;
+
+ // 调用百度AI图像识别API
+ wx.request({
+ url: `${config.API_URL}?access_token=${accessToken}`,
+ method: 'POST',
+ header: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ data: {
+ image: base64Image
+ },
+ success: (apiRes) => {
+ console.log('百度AI返回结果:', apiRes.data);
+
+ if (apiRes.data && apiRes.data.result) {
+ // 百度AI返回的是识别结果,需要转换为特征向量
+ const features = this.convertBaiduAIResultToFeatures(apiRes.data.result);
+ console.log('百度AI特征向量维度:', features.length);
+ callback(features);
+ } else {
+ console.warn('百度AI返回格式错误:', apiRes.data);
+ callback(null);
+ }
+ },
+ fail: (err) => {
+ console.error('百度AI API调用失败:', err);
+ callback(null);
+ }
+ });
+ },
+ fail: (err) => {
+ console.error('读取图片文件失败:', err);
+ callback(null);
+ }
+ });
+ });
+ },
+
+ // 获取百度AI access_token
+ getBaiduAccessToken: function(callback) {
+ const config = this.AI_CONFIG.BAIDU_AI;
+ const cacheKey = 'baidu_access_token';
+ const tokenCache = wx.getStorageSync(cacheKey);
+ const tokenExpire = wx.getStorageSync('baidu_token_expire') || 0;
+
+ // 检查缓存是否有效(百度token有效期30天,我们缓存25天)
+ if (tokenCache && tokenExpire > Date.now()) {
+ console.log('使用缓存的百度AI access_token');
+ callback(tokenCache);
+ return;
+ }
+
+ // 获取新的access_token
+ wx.request({
+ url: `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${config.API_KEY}&client_secret=${config.SECRET_KEY}`,
+ method: 'GET',
+ success: (res) => {
+ if (res.data && res.data.access_token) {
+ const accessToken = res.data.access_token;
+ const expireTime = Date.now() + (25 * 24 * 60 * 60 * 1000); // 25天后过期
+
+ // 缓存token
+ wx.setStorageSync(cacheKey, accessToken);
+ wx.setStorageSync('baidu_token_expire', expireTime);
+
+ console.log('获取百度AI access_token成功');
+ callback(accessToken);
+ } else {
+ console.error('获取百度AI access_token失败:', res.data);
+ callback(null);
+ }
+ },
+ fail: (err) => {
+ console.error('请求百度AI access_token失败:', err);
+ callback(null);
+ }
+ });
+ },
+
+ // 将百度AI识别结果转换为特征向量
+ convertBaiduAIResultToFeatures: function(result) {
+ // 百度AI返回格式:{ result: [{ keyword: '手机', score: 0.95 }, ...] }
+ const features = [];
+
+ console.log('百度AI识别结果:', JSON.stringify(result));
+
+ // 1. 提取关键词和置信度(128维,扩大维度以提高区分度)
+ const keywordFeatures = new Array(128).fill(0);
+ if (result && Array.isArray(result)) {
+ result.slice(0, 128).forEach((item, idx) => {
+ if (item.score) {
+ keywordFeatures[idx] = item.score; // 置信度作为特征值
+ }
+ });
+ }
+ features.push(...keywordFeatures);
+
+ // 2. 提取关键词的哈希特征(128维)
+ const hashFeatures = new Array(128).fill(0);
+ if (result && Array.isArray(result)) {
+ result.forEach((item, idx) => {
+ if (item.keyword) {
+ let hash = 0;
+ for (let i = 0; i < item.keyword.length; i++) {
+ hash = ((hash << 5) - hash) + item.keyword.charCodeAt(i);
+ hash = hash & 0x7FFFFFFF;
+ }
+ hashFeatures[idx % 128] += (hash % 1000) / 1000;
+ }
+ });
+ }
+ features.push(...hashFeatures);
+
+ // 3. 提取关键词的字符特征(64维)
+ const charFeatures = new Array(64).fill(0);
+ if (result && Array.isArray(result)) {
+ result.forEach((item) => {
+ if (item.keyword) {
+ // 将关键词的每个字符转换为特征
+ for (let i = 0; i < Math.min(item.keyword.length, 10); i++) {
+ const charCode = item.keyword.charCodeAt(i);
+ charFeatures[charCode % 64] += (item.score || 0) / 10;
+ }
+ }
+ });
+ }
+ features.push(...charFeatures);
+
+ // 4. 统计特征(64维)
+ const statsFeatures = new Array(64).fill(0);
+ if (result && Array.isArray(result) && result.length > 0) {
+ statsFeatures[0] = Math.min(result.length / 100, 1); // 识别结果数量(归一化)
+ const avgScore = result.reduce((sum, item) => sum + (item.score || 0), 0) / result.length;
+ statsFeatures[1] = avgScore; // 平均置信度
+ statsFeatures[2] = Math.max(...result.map(item => item.score || 0)); // 最高置信度
+
+ // 提取前几个关键词的置信度
+ result.slice(0, 10).forEach((item, idx) => {
+ if (item.score) {
+ statsFeatures[3 + idx] = item.score;
+ }
+ });
+ }
+ features.push(...statsFeatures);
+
+ // 总共384维,与增强特征(564维)兼容,通过截断处理
+ console.log(`百度AI特征向量生成完成,维度: ${features.length}`);
+ return features;
+ },
+
+ // 调用腾讯AI服务(支持两种方式)
+ callTencentAI: function(imagePath, callback) {
+ const config = this.AI_CONFIG.TENCENT_AI;
+
+ // 优先使用腾讯云API(如果配置了)
+ if (config.USE_TENCENT_CLOUD && config.SECRET_ID && config.SECRET_KEY &&
+ config.SECRET_ID !== 'YOUR_SECRET_ID') {
+ console.log('使用腾讯云图像识别API');
+ this.callTencentCloudAI(imagePath, callback);
+ return;
+ }
+
+ // 否则使用腾讯AI开放平台
+ console.log('使用腾讯AI开放平台');
+ console.log('腾讯AI配置检查:', {
+ APP_ID: config.APP_ID ? (config.APP_ID.substring(0, 4) + '...') : '未配置',
+ APP_KEY: config.APP_KEY ? (config.APP_KEY.substring(0, 4) + '...') : '未配置',
+ APP_ID_valid: config.APP_ID && config.APP_ID !== 'YOUR_TENCENT_APP_ID',
+ APP_KEY_valid: config.APP_KEY && config.APP_KEY !== 'YOUR_TENCENT_APP_KEY' && config.APP_KEY !== 'YOUR_SECRET_KEY'
+ });
+
+ if (!config.APP_ID || !config.APP_KEY || config.APP_ID === 'YOUR_TENCENT_APP_ID') {
+ console.error('❌ 腾讯AI配置不完整,请配置APP_ID和APP_KEY');
+ console.error('当前配置:', {
+ APP_ID: config.APP_ID,
+ APP_KEY: config.APP_KEY ? config.APP_KEY.substring(0, 10) + '...' : '未配置'
+ });
+ console.error('或者设置USE_TENCENT_CLOUD=true并配置SECRET_ID和SECRET_KEY使用腾讯云API');
+ console.warn('⚠️ 将降级到本地特征提取');
+ callback(null);
+ return;
+ }
+
+ // 检查AppKey是否是SecretId(常见错误)
+ if (config.APP_KEY && config.APP_KEY.startsWith('AKID')) {
+ console.warn('⚠️ 警告:APP_KEY看起来像是腾讯云的SecretId,而不是腾讯AI开放平台的AppKey');
+ console.warn('⚠️ 腾讯AI开放平台的AppKey通常是32位字符串,不是以AKID开头');
+ console.warn('⚠️ 请访问 https://ai.qq.com/ 获取正确的AppKey');
+ }
+
+ // 读取图片文件
+ wx.getFileSystemManager().readFile({
+ filePath: imagePath,
+ encoding: 'base64',
+ success: (res) => {
+ const base64Image = res.data;
+
+ // 生成腾讯AI签名
+ const timestamp = Math.floor(Date.now() / 1000);
+ const nonceStr = Math.random().toString(36).substring(2, 15);
+
+ // 构建参数字符串用于签名
+ const params = {
+ app_id: config.APP_ID,
+ time_stamp: timestamp,
+ nonce_str: nonceStr,
+ image: base64Image
+ };
+
+ // 生成签名(MD5)
+ const sign = this.generateTencentAISign(params, config.APP_KEY);
+
+ // 调用腾讯AI图像识别API
+ wx.request({
+ url: config.API_URL,
+ method: 'POST',
+ header: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ data: {
+ ...params,
+ sign: sign
+ },
+ success: (apiRes) => {
+ console.log('腾讯AI返回结果:', apiRes.data);
+
+ if (apiRes.data && apiRes.data.ret === 0) {
+ // 腾讯AI返回识别结果,转换为特征向量
+ const features = this.convertTencentAIResultToFeatures(apiRes.data.data || apiRes.data);
+ console.log('腾讯AI特征向量维度:', features.length);
+ callback(features);
+ } else {
+ console.warn('腾讯AI返回错误:', apiRes.data);
+ if (apiRes.data && apiRes.data.ret) {
+ console.warn('错误码:', apiRes.data.ret, '错误信息:', apiRes.data.msg);
+ }
+ callback(null);
+ }
+ },
+ fail: (err) => {
+ console.error('腾讯AI API调用失败:', err);
+ callback(null);
+ }
+ });
+ },
+ fail: (err) => {
+ console.error('读取图片文件失败:', err);
+ callback(null);
+ }
+ });
+ },
+
+ // 生成腾讯AI签名(MD5)
+ generateTencentAISign: function(params, appKey) {
+ // 1. 按键名排序
+ const sortedKeys = Object.keys(params).sort();
+
+ // 2. 拼接参数字符串(需要对值进行URL编码)
+ let signStr = '';
+ sortedKeys.forEach(key => {
+ if (params[key] && key !== 'sign') {
+ // 对参数值进行URL编码
+ const value = typeof params[key] === 'string' ? params[key] : String(params[key]);
+ signStr += `${key}=${encodeURIComponent(value)}&`;
+ }
+ });
+
+ // 3. 拼接app_key
+ signStr += `app_key=${appKey}`;
+
+ console.log('签名原始字符串(前100字符):', signStr.substring(0, 100) + '...');
+
+ // 4. MD5加密
+ // 优先使用crypto-js库(如果已安装:npm install crypto-js)
+ if (typeof CryptoJS !== 'undefined' && CryptoJS.MD5) {
+ const sign = CryptoJS.MD5(signStr).toString().toUpperCase();
+ console.log('✅ 使用CryptoJS生成MD5签名:', sign.substring(0, 16) + '...');
+ return sign;
+ }
+
+ // 如果没有crypto-js,尝试使用全局MD5函数
+ if (typeof md5 !== 'undefined' && typeof md5 === 'function') {
+ const sign = md5(signStr).toUpperCase();
+ console.log('✅ 使用md5函数生成签名:', sign.substring(0, 16) + '...');
+ return sign;
+ }
+
+ // 降级方案:使用简单哈希(不推荐,签名可能不正确)
+ console.warn('⚠️ 未安装MD5库,使用简化签名(可能失败)');
+ console.warn('⚠️ 建议安装crypto-js:npm install crypto-js');
+ console.warn('⚠️ 然后在app.js顶部添加:const CryptoJS = require("crypto-js")');
+
+ let hash = 0;
+ for (let i = 0; i < signStr.length; i++) {
+ const char = signStr.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash;
+ }
+
+ // 转换为32位十六进制字符串(模拟MD5)
+ const md5Hash = Math.abs(hash).toString(16).toUpperCase().padStart(32, '0');
+
+ return md5Hash;
+ },
+
+ // 调用腾讯云图像识别API(使用SecretId和SecretKey)
+ callTencentCloudAI: function(imagePath, callback) {
+ console.log('========== 调用腾讯云图像识别API ==========');
+ console.log('SecretId:', this.AI_CONFIG.TENCENT_AI.SECRET_ID ? (this.AI_CONFIG.TENCENT_AI.SECRET_ID.substring(0, 8) + '...') : '未配置');
+ console.log('SecretKey:', this.AI_CONFIG.TENCENT_AI.SECRET_KEY ? (this.AI_CONFIG.TENCENT_AI.SECRET_KEY.substring(0, 8) + '...') : '未配置');
+ console.log('Region:', this.AI_CONFIG.TENCENT_AI.REGION);
+
+ const config = this.AI_CONFIG.TENCENT_AI;
+
+ // 检查配置
+ if (!config.SECRET_ID || !config.SECRET_KEY || config.SECRET_KEY === 'YOUR_SECRET_KEY') {
+ console.error('❌ 腾讯云配置不完整,请配置SECRET_ID和SECRET_KEY');
+ console.warn('⚠️ 将降级到腾讯AI开放平台');
+ this.callTencentAI(imagePath, callback);
+ return;
+ }
+
+ // 读取图片文件
+ wx.getFileSystemManager().readFile({
+ filePath: imagePath,
+ encoding: 'base64',
+ success: (res) => {
+ const base64Image = res.data;
+ console.log('图片读取成功,Base64长度:', base64Image.length);
+
+ // 腾讯云API v3需要复杂的签名,建议使用云函数或服务器端
+ // 这里提供一个通过云函数调用的方案
+
+ // 检查云开发环境
+ if (!wx.cloud) {
+ console.error('❌ 未启用云开发,无法调用云函数');
+ console.warn('⚠️ 腾讯云API需要通过云函数调用,请:');
+ console.warn(' 1) 在app.js中初始化云开发环境');
+ console.warn(' 2) 创建云函数 tencentAI');
+ console.warn(' 3) 或者降级使用腾讯AI开放平台');
+ console.warn('⚠️ 将降级到腾讯AI开放平台');
+ this.callTencentAI(imagePath, callback);
+ return;
+ }
+
+ // 如果有云函数,通过云函数调用
+ if (typeof wx.cloud.callFunction === 'function') {
+ console.log('通过云函数调用腾讯云API...');
+ wx.cloud.callFunction({
+ name: 'tencentAI', // 需要在云函数中创建
+ data: {
+ image: base64Image,
+ secretId: config.SECRET_ID,
+ secretKey: config.SECRET_KEY,
+ region: config.REGION || 'ap-beijing'
+ },
+ success: (cloudRes) => {
+ console.log('云函数调用成功,返回结果:', cloudRes.result ? '有数据' : '无数据');
+ if (cloudRes.result && cloudRes.result.features) {
+ console.log('✅ 腾讯云AI返回特征向量,维度:', cloudRes.result.features.length);
+ callback(cloudRes.result.features);
+ } else if (cloudRes.result && cloudRes.result.error) {
+ console.error('❌ 腾讯云API返回错误:', cloudRes.result.error);
+ console.warn('⚠️ 将降级到腾讯AI开放平台');
+ this.callTencentAI(imagePath, callback);
+ } else {
+ console.warn('⚠️ 腾讯云AI返回格式错误,将降级到腾讯AI开放平台');
+ this.callTencentAI(imagePath, callback);
+ }
+ },
+ fail: (err) => {
+ console.error('❌ 云函数调用失败:', err);
+ console.warn('可能原因:1) 云函数 tencentAI 不存在 2) 云函数代码错误 3) 网络问题');
+ console.warn('⚠️ 将降级到腾讯AI开放平台');
+ this.callTencentAI(imagePath, callback);
+ }
+ });
+ } else {
+ console.error('❌ wx.cloud.callFunction 不可用');
+ console.warn('⚠️ 将降级到腾讯AI开放平台');
+ this.callTencentAI(imagePath, callback);
+ }
+ },
+ fail: (err) => {
+ console.error('❌ 读取图片文件失败:', err);
+ callback(null);
+ }
+ });
+ },
+
+ // 将腾讯AI识别结果转换为特征向量
+ convertTencentAIResultToFeatures: function(data) {
+ const features = [];
+
+ console.log('腾讯AI识别结果:', JSON.stringify(data));
+
+ // 腾讯AI返回格式根据不同的识别类型不同
+ // 这里提供一个通用的转换方法
+
+ // 1. 提取标签和置信度(128维)
+ const tagFeatures = new Array(128).fill(0);
+ if (data.tags && Array.isArray(data.tags)) {
+ data.tags.slice(0, 128).forEach((tag, idx) => {
+ if (tag.confidence !== undefined) {
+ tagFeatures[idx] = tag.confidence / 100; // 归一化到0-1
+ }
+ });
+ }
+ features.push(...tagFeatures);
+
+ // 2. 提取类别特征(128维)
+ const categoryFeatures = new Array(128).fill(0);
+ if (data.category) {
+ let hash = 0;
+ for (let i = 0; i < data.category.length; i++) {
+ hash = ((hash << 5) - hash) + data.category.charCodeAt(i);
+ hash = hash & 0x7FFFFFFF;
+ }
+ categoryFeatures[hash % 128] = 1;
+ }
+ features.push(...categoryFeatures);
+
+ // 3. 提取标签哈希特征(64维)
+ const tagHashFeatures = new Array(64).fill(0);
+ if (data.tags && Array.isArray(data.tags)) {
+ data.tags.forEach((tag) => {
+ if (tag.tag_name) {
+ let hash = 0;
+ for (let i = 0; i < tag.tag_name.length; i++) {
+ hash = ((hash << 5) - hash) + tag.tag_name.charCodeAt(i);
+ hash = hash & 0x7FFFFFFF;
+ }
+ tagHashFeatures[hash % 64] += (tag.confidence || 0) / 100;
+ }
+ });
+ }
+ features.push(...tagHashFeatures);
+
+ // 4. 统计特征(64维)
+ const statsFeatures = new Array(64).fill(0);
+ if (data.tags && Array.isArray(data.tags) && data.tags.length > 0) {
+ statsFeatures[0] = Math.min(data.tags.length / 100, 1);
+ const avgConf = data.tags.reduce((sum, tag) => sum + (tag.confidence || 0), 0) / data.tags.length;
+ statsFeatures[1] = avgConf / 100;
+ statsFeatures[2] = Math.max(...data.tags.map(tag => tag.confidence || 0)) / 100;
+
+ // 提取前几个标签的置信度
+ data.tags.slice(0, 10).forEach((tag, idx) => {
+ if (tag.confidence !== undefined) {
+ statsFeatures[3 + idx] = tag.confidence / 100;
+ }
+ });
+ }
+ features.push(...statsFeatures);
+
+ // 总共384维,与百度AI和增强特征兼容
+ console.log(`腾讯AI特征向量生成完成,维度: ${features.length}`);
+ return features;
+ },
+
+ // 调用MobileNet开源模型(TensorFlow.js)
+ callMobileNet: function(imagePath, callback) {
+ console.log('使用MobileNet开源模型提取特征');
+
+ // 检查是否支持
+ try {
+ const mobilenetExtractor = require('./utils/mobilenetFeatureExtractor.js');
+
+ // 检查是否支持
+ if (!mobilenetExtractor.isSupported || !mobilenetExtractor.isSupported()) {
+ console.warn('MobileNet不支持当前环境,降级到增强特征提取');
+ console.warn('需要:1) 引入TensorFlow.js 2) 支持createOffscreenCanvas');
+ this.fallbackToLocalFeatures(imagePath, callback);
+ return;
+ }
+
+ // 提取特征
+ mobilenetExtractor.extractFeatures(imagePath)
+ .then(features => {
+ console.log('MobileNet特征提取成功,维度:', features.length);
+ callback(features);
+ })
+ .catch(err => {
+ console.error('MobileNet特征提取失败:', err);
+ console.warn('降级到增强特征提取');
+ this.fallbackToLocalFeatures(imagePath, callback);
+ });
+ } catch (error) {
+ console.error('MobileNet模块加载失败:', error);
+ console.warn('请确保已安装: npm install @tensorflow/tfjs-platform-miniprogram');
+ console.warn('或创建utils/mobilenetFeatureExtractor.js文件');
+ this.fallbackToLocalFeatures(imagePath, callback);
+ }
+ },
+
+ // 调用自定义AI服务
+ callCustomAIService: function(imagePath, callback) {
+ console.log('调用自定义AI服务:', this.AI_CONFIG.CUSTOM.API_URL);
+
+ const config = this.AI_CONFIG.CUSTOM;
+ if (!config.API_URL || config.API_URL === 'https://api.example.com/ai/image-features') {
+ console.error('自定义AI服务配置不完整');
+ callback(null);
+ return;
+ }
+
+ // 读取图片文件
+ wx.getFileSystemManager().readFile({
+ filePath: imagePath,
+ encoding: 'base64',
+ success: (res) => {
+ // 调用自定义AI服务API
+ wx.request({
+ url: config.API_URL,
+ method: 'POST',
+ header: config.headers || {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${config.API_KEY}`
+ },
+ data: {
+ image: res.data,
+ type: 'base64'
+ },
+ success: (apiRes) => {
+ if (apiRes.data && apiRes.data.features) {
+ console.log('自定义AI服务返回特征向量,维度:', apiRes.data.features.length);
+ callback(apiRes.data.features);
+ } else {
+ console.warn('自定义AI服务返回格式错误,期望格式: { features: [...] }');
+ callback(null);
+ }
+ },
+ fail: (err) => {
+ console.error('自定义AI服务调用失败:', err);
+ callback(null);
+ }
+ });
+ },
+ fail: (err) => {
+ console.error('读取图片文件失败:', err);
+ callback(null);
+ }
+ });
+ },
+
+ // 计算与AI特征的相似度
+ compareWithAIFeatures: function(targetFeatures, items, callback) {
+ // 简化实现,直接对每个物品计算相似度
+ const similarItems = items.map(item => ({
+ ...item,
+ similarity: this.calculateCosineSimilarity(targetFeatures, item.imageFeatures),
+ isAISimilarity: true
+ }));
+
+ callback(similarItems);
+ },
+
+ // 获取或生成物品图片的特征向量(支持增强特征和真实AI)
+ getOrGenerateItemFeatures: function(imageUrl, callback) {
+ // 检查物品特征缓存
+ if (!this.globalData.itemFeaturesCache) {
+ this.globalData.itemFeaturesCache = {};
+ }
+
+ // 生成缓存键
+ const cacheKey = this.generateCacheKey(imageUrl);
+
+ // 如果缓存中已有,则直接返回
+ if (this.globalData.itemFeaturesCache[cacheKey]) {
+ callback(this.globalData.itemFeaturesCache[cacheKey]);
+ return;
+ }
+
+ // 使用增强特征提取或真实AI服务
+ if (this.AI_CONFIG.useRealAI && this.AI_CONFIG.API_URL && this.AI_CONFIG.API_KEY) {
+ this.callRealAIService(imageUrl, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ this.globalData.itemFeaturesCache[cacheKey] = features;
+ }
+ callback(features);
+ });
+ } else if (this.AI_CONFIG.useEnhancedFeatures) {
+ this.extractEnhancedImageFeatures(imageUrl, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ this.globalData.itemFeaturesCache[cacheKey] = features;
+ }
+ callback(features);
+ });
+ } else {
+ // 使用基础特征提取
+ this.extractImageContentFeatures(imageUrl, (features) => {
+ if (features && Array.isArray(features) && features.length > 0) {
+ this.globalData.itemFeaturesCache[cacheKey] = features;
+ }
+ callback(features);
+ });
+ }
+ },
+
+ // 基于图片内容提取特征向量(使用canvas读取像素数据)
+ extractImageContentFeatures: function(imagePath, callback) {
+ console.log('开始提取图片内容特征:', imagePath);
+
+ if (!this.globalData.itemFeaturesCache) {
+ this.globalData.itemFeaturesCache = {};
+ }
+ const cacheKey = this.generateCacheKey(imagePath);
+ if (this.globalData.itemFeaturesCache[cacheKey]) {
+ console.log('使用缓存的图片内容特征');
+ callback(this.globalData.itemFeaturesCache[cacheKey]);
+ return;
+ }
+
+ this.resolveImagePathForFeatures(imagePath, (resolvedPath) => {
+ wx.getImageInfo({
+ src: resolvedPath,
+ success: (res) => {
+ const width = res.width;
+ const height = res.height;
+ console.log(`图片信息: ${width}x${height}, 解析后路径: ${resolvedPath}`);
+
+ this.readImagePixels(resolvedPath, width, height, (pixelFeatures) => {
+ if (pixelFeatures) {
+ const features = this.generateContentBasedFeatures(pixelFeatures, width, height);
+ if (features && Array.isArray(features) && features.length > 0) {
+ this.globalData.itemFeaturesCache[cacheKey] = features;
+ callback(features);
+ } else {
+ callback([]);
+ }
+ } else {
+ console.warn('读取像素数据失败,无法生成内容特征');
+ callback([]);
+ }
+ });
+ },
+ fail: (err) => {
+ console.error('获取图片信息失败:', err);
+ callback([]);
+ }
+ });
+ }, (error) => {
+ console.warn('解析图片路径失败,无法提取内容特征:', error);
+ callback([]);
+ });
+ },
+
+ // 将cloud://或相对路径转换为可读取像素的真实路径
+ resolveImagePathForFeatures: function(imagePath, onSuccess, onFail) {
+ if (!imagePath || typeof imagePath !== 'string') {
+ onFail && onFail(new Error('无效的图片路径'));
+ return;
+ }
+
+ // 非cloud路径直接返回
+ if (!imagePath.startsWith('cloud://')) {
+ onSuccess(imagePath);
+ return;
+ }
+
+ if (!wx.cloud || typeof wx.cloud.getTempFileURL !== 'function') {
+ onFail && onFail(new Error('当前环境不支持云文件转换'));
+ return;
+ }
+
+ if (!this.cloudUrlCache) {
+ this.cloudUrlCache = {};
+ }
+
+ if (this.cloudUrlCache[imagePath]) {
+ onSuccess(this.cloudUrlCache[imagePath]);
+ return;
+ }
+
+ wx.cloud.getTempFileURL({
+ fileList: [imagePath],
+ success: (res) => {
+ const fileInfo = res.fileList && res.fileList[0];
+ if (fileInfo && fileInfo.tempFileURL) {
+ this.cloudUrlCache[imagePath] = fileInfo.tempFileURL;
+ onSuccess(fileInfo.tempFileURL);
+ } else {
+ onFail && onFail(new Error('未获取到临时URL'));
+ }
+ },
+ fail: (err) => {
+ onFail && onFail(err);
+ }
+ });
+ },
+
+ // 读取图片像素数据
+ readImagePixels: function(imagePath, width, height, callback) {
+ // 在小程序中,读取像素数据需要使用canvas
+ // 由于小程序限制,我们使用一个简化的方法
+ // 尝试使用离屏canvas(如果支持)
+
+ try {
+ // 检查是否支持createOffscreenCanvas(微信小程序基础库2.7.0+)
+ if (wx.createOffscreenCanvas && typeof wx.createOffscreenCanvas === 'function') {
+ const canvasWidth = Math.min(width, 200); // 限制大小以提高性能
+ const canvasHeight = Math.min(height, 200);
+
+ const offscreenCanvas = wx.createOffscreenCanvas({
+ type: '2d',
+ width: canvasWidth,
+ height: canvasHeight
+ });
+
+ const ctx = offscreenCanvas.getContext('2d');
+ const img = offscreenCanvas.createImage();
+
+ img.onload = () => {
+ try {
+ // 绘制图片到canvas
+ ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
+
+ // 读取像素数据
+ const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
+ callback(imageData);
+ } catch (e) {
+ console.warn('读取像素数据失败:', e);
+ callback(null);
+ }
+ };
+
+ img.onerror = () => {
+ console.warn('加载图片到canvas失败');
+ callback(null);
+ };
+
+ img.src = imagePath;
+ } else {
+ // 不支持offscreen canvas,返回null(将降级到基于路径的方法)
+ console.warn('当前环境不支持createOffscreenCanvas,将使用基于路径的特征提取');
+ callback(null);
+ }
+ } catch (e) {
+ console.warn('创建canvas失败:', e);
+ callback(null);
+ }
+ },
+
+ // 增强的图像特征提取(类似淘宝以图搜物)
+ extractEnhancedImageFeatures: function(imagePath, callback) {
+ console.log('使用增强特征提取(类似淘宝以图搜物):', imagePath);
+
+ // 检查缓存
+ if (!this.globalData.itemFeaturesCache) {
+ this.globalData.itemFeaturesCache = {};
+ }
+ const cacheKey = this.generateCacheKey(imagePath);
+ if (this.globalData.itemFeaturesCache[cacheKey]) {
+ console.log('使用缓存的增强特征');
+ const cachedFeatures = this.globalData.itemFeaturesCache[cacheKey];
+ if (cachedFeatures && Array.isArray(cachedFeatures) && cachedFeatures.length > 0) {
+ callback(cachedFeatures);
+ return;
+ }
+ }
+
+ // 获取图片信息
+ wx.getImageInfo({
+ src: imagePath,
+ success: (res) => {
+ const width = res.width;
+ const height = res.height;
+
+ console.log(`图片信息获取成功: ${width}x${height}`);
+
+ // 读取像素数据
+ this.readImagePixels(imagePath, width, height, (pixelData) => {
+ if (pixelData && pixelData.data) {
+ // 使用增强特征提取
+ try {
+ const features = this.generateEnhancedFeatures(pixelData, width, height);
+ if (features && Array.isArray(features) && features.length > 0) {
+ console.log(`增强特征提取成功,维度: ${features.length}`);
+ this.globalData.itemFeaturesCache[cacheKey] = features;
+ callback(features);
+ } else {
+ console.warn('增强特征提取返回空特征,降级到基础特征提取');
+ this.extractImageContentFeatures(imagePath, callback);
+ }
+ } catch (err) {
+ console.error('增强特征提取出错:', err);
+ this.extractImageContentFeatures(imagePath, callback);
+ }
+ } else {
+ // 降级到基础特征提取
+ console.warn('无法读取像素数据,降级到基础特征提取');
+ this.extractImageContentFeatures(imagePath, callback);
+ }
+ });
+ },
+ fail: (err) => {
+ console.error('获取图片信息失败:', err);
+ // 降级到基础特征提取
+ this.extractImageContentFeatures(imagePath, callback);
+ }
+ });
+ },
+
+ // 生成增强特征向量(类似淘宝以图搜物)
+ generateEnhancedFeatures: function(imageData, width, height) {
+ if (!imageData || !imageData.data) {
+ return this.generateContentBasedFeatures(imageData, width, height);
+ }
+
+ const pixels = imageData.data;
+ const features = [];
+
+ // 1. 基础颜色特征(HSV颜色空间 + RGB)
+ const hsvFeatures = this.extractHSVFeatures(pixels, width, height);
+ features.push(...hsvFeatures); // 约100维
+
+ // 2. 边缘特征(Sobel边缘检测)
+ const edgeFeatures = this.extractEdgeFeatures(pixels, width, height);
+ features.push(...edgeFeatures); // 64维
+
+ // 3. 纹理特征(LBP - 局部二值模式)
+ const textureFeatures = this.extractTextureFeatures(pixels, width, height);
+ features.push(...textureFeatures); // 256维
+
+ // 4. 形状特征(轮廓和几何特征)
+ const shapeFeatures = this.extractShapeFeatures(pixels, width, height);
+ features.push(...shapeFeatures); // 32维
+
+ // 5. 多尺度特征(不同分辨率下的特征)
+ const multiScaleFeatures = this.extractMultiScaleFeatures(pixels, width, height);
+ features.push(...multiScaleFeatures); // 64维
+
+ // 6. 空间分布特征(颜色在空间中的分布)
+ const spatialFeatures = this.extractSpatialFeatures(pixels, width, height);
+ features.push(...spatialFeatures); // 48维
+
+ console.log(`生成增强特征向量,总维度: ${features.length}`);
+ return features;
+ },
+
+ // 提取HSV颜色特征
+ extractHSVFeatures: function(pixels, width, height) {
+ const features = [];
+ const hHist = new Array(36).fill(0); // 色调:0-360度,分成36个区间
+ const sHist = new Array(32).fill(0); // 饱和度:0-1,分成32个区间
+ const vHist = new Array(32).fill(0); // 明度:0-1,分成32个区间
+
+ let hSum = 0, sSum = 0, vSum = 0;
+ let pixelCount = 0;
+
+ for (let i = 0; i < pixels.length; i += 4) {
+ const r = pixels[i] / 255;
+ const g = pixels[i + 1] / 255;
+ const b = pixels[i + 2] / 255;
+ const a = pixels[i + 3];
+
+ if (a > 0) {
+ // RGB转HSV
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+ const delta = max - min;
+
+ let h = 0;
+ if (delta !== 0) {
+ if (max === r) {
+ h = 60 * (((g - b) / delta) % 6);
+ } else if (max === g) {
+ h = 60 * ((b - r) / delta + 2);
+ } else {
+ h = 60 * ((r - g) / delta + 4);
+ }
+ }
+ if (h < 0) h += 360;
+
+ const s = max === 0 ? 0 : delta / max;
+ const v = max;
+
+ // 统计直方图
+ hHist[Math.floor(h / 10)]++;
+ sHist[Math.floor(s * 31)]++;
+ vHist[Math.floor(v * 31)]++;
+
+ hSum += h;
+ sSum += s;
+ vSum += v;
+ pixelCount++;
+ }
+ }
+
+ // 归一化直方图
+ const normalize = (hist) => {
+ const sum = hist.reduce((a, b) => a + b, 0);
+ return sum > 0 ? hist.map(v => v / sum) : hist;
+ };
+
+ features.push(...normalize(hHist)); // 36维
+ features.push(...normalize(sHist)); // 32维
+ features.push(...normalize(vHist)); // 32维
+
+ // 添加平均HSV值(归一化)
+ if (pixelCount > 0) {
+ features.push((hSum / pixelCount) / 360); // 归一化到0-1
+ features.push(sSum / pixelCount);
+ features.push(vSum / pixelCount);
+ } else {
+ features.push(0, 0, 0);
+ }
+
+ return features; // 总共100维
+ },
+
+ // 提取边缘特征(简化版Sobel算子)
+ extractEdgeFeatures: function(pixels, width, height) {
+ const features = [];
+ const edgeHist = new Array(64).fill(0);
+
+ // Sobel算子
+ const sobelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
+ const sobelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
+
+ // 转换为灰度图
+ const gray = [];
+ for (let i = 0; i < pixels.length; i += 4) {
+ const r = pixels[i];
+ const g = pixels[i + 1];
+ const b = pixels[i + 2];
+ gray.push(0.299 * r + 0.587 * g + 0.114 * b);
+ }
+
+ // 计算边缘强度(简化版,只处理内部像素)
+ for (let y = 1; y < height - 1; y++) {
+ for (let x = 1; x < width - 1; x++) {
+ let gx = 0, gy = 0;
+
+ for (let i = -1; i <= 1; i++) {
+ for (let j = -1; j <= 1; j++) {
+ const idx = (y + i) * width + (x + j);
+ const pixel = gray[idx];
+ gx += pixel * sobelX[i + 1][j + 1];
+ gy += pixel * sobelY[i + 1][j + 1];
+ }
+ }
+
+ const magnitude = Math.sqrt(gx * gx + gy * gy);
+ edgeHist[Math.min(Math.floor(magnitude / 4), 63)]++;
+ }
+ }
+
+ // 归一化
+ const sum = edgeHist.reduce((a, b) => a + b, 0);
+ if (sum > 0) {
+ return edgeHist.map(v => v / sum);
+ }
+ return edgeHist;
+ },
+
+ // 提取纹理特征(简化版LBP)
+ extractTextureFeatures: function(pixels, width, height) {
+ const features = new Array(256).fill(0);
+
+ // 转换为灰度图
+ const gray = [];
+ for (let i = 0; i < pixels.length; i += 4) {
+ const r = pixels[i];
+ const g = pixels[i + 1];
+ const b = pixels[i + 2];
+ gray.push(0.299 * r + 0.587 * g + 0.114 * b);
+ }
+
+ // 计算LBP(局部二值模式)
+ for (let y = 1; y < height - 1; y++) {
+ for (let x = 1; x < width - 1; x++) {
+ const center = gray[y * width + x];
+ let lbp = 0;
+
+ // 8邻域
+ const neighbors = [
+ gray[(y - 1) * width + (x - 1)], // 左上
+ gray[(y - 1) * width + x], // 上
+ gray[(y - 1) * width + (x + 1)], // 右上
+ gray[y * width + (x + 1)], // 右
+ gray[(y + 1) * width + (x + 1)], // 右下
+ gray[(y + 1) * width + x], // 下
+ gray[(y + 1) * width + (x - 1)], // 左下
+ gray[y * width + (x - 1)] // 左
+ ];
+
+ neighbors.forEach((neighbor, idx) => {
+ if (neighbor >= center) {
+ lbp |= (1 << idx);
+ }
+ });
+
+ features[lbp]++;
+ }
+ }
+
+ // 归一化
+ const sum = features.reduce((a, b) => a + b, 0);
+ if (sum > 0) {
+ return features.map(v => v / sum);
+ }
+ return features;
+ },
+
+ // 提取形状特征
+ extractShapeFeatures: function(pixels, width, height) {
+ const features = [];
+
+ // 转换为灰度图
+ const gray = [];
+ for (let i = 0; i < pixels.length; i += 4) {
+ const r = pixels[i];
+ const g = pixels[i + 1];
+ const b = pixels[i + 2];
+ gray.push(0.299 * r + 0.587 * g + 0.114 * b);
+ }
+
+ // 计算阈值(简化版,使用平均亮度)
+ const avgBrightness = gray.reduce((a, b) => a + b, 0) / gray.length;
+ const threshold = avgBrightness;
+
+ // 计算前景像素比例
+ let foregroundCount = 0;
+ for (let i = 0; i < gray.length; i++) {
+ if (gray[i] < threshold) {
+ foregroundCount++;
+ }
+ }
+ features.push(foregroundCount / gray.length);
+
+ // 计算宽高比
+ features.push(width / (width + height));
+ features.push(height / (width + height));
+
+ // 计算紧凑度(面积/周长^2的近似)
+ let perimeter = 0;
+ for (let y = 1; y < height - 1; y++) {
+ for (let x = 1; x < width - 1; x++) {
+ const idx = y * width + x;
+ const center = gray[idx] < threshold ? 1 : 0;
+
+ // 检查4邻域
+ const neighbors = [
+ gray[(y - 1) * width + x] < threshold ? 1 : 0,
+ gray[y * width + (x + 1)] < threshold ? 1 : 0,
+ gray[(y + 1) * width + x] < threshold ? 1 : 0,
+ gray[y * width + (x - 1)] < threshold ? 1 : 0
+ ];
+
+ // 如果中心是前景且邻域有背景,则是边界
+ if (center === 1 && neighbors.some(n => n === 0)) {
+ perimeter++;
+ }
+ }
+ }
+
+ const compactness = perimeter > 0 ? foregroundCount / (perimeter * perimeter) : 0;
+ features.push(Math.min(compactness, 1));
+
+ // 填充到32维(使用统计特征)
+ while (features.length < 32) {
+ features.push(0);
+ }
+
+ return features.slice(0, 32);
+ },
+
+ // 提取多尺度特征
+ extractMultiScaleFeatures: function(pixels, width, height) {
+ const features = [];
+
+ // 计算不同区域的平均颜色
+ const regions = [
+ { x: 0, y: 0, w: width / 2, h: height / 2 }, // 左上
+ { x: width / 2, y: 0, w: width / 2, h: height / 2 }, // 右上
+ { x: 0, y: height / 2, w: width / 2, h: height / 2 }, // 左下
+ { x: width / 2, y: height / 2, w: width / 2, h: height / 2 } // 右下
+ ];
+
+ regions.forEach(region => {
+ let rSum = 0, gSum = 0, bSum = 0, count = 0;
+
+ for (let y = Math.floor(region.y); y < Math.floor(region.y + region.h) && y < height; y++) {
+ for (let x = Math.floor(region.x); x < Math.floor(region.x + region.w) && x < width; x++) {
+ const idx = (y * width + x) * 4;
+ rSum += pixels[idx];
+ gSum += pixels[idx + 1];
+ bSum += pixels[idx + 2];
+ count++;
+ }
+ }
+
+ if (count > 0) {
+ features.push(rSum / (count * 255));
+ features.push(gSum / (count * 255));
+ features.push(bSum / (count * 255));
+ } else {
+ features.push(0, 0, 0);
+ }
+ });
+
+ // 填充到64维
+ while (features.length < 64) {
+ features.push(0);
+ }
+
+ return features.slice(0, 64);
+ },
+
+ // 提取空间分布特征
+ extractSpatialFeatures: function(pixels, width, height) {
+ const features = [];
+
+ // 计算颜色在空间中的分布(将图片分成3x3网格)
+ const gridSize = 3;
+ const cellWidth = width / gridSize;
+ const cellHeight = height / gridSize;
+
+ for (let gy = 0; gy < gridSize; gy++) {
+ for (let gx = 0; gx < gridSize; gx++) {
+ let rSum = 0, gSum = 0, bSum = 0, count = 0;
+
+ const startX = Math.floor(gx * cellWidth);
+ const endX = Math.floor((gx + 1) * cellWidth);
+ const startY = Math.floor(gy * cellHeight);
+ const endY = Math.floor((gy + 1) * cellHeight);
+
+ for (let y = startY; y < endY && y < height; y++) {
+ for (let x = startX; x < endX && x < width; x++) {
+ const idx = (y * width + x) * 4;
+ rSum += pixels[idx];
+ gSum += pixels[idx + 1];
+ bSum += pixels[idx + 2];
+ count++;
+ }
+ }
+
+ if (count > 0) {
+ features.push(rSum / (count * 255));
+ features.push(gSum / (count * 255));
+ features.push(bSum / (count * 255));
+ } else {
+ features.push(0, 0, 0);
+ }
+ }
+ }
+
+ return features; // 3x3x3 = 27维,填充到48维
+ },
+
+ // 基于像素数据生成特征向量(基础版本)
+ generateContentBasedFeatures: function(imageData, width, height) {
+ if (!imageData || !imageData.data) {
+ return [];
+ }
+
+ const pixels = imageData.data;
+ const pixelCount = width * height;
+ const features = [];
+
+ // 1. 颜色直方图特征(RGB各64个bin)
+ const rHist = new Array(64).fill(0);
+ const gHist = new Array(64).fill(0);
+ const bHist = new Array(64).fill(0);
+
+ // 2. 平均颜色
+ let rSum = 0, gSum = 0, bSum = 0;
+
+ // 3. 亮度分布
+ const brightnessHist = new Array(32).fill(0);
+
+ // 处理像素数据
+ for (let i = 0; i < pixels.length; i += 4) {
+ const r = pixels[i];
+ const g = pixels[i + 1];
+ const b = pixels[i + 2];
+ const a = pixels[i + 3];
+
+ if (a > 0) { // 只处理不透明像素
+ // 颜色直方图
+ rHist[Math.floor(r / 4)]++;
+ gHist[Math.floor(g / 4)]++;
+ bHist[Math.floor(b / 4)]++;
+
+ // 累计颜色
+ rSum += r;
+ gSum += g;
+ bSum += b;
+
+ // 亮度(使用YUV公式)
+ const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
+ brightnessHist[Math.floor(brightness / 8)]++;
+ }
+ }
+
+ // 归一化直方图
+ const normalizeHist = (hist) => {
+ const sum = hist.reduce((a, b) => a + b, 0);
+ return sum > 0 ? hist.map(v => v / sum) : hist;
+ };
+
+ // 添加特征
+ features.push(...normalizeHist(rHist)); // 64维
+ features.push(...normalizeHist(gHist)); // 64维
+ features.push(...normalizeHist(bHist)); // 64维
+ features.push(...normalizeHist(brightnessHist)); // 32维
+
+ // 平均颜色(归一化到0-1)
+ features.push(rSum / (pixelCount * 255)); // 1维
+ features.push(gSum / (pixelCount * 255)); // 1维
+ features.push(bSum / (pixelCount * 255)); // 1维
+
+ // 宽高比和尺寸特征
+ features.push(width / (width + height)); // 1维
+ features.push(height / (width + height)); // 1维
+ features.push((width * height) / 1000000); // 1维(归一化的面积)
+
+ // 总维度:64+64+64+32+3+3 = 230维
+
+ console.log(`生成基于内容的特征向量,维度: ${features.length}`);
+ return features;
+ },
+
+ // 生成模拟的AI图像特征(改进版:使用图片内容哈希)
+ generateMockAIFeatures: function(imagePath) {
+ console.log('使用模拟AI特征计算:', imagePath);
+
+ // 生成特征向量,模拟真实AI模型的输出
+ const features = [];
+ const dimensions = 128; // 增加维度以提高准确性
+
+ // 使用基于路径和内容的确定性方法生成特征
+ // 关键改进:对于临时文件,使用完整路径来确保不同文件有不同的哈希值
+ let hashBase = 0;
+
+ // 保存原始路径用于完整哈希计算
+ const originalPath = imagePath;
+ let normalizedPath = imagePath;
+
+ // 对于临时文件路径(wxfile://),使用完整路径来生成哈希,确保每个临时文件都有唯一特征
+ // 对于其他路径,仍然进行规范化处理
+ if (normalizedPath.startsWith('wxfile://')) {
+ // 临时文件:使用完整路径,确保不同临时文件有不同的哈希
+ normalizedPath = originalPath; // 使用完整路径
+ } else if (normalizedPath.startsWith('cloud://')) {
+ // 云存储路径:提取文件路径部分
+ const parts = normalizedPath.split('/');
+ if (parts.length > 2) {
+ normalizedPath = parts.slice(2).join('/'); // 提取文件路径部分
+ }
+ }
+
+ // 使用改进的哈希算法,考虑字符位置来增强区分度
+ // 对完整路径进行多轮哈希计算,确保不同路径产生不同的哈希值
+ for (let i = 0; i < normalizedPath.length; i++) {
+ const charCode = normalizedPath.charCodeAt(i);
+ // 使用字符位置和字符编码的组合来增强区分度
+ hashBase = ((hashBase << 5) - hashBase) + charCode + (i * 17);
+ hashBase = hashBase & 0x7FFFFFFF; // 转换为32位正整数
+ }
+
+ // 对路径进行反向哈希,进一步增强区分度
+ let reverseHash = 0;
+ for (let i = normalizedPath.length - 1; i >= 0; i--) {
+ const charCode = normalizedPath.charCodeAt(i);
+ reverseHash = ((reverseHash << 5) - reverseHash) + charCode + (i * 23);
+ reverseHash = reverseHash & 0x7FFFFFFF;
+ }
+
+ // 如果路径包含文件名,也使用文件名生成额外的哈希
+ const fileName = normalizedPath.split('/').pop() || normalizedPath;
+ let fileNameHash = 0;
+ for (let i = 0; i < fileName.length; i++) {
+ const charCode = fileName.charCodeAt(i);
+ fileNameHash = ((fileNameHash << 5) - fileNameHash) + charCode + (i * 31);
+ fileNameHash = fileNameHash & 0x7FFFFFFF;
+ }
+
+ // 组合多个哈希值,使用更大的质数来减少碰撞
+ hashBase = ((hashBase * 7919) + (reverseHash * 65537) + (fileNameHash * 97)) & 0x7FFFFFFF;
+
+ // 添加时间戳和随机数,确保不同图片路径生成不同特征
+ // 注意:对于相同路径,我们希望生成相同的特征(用于缓存),所以不使用时间戳
+ // 但如果路径不同,hashBase已经不同了,所以不需要时间戳
+ // 这里保留代码但不使用时间戳,确保相同路径生成相同特征
+
+ // 根据图片类型添加不同的特征模式
+ let categoryWeight = 0;
+ if (normalizedPath.includes('lost') || normalizedPath.includes('失物')) {
+ categoryWeight = 0.8;
+ } else if (normalizedPath.includes('found') || normalizedPath.includes('招领')) {
+ categoryWeight = 0.2;
+ } else if (normalizedPath.includes('match') || normalizedPath.includes('匹配')) {
+ categoryWeight = 0.5;
+ }
+
+ // 生成特征向量 - 确保每个路径生成唯一的特征
+ // 改进:使用更分散的哈希分布,避免特征值集中在某个范围
+ for (let i = 0; i < dimensions; i++) {
+ // 使用多个哈希函数组合生成更丰富的特征
+ // 添加位置相关的因子,确保不同位置的特征不同
+ const positionFactor = (i * 17 + hashBase) % 1000;
+
+ // 使用不同的哈希算法,增加随机性
+ const hash1 = (hashBase * (i + 1) * 3 + i * 7) % 1000;
+ const hash2 = ((hashBase ^ (i * 7)) * 5 + i * 11) % 1000;
+ const hash3 = Math.floor(Math.abs(Math.sin((hashBase + i) * 0.1)) * 1000);
+ const hash4 = ((hashBase * 13 + i * 19) ^ (reverseHash * 3)) % 1000;
+
+ // 组合多个哈希值,添加位置因子和文件名哈希
+ let sum = hash1 + hash2 + hash3 + hash4 + positionFactor;
+
+ // 添加文件名哈希的影响(不同文件名会有不同影响)
+ sum += (fileNameHash % (i + 1)) * 5;
+
+ // 添加类别权重影响(只在部分维度)
+ if (i < 10) {
+ sum += categoryWeight * 100;
+ }
+
+ // 添加路径唯一性因子(确保不同路径生成不同特征)
+ // 使用更大的因子,增强区分度
+ const pathUniqueness = (hashBase % (i + 1)) * 20;
+ sum += pathUniqueness;
+
+ // 添加反向哈希的影响
+ sum += (reverseHash % (i + 1)) * 15;
+
+ // 使用sigmoid函数归一化到0-1范围
+ // 调整参数,使特征值分布更分散
+ const featureValue = 1 / (1 + Math.exp(-sum / 300)); // 减小分母,使特征值更分散
+ features.push(parseFloat(featureValue.toFixed(4)));
+ }
+
+ // 添加路径唯一性标记(在特定维度设置特殊值)
+ // 确保不同路径在这些维度有明显区别
+ const uniqueMarker1 = (hashBase % 100) / 100;
+ const uniqueMarker2 = (reverseHash % 100) / 100;
+ const uniqueMarker3 = (fileNameHash % 100) / 100;
+ features[0] = uniqueMarker1; // 第0维使用路径哈希
+ features[1] = uniqueMarker2; // 第1维使用反向哈希
+ features[2] = uniqueMarker3; // 第2维使用文件名哈希
+
+ // 验证特征向量的唯一性
+ const featureSum = features.reduce((sum, val) => sum + val, 0);
+ const featureHash = features.slice(0, 10).reduce((sum, val) => sum + val * 100, 0);
+ console.log(`基于路径的特征生成完成,路径哈希: ${hashBase}, 特征总和: ${featureSum.toFixed(3)}, 特征哈希: ${featureHash.toFixed(3)}`);
+
+ // 添加一些基于图片路径的语义特征
+ // 颜色特征(基于路径中的关键词)
+ if (normalizedPath.includes('black') || normalizedPath.includes('黑色')) {
+ features[10] = 0.9;
+ features[11] = 0.1;
+ } else if (normalizedPath.includes('red') || normalizedPath.includes('红色')) {
+ features[10] = 0.1;
+ features[11] = 0.9;
+ }
+
+ // 尺寸特征
+ if (normalizedPath.includes('small') || normalizedPath.includes('小')) {
+ features[20] = 0.2;
+ } else if (normalizedPath.includes('large') || normalizedPath.includes('大')) {
+ features[20] = 0.8;
+ }
+
+ return features;
+ },
+
+ // 判定特征向量是否疑似基于路径的降级结果
+ isMockFeatureVector: function(features) {
+ if (!Array.isArray(features) || features.length === 0) {
+ return true;
+ }
+ const sum = features.reduce((acc, val) => acc + Math.abs(val || 0), 0);
+ if (sum === 0) {
+ return true;
+ }
+ return features.length === 128 && sum < 10;
+ },
+
+ // 计算余弦相似度
+ // 计算路径相似度(用于检测两个图片路径是否相似)
+ calculatePathSimilarity: function(path1, path2) {
+ if (!path1 || !path2) return 0;
+ if (path1 === path2) return 1;
+
+ // 提取文件名
+ const fileName1 = path1.split('/').pop() || path1;
+ const fileName2 = path2.split('/').pop() || path2;
+
+ // 如果文件名相同,返回高相似度
+ if (fileName1 === fileName2) return 0.9;
+
+ // 计算文件名相似度(简单的字符匹配)
+ let matchCount = 0;
+ const minLen = Math.min(fileName1.length, fileName2.length);
+ const maxLen = Math.max(fileName1.length, fileName2.length);
+
+ for (let i = 0; i < minLen; i++) {
+ if (fileName1[i] === fileName2[i]) {
+ matchCount++;
+ }
+ }
+
+ return matchCount / maxLen;
+ },
+
+ calculateCosineSimilarity: function(vecA, vecB) {
+ // 确保向量维度一致
+ const minLength = Math.min(vecA.length, vecB.length);
+
+ if (minLength === 0) {
+ console.warn('特征向量为空,相似度设为0');
+ return 0;
+ }
+
+ // 计算向量点积(只使用相同长度的部分)
+ let dotProduct = 0;
+ for (let i = 0; i < minLength; i++) {
+ const valA = vecA[i] || 0;
+ const valB = vecB[i] || 0;
+ dotProduct += valA * valB;
+ }
+
+ // 计算向量范数(只使用相同长度的部分,确保一致性)
+ // 重要:必须使用minLength,否则维度不匹配会导致计算错误
+ let normA = 0, normB = 0;
+ for (let i = 0; i < minLength; i++) {
+ const valA = vecA[i] || 0;
+ const valB = vecB[i] || 0;
+ normA += Math.pow(valA, 2);
+ normB += Math.pow(valB, 2);
+ }
+ normA = Math.sqrt(normA);
+ normB = Math.sqrt(normB);
+
+ // 避免除以0
+ if (normA === 0 || normB === 0) {
+ console.warn('特征向量范数为0,相似度设为0', {
+ normA: normA,
+ normB: normB,
+ vecALength: vecA.length,
+ vecBLength: vecB.length,
+ minLength: minLength,
+ vecAFirst5: vecA.slice(0, 5),
+ vecBFirst5: vecB.slice(0, 5)
+ });
+ return 0;
+ }
+
+ // 计算余弦相似度
+ const similarity = dotProduct / (normA * normB);
+
+ // 检查相似度是否有效
+ if (isNaN(similarity) || !isFinite(similarity)) {
+ console.warn('相似度计算结果无效:', {
+ dotProduct: dotProduct,
+ normA: normA,
+ normB: normB,
+ similarity: similarity
+ });
+ return 0;
+ }
+
+ // 余弦相似度范围是-1到1,但由于特征向量都是非负的,实际范围是0到1
+ // 如果相似度小于0,说明向量方向相反,应该返回0
+ // 如果相似度大于1,说明计算有误,应该限制为1
+ const normalizedSimilarity = Math.max(0, Math.min(1, similarity));
+
+ // 诊断:如果相似度异常高,输出详细信息
+ if (normalizedSimilarity > 0.7) {
+ console.warn('⚠️ 高相似度诊断:', {
+ similarity: normalizedSimilarity.toFixed(4),
+ dotProduct: dotProduct.toFixed(4),
+ normA: normA.toFixed(4),
+ normB: normB.toFixed(4),
+ vecALength: vecA.length,
+ vecBLength: vecB.length,
+ vecAFirst10: vecA.slice(0, 10).map(v => v.toFixed(3)),
+ vecBFirst10: vecB.slice(0, 10).map(v => v.toFixed(3)),
+ vecASum: vecA.reduce((sum, v) => sum + Math.abs(v), 0).toFixed(3),
+ vecBSum: vecB.reduce((sum, v) => sum + Math.abs(v), 0).toFixed(3)
+ });
+
+ // 检查特征向量是否相同
+ const vecAStr = vecA.slice(0, 20).map(v => v.toFixed(3)).join(',');
+ const vecBStr = vecB.slice(0, 20).map(v => v.toFixed(3)).join(',');
+ if (vecAStr === vecBStr) {
+ console.error('❌ 错误:特征向量前20维完全相同!这会导致相似度为1.0');
+ console.error(' 这通常是因为都使用了基于路径的特征提取,且路径相同或相似');
+ return 0.5; // 如果特征向量相同,返回中等相似度而不是1.0
+ }
+
+ // 检查特征向量是否过于相似(前20维差异很小)
+ let diffCount = 0;
+ for (let i = 0; i < Math.min(20, vecA.length, vecB.length); i++) {
+ if (Math.abs(vecA[i] - vecB[i]) > 0.01) {
+ diffCount++;
+ }
+ }
+ if (diffCount < 5) {
+ console.warn(`⚠️ 警告:特征向量前20维中只有${diffCount}个维度有明显差异(>0.01)`);
+ console.warn(' 这可能导致相似度异常高');
+ }
+ }
+
+ return parseFloat(normalizedSimilarity.toFixed(3));
+ },
+
+ // 生成缓存键
+ generateCacheKey: function(imagePath) {
+ // 使用图片路径的哈希作为缓存键
+ // 简单的字符串哈希函数
+ let hash = 0;
+ for (let i = 0; i < imagePath.length; i++) {
+ const char = imagePath.charCodeAt(i);
+ hash = ((hash << 5) - hash) + char;
+ hash = hash & hash; // 转换为32位整数
+ }
+ const cacheKey = `similarity_${hash}`;
+ console.log(`生成缓存键: ${imagePath} -> ${cacheKey}`);
+ return cacheKey;
+ },
+
+ // 发布失物信息
+ publishLostItem: function(data, callback) {
+ console.log('开始发布失物信息');
+
+ // 检查环境是否已验证为无效
+ if (this.globalData.cloudEnvValidated === false && this.globalData.db === null) {
+ console.log('云开发环境已确认为无效,直接使用本地发布');
+ this.publishItemLocally('lost', data, callback);
+ return;
+ }
+
+ try {
+ const db = this.globalData.db;
+ const useCloudDB = db && wx.cloud;
+
+ // 处理图片路径,确保所有用户都能访问
+ const processedData = this.processItemImages(data);
+
+ if (useCloudDB) {
+ console.log('使用云数据库发布');
+ // 创建物品对象 - 移除_openid,由云开发自动生成
+ const item = {
+ type: 'lost',
+ ...processedData,
+ // 注意:_openid 由云开发自动生成,不能手动设置
+ publishTime: db.serverDate ? db.serverDate() : new Date(), // 降级处理
+ createTime: new Date().toISOString()
+ };
+
+ // 尝试插入到云数据库
+ db.collection('items').add({
+ data: item,
+ success: res => {
+ console.log('失物信息发布成功,记录ID:', res._id);
+ // 标记环境为有效
+ this.globalData.cloudEnvValidated = true;
+
+ // 将物品添加到用户自己发布的列表(内存中)
+ const userItem = {
+ ...item,
+ id: res._id,
+ isUserPublished: true,
+ imageFeatures: null
+ };
+ this.globalData.userPublishedItems.push(userItem);
+
+ // 添加到全局所有物品列表,供其他用户查看
+ const globalItem = {
+ ...userItem,
+ isUserPublished: false
+ };
+ this.globalData.allPublishedItems.push(globalItem);
+
+ // 异步提取真实的图片内容特征
+ const firstImage = processedData.images && processedData.images.length > 0
+ ? processedData.images[0]
+ : '/images/lost.png';
+ this.generateAndPersistFeaturesForItem(res._id, firstImage, (features) => {
+ userItem.imageFeatures = features;
+ globalItem.imageFeatures = features;
+
+ if (!this.globalData.settings) {
+ this.globalData.settings = { enableSmartMatch: true };
+ }
+ if (this.globalData.settings.enableSmartMatch !== false) {
+ console.log('启动智能匹配查找,用户物品:', userItem.title);
+ this.findMatchesForNewItem(userItem);
+ } else {
+ console.log('智能匹配功能已关闭,跳过匹配查找');
+ }
+ });
+
+ // 创建发布成功通知
+ this.createSystemMessage(
+ 'system',
+ '发布成功',
+ `您发布的失物信息"${processedData.title || '未命名物品'}"已成功发布,系统将为您自动匹配相关招领信息。`,
+ res._id,
+ 'lost'
+ );
+
+ callback({
+ code: 200,
+ message: '发布成功',
+ success: true,
+ data: {
+ id: res._id,
+ status: 'success',
+ publishTime: new Date().toISOString()
+ }
+ });
+ },
+ fail: err => {
+ // 检查是否是环境不存在的错误
+ const isEnvError = err.errCode === -501000 || (err.errMsg && err.errMsg.includes('env not exists'));
+
+ if (isEnvError) {
+ console.warn('云开发环境不存在,自动切换到本地模拟发布模式');
+ // 清除数据库引用,确保后续不再尝试使用云数据库
+ this.globalData.db = null;
+ this.globalData.cloudEnvValidated = false;
+ } else {
+ console.error('云数据库发布失败:', err.errMsg || err);
+ }
+
+ // 降级到本地模拟发布
+ this.publishItemLocally('lost', processedData, callback);
+ }
+ });
+ } else {
+ // 降级到本地模拟发布
+ this.publishItemLocally('lost', processedData, callback);
+ }
+ } catch (error) {
+ console.error('发布过程出错:', error);
+ // 清除数据库引用,避免后续再次尝试
+ this.globalData.db = null;
+ this.globalData.cloudEnvValidated = false;
+ // 降级到本地模拟发布
+ this.publishItemLocally('lost', data, callback);
+ }
+ },
+
+ // 发布招领信息
+ publishFoundItem: function(data, callback) {
+ console.log('开始发布招领信息');
+
+ // 检查环境是否已验证为无效
+ if (this.globalData.cloudEnvValidated === false && this.globalData.db === null) {
+ console.log('云开发环境已确认为无效,直接使用本地发布');
+ this.publishItemLocally('found', data, callback);
+ return;
+ }
+
+ try {
+ const db = this.globalData.db;
+ const useCloudDB = db && wx.cloud;
+
+ // 处理图片路径,确保所有用户都能访问
+ const processedData = this.processItemImages(data);
+
+ if (useCloudDB) {
+ console.log('使用云数据库发布');
+ // 创建物品对象 - 移除_openid,由云开发自动生成
+ const item = {
+ type: 'found',
+ ...processedData,
+ // 注意:_openid 由云开发自动生成,不能手动设置
+ publishTime: db.serverDate ? db.serverDate() : new Date(), // 降级处理
+ createTime: new Date().toISOString()
+ };
+
+ // 尝试插入到云数据库
+ db.collection('items').add({
+ data: item,
+ success: res => {
+ console.log('招领信息发布成功,记录ID:', res._id);
+ // 标记环境为有效
+ this.globalData.cloudEnvValidated = true;
+
+ // 将物品添加到用户自己发布的列表(内存中)
+ const userItem = {
+ ...item,
+ id: res._id,
+ isUserPublished: true,
+ imageFeatures: null
+ };
+ this.globalData.userPublishedItems.push(userItem);
+
+ // 添加到全局所有物品列表,供其他用户查看
+ const globalItem = {
+ ...userItem,
+ isUserPublished: false
+ };
+ this.globalData.allPublishedItems.push(globalItem);
+
+ // 异步提取真实的图片内容特征
+ const firstImage = processedData.images && processedData.images.length > 0
+ ? processedData.images[0]
+ : '/images/found.png';
+ this.generateAndPersistFeaturesForItem(res._id, firstImage, (features) => {
+ userItem.imageFeatures = features;
+ globalItem.imageFeatures = features;
+
+ if (!this.globalData.settings) {
+ this.globalData.settings = { enableSmartMatch: true };
+ }
+ if (this.globalData.settings.enableSmartMatch !== false) {
+ console.log('启动智能匹配查找,用户物品:', userItem.title);
+ this.findMatchesForNewItem(userItem);
+ } else {
+ console.log('智能匹配功能已关闭,跳过匹配查找');
+ }
+ });
+
+ // 创建发布成功通知
+ this.createSystemMessage(
+ 'system',
+ '发布成功',
+ `您发布的招领信息"${processedData.title || '未命名物品'}"已成功发布,系统正在为您智能匹配相关失物信息。`,
+ res._id,
+ 'found'
+ );
+
+ callback({
+ code: 200,
+ message: '发布成功',
+ success: true,
+ data: {
+ id: res._id,
+ status: 'success',
+ publishTime: new Date().toISOString()
+ }
+ });
+ },
+ fail: err => {
+ // 检查是否是环境不存在的错误
+ const isEnvError = err.errCode === -501000 || (err.errMsg && err.errMsg.includes('env not exists'));
+
+ if (isEnvError) {
+ console.warn('云开发环境不存在,自动切换到本地模拟发布模式');
+ // 清除数据库引用,确保后续不再尝试使用云数据库
+ this.globalData.db = null;
+ this.globalData.cloudEnvValidated = false;
+ } else {
+ console.error('云数据库发布失败:', err.errMsg || err);
+ }
+
+ // 降级到本地模拟发布
+ this.publishItemLocally('found', processedData, callback);
+ }
+ });
+ } else {
+ // 降级到本地模拟发布
+ this.publishItemLocally('found', processedData, callback);
+ }
+ } catch (error) {
+ console.error('发布过程出错:', error);
+ // 清除数据库引用,避免后续再次尝试
+ this.globalData.db = null;
+ this.globalData.cloudEnvValidated = false;
+ // 降级到本地模拟发布
+ this.publishItemLocally('found', data, callback);
+ }
+ },
+
+ // 本地模拟发布物品(降级方案)
+ publishItemLocally: function(type, data, callback) {
+ console.log('========== 使用本地模拟方式发布物品 ==========');
+ console.log('物品类型:', type);
+ console.log('发布数据:', JSON.stringify(data, null, 2));
+
+ // 初始化必要的数据结构
+ if (!this.globalData.userPublishedItems) {
+ this.globalData.userPublishedItems = [];
+ }
+ if (!this.globalData.allPublishedItems) {
+ this.globalData.allPublishedItems = [];
+ }
+
+ // 模拟成功发布
+ setTimeout(() => {
+ // 创建模拟ID
+ const mockId = type + '_' + Date.now() + '_' + Math.floor(Math.random() * 1000);
+
+ // 处理图片路径,确保所有用户都能访问
+ const processedData = this.processItemImages(data);
+
+ // 创建模拟物品对象
+ const mockItem = {
+ type: type,
+ ...processedData,
+ id: mockId,
+ _openid: 'local_user',
+ publishTime: new Date(),
+ createTime: new Date().toISOString(),
+ isUserPublished: true,
+ imageFeatures: null
+ };
+
+ console.log('创建模拟物品:', mockItem.title);
+ console.log('物品ID:', mockId);
+ console.log('物品类型:', type);
+ console.log('图片数量:', mockItem.images ? mockItem.images.length : 0);
+ console.log('图片特征向量维度:', mockItem.imageFeatures ? mockItem.imageFeatures.length : 0);
+
+ // 添加到用户发布列表
+ this.globalData.userPublishedItems.push(mockItem);
+ console.log('✅ 已添加到用户发布列表,当前数量:', this.globalData.userPublishedItems.length);
+
+ // 添加到全局所有物品列表,供其他用户查看
+ // 创建深拷贝,避免引用问题
+ const globalItem = JSON.parse(JSON.stringify({
+ ...mockItem,
+ isUserPublished: false
+ }));
+
+ // 确保ID一致
+ globalItem.id = mockId;
+
+ this.globalData.allPublishedItems.push(globalItem);
+ console.log('✅ 已添加到全局物品列表,当前数量:', this.globalData.allPublishedItems.length);
+ console.log('全局物品列表内容预览:');
+ this.globalData.allPublishedItems.forEach((item, idx) => {
+ console.log(` [${idx}] ID: ${item.id}, 类型: ${item.type}, 标题: ${item.title}, isUserPublished: ${item.isUserPublished}, 有图片特征: ${!!item.imageFeatures}`);
+ });
+
+ // 验证物品是否真的保存成功
+ const savedItem = this.globalData.allPublishedItems.find(item => item.id === mockId);
+ if (savedItem) {
+ console.log('✅ 验证成功:物品已保存到全局列表');
+ console.log('保存的物品详情:', JSON.stringify(savedItem, null, 2));
+ } else {
+ console.error('❌ 验证失败:物品未保存到全局列表!');
+ }
+
+ // 为多用户测试准备:添加一些模拟的其他用户发布的物品
+ if (this.globalData.isDebug && this.globalData.allPublishedItems.length < 5) {
+ this.addMockItemsFromOtherUsers();
+ }
+
+ // 创建发布成功通知
+ const typeText = type === 'lost' ? '失物' : '招领';
+ this.createSystemMessage(
+ 'system',
+ '发布成功',
+ `您发布的${typeText}信息"${processedData.title || '未命名物品'}"已成功发布,系统正在为您智能匹配相关信息。`,
+ mockId,
+ type
+ );
+
+ console.log('========== 本地模拟发布成功 ==========');
+ console.log('模拟ID:', mockId);
+
+ // 返回成功结果
+ callback({
+ code: 200,
+ message: '发布成功',
+ success: true,
+ data: {
+ id: mockId,
+ status: 'success',
+ publishTime: new Date().toISOString()
+ }
+ });
+
+ const firstImage = processedData.images && processedData.images.length > 0
+ ? processedData.images[0]
+ : (type === 'lost' ? '/images/lost.png' : '/images/found.png');
+ this.generateAndPersistFeaturesForItem(mockId, firstImage, (features) => {
+ mockItem.imageFeatures = features;
+ globalItem.imageFeatures = features;
+
+ if (!this.globalData.settings) {
+ this.globalData.settings = { enableSmartMatch: true };
+ }
+ if (this.globalData.settings.enableSmartMatch !== false) {
+ console.log('启动智能匹配查找(本地模式),用户物品:', mockItem.title);
+ this.findMatchesForNewItem(mockItem);
+ } else {
+ console.log('智能匹配功能已关闭,跳过匹配查找');
+ }
+ });
+ }, 500);
+ },
+
+ // 添加模拟的其他用户发布的物品(用于测试多用户场景)
+ addMockItemsFromOtherUsers: function() {
+ console.log('添加模拟的其他用户发布的物品');
+
+ const mockItems = [
+ {
+ id: 'found_mock_1',
+ type: 'found',
+ title: '捡到钱包一个',
+ description: '在图书馆三楼捡到黑色钱包一个,内含身份证、银行卡等物品,请失主联系。',
+ location: '图书馆三楼',
+ time: new Date().toISOString().split('T')[0],
+ contactName: '李同学',
+ contactPhone: '13900139000',
+ // 注意:移除_openid字段,云数据库会自动生成
+ publishTime: new Date(Date.now() - 3600000), // 1小时前
+ createTime: new Date(Date.now() - 3600000).toISOString(),
+ images: ['/images/found.png'],
+ isUserPublished: false
+ },
+ {
+ id: 'lost_mock_2',
+ type: 'lost',
+ title: '丢失校园卡',
+ description: '在食堂丢失校园卡一张,卡号末尾四位是1234,请捡到的同学联系我。',
+ location: '第一食堂',
+ time: new Date().toISOString().split('T')[0],
+ contactName: '王同学',
+ contactPhone: '13700137000',
+ // 注意:移除_openid字段,云数据库会自动生成
+ publishTime: new Date(Date.now() - 7200000), // 2小时前
+ createTime: new Date(Date.now() - 7200000).toISOString(),
+ images: ['/images/lost.png'],
+ isUserPublished: false
+ },
+ {
+ id: 'found_mock_3',
+ type: 'found',
+ title: '捡到雨伞一把',
+ description: '在教学楼B栋门口捡到蓝色雨伞一把,天气不好,失主请尽快联系。',
+ location: '教学楼B栋',
+ time: new Date().toISOString().split('T')[0],
+ contactName: '张同学',
+ contactPhone: '13800138000',
+ // 注意:移除_openid字段,云数据库会自动生成
+ publishTime: new Date(Date.now() - 10800000), // 3小时前
+ createTime: new Date(Date.now() - 10800000).toISOString(),
+ images: ['/images/found.png'],
+ isUserPublished: false
+ },
+ {
+ id: 'lost_mock_4',
+ type: 'lost',
+ title: '丢失笔记本电脑',
+ description: '在自习室不小心遗失联想笔记本电脑一台,有重要资料,必有重谢!',
+ location: '第二自习室',
+ time: new Date().toISOString().split('T')[0],
+ contactName: '陈同学',
+ contactPhone: '13600136000',
+ // 注意:移除_openid字段,云数据库会自动生成
+ publishTime: new Date(Date.now() - 14400000), // 4小时前
+ createTime: new Date(Date.now() - 14400000).toISOString(),
+ images: ['/images/lost.png'],
+ isUserPublished: false
+ }
+ ];
+
+ // 检查是否已存在相同ID的物品,并进行严格的有效性检查
+ mockItems.forEach(item => {
+ try {
+ if (item && item.id) {
+ const exists = this.globalData.allPublishedItems.some(existingItem =>
+ existingItem && existingItem.id === item.id
+ );
+ if (!exists) {
+ // 处理图片路径确保其他用户能访问
+ const processedItem = this.processItemImages(item);
+ if (processedItem) {
+ this.globalData.allPublishedItems.push(processedItem);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('添加模拟物品时出错:', error);
+ }
+ });
+ },
+
+ // 从云数据库获取物品列表(带降级逻辑)
+ getItemsFromCloud: function(type, pageNum, pageSize, callback) {
+ console.log('获取物品列表,类型:', type, '页码:', pageNum);
+
+ // 检查环境是否已验证为无效
+ if (this.globalData.cloudEnvValidated === false && this.globalData.db === null) {
+ console.log('云开发环境已确认为无效,直接使用本地数据');
+ this.getLocalItems(type, pageNum, pageSize, callback);
+ return;
+ }
+
+ try {
+ const db = this.globalData.db;
+ const useCloudDB = db && wx.cloud;
+
+ if (useCloudDB) {
+ console.log('尝试从云数据库获取');
+ // 构建查询条件
+ const query = db.collection('items');
+ const condition = type && type !== 'all' ? { type: type } : {};
+
+ // 计算分页偏移量
+ const skip = (pageNum - 1) * pageSize;
+
+ // 执行查询
+ query.where(condition)
+ .orderBy('publishTime', 'desc') // 按发布时间倒序
+ .skip(skip)
+ .limit(pageSize)
+ .get({
+ success: res => {
+ console.log('云数据库查询成功,返回', res.data.length, '条记录');
+ // 标记环境为有效
+ this.globalData.cloudEnvValidated = true;
+
+ // 处理返回的数据,添加id字段和是否为当前用户发布的标记
+ const openid = wx.getStorageSync('openid') || 'anonymous';
+ const items = res.data.map(item => ({
+ id: item._id,
+ ...item,
+ isUserPublished: item._openid === openid
+ }));
+
+ callback({
+ items: items,
+ hasMore: items.length === pageSize // 如果返回的数量等于页面大小,可能还有更多
+ });
+ },
+ fail: err => {
+ // 检查是否是环境不存在的错误
+ const isEnvError = err.errCode === -501000 || (err.errMsg && err.errMsg.includes('env not exists'));
+
+ if (isEnvError) {
+ console.warn('云开发环境不存在,自动切换到本地模拟数据模式');
+ // 清除数据库引用,确保后续不再尝试使用云数据库
+ this.globalData.db = null;
+ this.globalData.cloudEnvValidated = false;
+ } else {
+ console.error('云数据库查询失败:', err.errMsg || err);
+ }
+
+ // 降级到本地数据
+ this.getLocalItems(type, pageNum, pageSize, callback);
+ }
+ });
+ } else {
+ // 降级到本地数据
+ this.getLocalItems(type, pageNum, pageSize, callback);
+ }
+ } catch (error) {
+ console.error('获取物品列表过程出错:', error);
+ // 清除数据库引用,避免后续再次尝试
+ this.globalData.db = null;
+ this.globalData.cloudEnvValidated = false;
+ // 降级到本地数据
+ this.getLocalItems(type, pageNum, pageSize, callback);
+ }
+ },
+
+ // 从云数据库搜索物品(带降级逻辑)
+ searchItemsFromCloud: function(keyword, type, callback) {
+ console.log('搜索物品,关键词:', keyword, '类型:', type);
+
+ // 检查环境是否已验证为无效
+ if (this.globalData.cloudEnvValidated === false && this.globalData.db === null) {
+ console.log('云开发环境已确认为无效,直接使用本地搜索');
+ this.searchLocalItems(keyword, type, callback);
+ return;
+ }
+
+ try {
+ const db = this.globalData.db;
+ const useCloudDB = db && wx.cloud;
+
+ if (useCloudDB) {
+ console.log('尝试从云数据库搜索');
+ // 构建查询条件
+ const _ = db.command;
+ let condition = {};
+
+ // 添加类型筛选条件
+ if (type && type !== 'all') {
+ condition.type = type;
+ }
+
+ // 添加关键词搜索条件(使用正则表达式)
+ const reg = db.RegExp({
+ regexp: keyword,
+ options: 'i', // 不区分大小写
+ });
+
+ condition = _.or([
+ { title: reg },
+ { description: reg }
+ ]);
+
+ // 如果指定了类型,需要与关键词条件合并
+ if (type && type !== 'all') {
+ condition = _.and([
+ { type: type },
+ condition
+ ]);
+ }
+
+ // 执行查询
+ db.collection('items')
+ .where(condition)
+ .orderBy('publishTime', 'desc') // 按发布时间倒序
+ .limit(50) // 限制返回数量
+ .get({
+ success: res => {
+ console.log('云数据库搜索成功,返回', res.data.length, '条记录');
+ // 标记环境为有效
+ this.globalData.cloudEnvValidated = true;
+
+ // 处理返回的数据,添加id字段和是否为当前用户发布的标记
+ const openid = wx.getStorageSync('openid') || 'anonymous';
+ const items = res.data.map(item => ({
+ id: item._id,
+ ...item,
+ isUserPublished: item._openid === openid
+ }));
+
+ callback(items);
+ },
+ fail: err => {
+ // 检查是否是环境不存在的错误
+ const isEnvError = err.errCode === -501000 || (err.errMsg && err.errMsg.includes('env not exists'));
+
+ if (isEnvError) {
+ console.warn('云开发环境不存在,自动切换到本地搜索模式');
+ // 清除数据库引用,确保后续不再尝试使用云数据库
+ this.globalData.db = null;
+ this.globalData.cloudEnvValidated = false;
+ } else {
+ console.error('云数据库搜索失败:', err.errMsg || err);
+ }
+
+ // 降级到本地搜索
+ this.searchLocalItems(keyword, type, callback);
+ }
+ });
+ } else {
+ // 降级到本地搜索
+ this.searchLocalItems(keyword, type, callback);
+ }
+ } catch (error) {
+ console.error('搜索过程出错:', error);
+ // 清除数据库引用,避免后续再次尝试
+ this.globalData.db = null;
+ this.globalData.cloudEnvValidated = false;
+ // 降级到本地搜索
+ this.searchLocalItems(keyword, type, callback);
+ }
+ },
+
+ // 获取本地物品数据(降级方案)
+ getLocalItems: function(type, pageNum, pageSize, callback) {
+ console.log('使用本地模拟数据获取物品列表', type, pageNum);
+
+ // 初始化空数组,避免undefined错误
+ const userItems = this.globalData.userPublishedItems || [];
+ const allItems = this.globalData.allPublishedItems || [];
+
+ console.log('用户发布物品数量:', userItems.length);
+ console.log('全局物品数量:', allItems.length);
+
+ // 合并当前用户和模拟的其他用户发布的物品
+ let items = [...userItems, ...allItems];
+
+ // 去重,避免重复显示
+ const uniqueItems = [];
+ const itemIds = new Set();
+ items.forEach(item => {
+ if (item && item.id && !itemIds.has(item.id)) {
+ itemIds.add(item.id);
+ // 处理每个物品的图片路径
+ const processedItem = this.processItemImages(item);
+ uniqueItems.push(processedItem);
+ }
+ });
+
+ console.log('去重后物品数量:', uniqueItems.length);
+
+ // 添加一些模拟数据(如果需要)
+ if (uniqueItems.length < 5) {
+ const mockItems = this.generateMockItems(type);
+ mockItems.forEach(item => {
+ if (item && item.id && !itemIds.has(item.id)) {
+ itemIds.add(item.id);
+ // 处理模拟物品的图片路径
+ const processedItem = this.processItemImages(item);
+ uniqueItems.push(processedItem);
+ }
+ });
+ }
+
+ // 按类型筛选
+ if (type && type !== 'all') {
+ items = uniqueItems.filter(item => item && item.type === type);
+ } else {
+ items = uniqueItems;
+ }
+
+ console.log('筛选后物品数量:', items.length);
+
+ // 按发布时间倒序排序
+ items.sort((a, b) => {
+ const timeA = a.publishTime || a.createTime || 0;
+ const timeB = b.publishTime || b.createTime || 0;
+ return new Date(timeB) - new Date(timeA);
+ });
+
+ // 分页处理
+ const startIndex = (pageNum - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedItems = items.slice(startIndex, endIndex);
+
+ console.log('本地数据返回', paginatedItems.length, '条记录,是否有更多:', endIndex < items.length);
+
+ callback({
+ items: paginatedItems,
+ hasMore: endIndex < items.length
+ });
+ },
+
+ // 本地搜索物品(降级方案)
+ searchLocalItems: function(keyword, type, callback) {
+ console.log('使用本地模拟数据搜索物品', keyword, type);
+
+ // 合并当前用户和模拟的其他用户发布的物品
+ let allItems = [
+ ...(this.globalData.userPublishedItems || []),
+ ...(this.globalData.allPublishedItems || [])
+ ];
+
+ // 去重并处理图片路径
+ const uniqueItems = [];
+ const itemIds = new Set();
+ allItems.forEach(item => {
+ if (item && item.id && !itemIds.has(item.id)) {
+ itemIds.add(item.id);
+ // 处理每个物品的图片路径
+ const processedItem = this.processItemImages(item);
+ uniqueItems.push(processedItem);
+ }
+ });
+
+ // 添加一些模拟数据(如果需要)
+ if (uniqueItems.length < 5) {
+ const mockItems = this.generateMockItems(type);
+ mockItems.forEach(item => {
+ if (item && item.id && !itemIds.has(item.id)) {
+ itemIds.add(item.id);
+ // 处理模拟物品的图片路径
+ const processedItem = this.processItemImages(item);
+ uniqueItems.push(processedItem);
+ }
+ });
+ }
+
+ // 按类型筛选
+ if (type && type !== 'all') {
+ allItems = uniqueItems.filter(item => item && item.type === type);
+ } else {
+ allItems = uniqueItems;
+ }
+
+ // 按关键词搜索
+ const searchResults = keyword ? allItems.filter(item => {
+ if (!item) return false;
+
+ const searchRegex = new RegExp(keyword, 'i');
+ const titleMatch = item.title && searchRegex.test(item.title);
+ const descMatch = item.description && searchRegex.test(item.description);
+ const locationMatch = item.location && searchRegex.test(item.location);
+ return titleMatch || descMatch || locationMatch;
+ }) : allItems;
+
+ // 按发布时间倒序排序
+ searchResults.sort((a, b) => {
+ if (!a || !b) return 0;
+
+ const timeA = a.publishTime || a.createTime || 0;
+ const timeB = b.publishTime || b.createTime || 0;
+ return new Date(timeB) - new Date(timeA);
+ });
+
+ console.log('本地搜索返回', searchResults.length, '条记录');
+ callback(searchResults);
+ },
+
+ // 生成模拟物品数据(用于降级方案)
+ generateMockItems: function(type) {
+ // 确保使用绝对路径的默认图片
+ const defaultLostImage = '/images/lost.png';
+ const defaultFoundImage = '/images/found.png';
+ const defaultMatchImage = '/images/match.png';
+
+ const mockLostItems = [
+ {
+ id: 'mock_lost_1',
+ type: 'lost',
+ title: '丢失的手机',
+ description: '在图书馆丢失的黑色智能手机,有明显的保护壳磨损痕迹。',
+ location: '学校图书馆',
+ time: new Date().toISOString().split('T')[0],
+ images: [defaultLostImage],
+ contactName: '张三',
+ contactPhone: '13800138000',
+ isUserPublished: false,
+ publishTime: new Date(Date.now() - 3600000), // 1小时前
+ createTime: new Date(Date.now() - 3600000).toISOString()
+ },
+ {
+ id: 'mock_lost_2',
+ type: 'lost',
+ title: '钱包丢失',
+ description: '棕色皮质钱包,内有学生证和银行卡。',
+ location: '校园食堂',
+ time: new Date(Date.now() - 86400000).toISOString().split('T')[0],
+ images: [defaultFoundImage],
+ contactName: '李四',
+ contactPhone: '13900139000',
+ isUserPublished: false,
+ publishTime: new Date(Date.now() - 86400000), // 1天前
+ createTime: new Date(Date.now() - 86400000).toISOString()
+ }
+ ];
+
+ const mockFoundItems = [
+ {
+ id: 'mock_found_1',
+ type: 'found',
+ title: '捡到钥匙串',
+ description: '在教学楼楼梯口捡到的钥匙串,有三把钥匙。',
+ location: '教学楼',
+ time: new Date().toISOString().split('T')[0],
+ images: [defaultMatchImage],
+ contactName: '王五',
+ contactPhone: '13700137000',
+ isUserPublished: false,
+ publishTime: new Date(Date.now() - 7200000), // 2小时前
+ createTime: new Date(Date.now() - 7200000).toISOString()
+ },
+ {
+ id: 'mock_found_2',
+ type: 'found',
+ title: '捡到书包',
+ description: '在操场捡到的蓝色书包,内有课本和笔记本。',
+ location: '操场',
+ time: new Date(Date.now() - 172800000).toISOString().split('T')[0],
+ images: [defaultMatchImage],
+ contactName: '赵六',
+ contactPhone: '13600136000',
+ isUserPublished: false,
+ publishTime: new Date(Date.now() - 172800000), // 2天前
+ createTime: new Date(Date.now() - 172800000).toISOString()
+ }
+ ];
+
+ if (type === 'lost') {
+ return mockLostItems;
+ } else if (type === 'found') {
+ return mockFoundItems;
+ } else {
+ return [...mockLostItems, ...mockFoundItems];
+ }
+ },
+
+ // 智能匹配核心功能
+ getSmartMatches: function(matchType, pageNum, callback) {
+ console.log('获取智能匹配结果,类型:', matchType);
+
+ // 使用异步方式生成匹配数据,先从云数据库查询所有物品
+ // 使用 setTimeout 将计算任务放到下一个事件循环,避免阻塞主线程
+ setTimeout(() => {
+ try {
+ // 先从云数据库获取所有可比较的物品
+ this.getAllComparableItemsFromCloud((allItems) => {
+ try {
+ // 生成模拟的匹配数据(传入从云数据库获取的物品)
+ const matchedItems = this.generateMockMatches(matchType, allItems);
+
+ // 分页处理
+ const pageSize = 10;
+ const startIndex = (pageNum - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedItems = matchedItems.slice(startIndex, endIndex);
+
+ const response = {
+ code: 200,
+ message: 'success',
+ data: {
+ items: paginatedItems,
+ total: matchedItems.length
+ },
+ items: paginatedItems, // 兼容search.js的格式
+ hasMore: endIndex < matchedItems.length
+ };
+
+ callback(response);
+ } catch (error) {
+ console.error('生成匹配数据时出错:', error);
+ callback({
+ code: 500,
+ message: '匹配失败',
+ data: { items: [], total: 0 },
+ items: [],
+ hasMore: false
+ });
+ }
+ });
+ } catch (error) {
+ console.error('获取云数据库物品时出错:', error);
+ // 降级到使用内存中的物品
+ try {
+ const matchedItems = this.generateMockMatches(matchType);
+ const pageSize = 10;
+ const startIndex = (pageNum - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedItems = matchedItems.slice(startIndex, endIndex);
+
+ callback({
+ code: 200,
+ message: 'success',
+ data: {
+ items: paginatedItems,
+ total: matchedItems.length
+ },
+ items: paginatedItems,
+ hasMore: endIndex < matchedItems.length
+ });
+ } catch (err) {
+ console.error('降级匹配也失败:', err);
+ callback({
+ code: 500,
+ message: '匹配失败',
+ data: { items: [], total: 0 },
+ items: [],
+ hasMore: false
+ });
+ }
+ }
+ }, 100); // 减少延迟时间,提高响应速度
+ },
+
+ // 从云数据库获取所有可比较的物品(异步)
+ getAllComparableItemsFromCloud: function(callback) {
+ console.log('========== 从云数据库获取所有可比较物品 ==========');
+
+ // 先获取内存中的物品
+ const memoryItems = this.getAllComparableItems(true);
+ console.log('内存中的物品数量:', memoryItems.length);
+
+ // 检查云数据库是否可用
+ if (this.globalData.cloudEnvValidated === false && this.globalData.db === null) {
+ console.log('云开发环境已确认为无效,只使用内存中的物品');
+ callback(memoryItems);
+ return;
+ }
+
+ const db = this.globalData.db;
+ if (!db || !wx.cloud) {
+ console.log('云数据库不可用,只使用内存中的物品');
+ callback(memoryItems);
+ return;
+ }
+
+ console.log('从云数据库查询所有物品...');
+
+ // 从云数据库查询所有物品(不限制类型,因为我们需要匹配不同类型的物品)
+ db.collection('items')
+ .get({
+ success: (res) => {
+ console.log('✅ 云数据库查询成功,返回', res.data.length, '条记录');
+
+ // 处理返回的数据
+ const openid = wx.getStorageSync('openid') || 'local_user';
+ const cloudItems = res.data.map(item => {
+ // 处理图片路径
+ const processedItem = this.processItemImages({
+ id: item._id,
+ ...item,
+ isUserPublished: item._openid === openid
+ });
+
+ let persistedFeatures = 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) {
+ persistedFeatures = vector.features;
+ }
+ }
+
+ return {
+ id: item._id,
+ type: item.type,
+ title: item.title,
+ description: item.description || '',
+ location: item.location || '',
+ time: item.time || item.date || item.publishTime || item.createTime,
+ images: processedItem.images || [],
+ isUserPublished: item._openid === openid,
+ imageFeatures: persistedFeatures || item.imageFeatures || null
+ };
+ }).filter(item => item.images && item.images.length > 0); // 只保留有图片的物品
+
+ console.log('云数据库物品数量(有图片):', cloudItems.length);
+
+ // 合并内存中的物品和云数据库的物品,去重(基于id)
+ const itemMap = new Map();
+
+ // 先添加内存中的物品
+ memoryItems.forEach(item => {
+ itemMap.set(item.id, item);
+ });
+
+ // 再添加云数据库的物品(会覆盖内存中相同id的物品)
+ cloudItems.forEach(item => {
+ // 排除用户自己发布的物品
+ if (!item.isUserPublished) {
+ itemMap.set(item.id, item);
+ }
+ });
+
+ const allItems = Array.from(itemMap.values());
+ console.log('合并后的总物品数量:', allItems.length);
+ console.log('其中类型为 lost 的物品:', allItems.filter(item => item.type === 'lost').length);
+ console.log('其中类型为 found 的物品:', allItems.filter(item => item.type === 'found').length);
+
+ callback(allItems);
+ },
+ fail: (err) => {
+ console.error('❌ 云数据库查询失败:', err);
+ console.log('降级到只使用内存中的物品');
+ callback(memoryItems);
+ }
+ });
+ },
+
+ // 生成模拟匹配数据
+ // matchType: 匹配到的物品类型(lost=匹配到的失物,found=匹配到的招领)
+ // 智能匹配规则:失物匹配招领,招领匹配失物
+ // allItemsFromCloud: 从云数据库获取的所有物品(可选,如果不提供则从内存获取)
+ generateMockMatches: function(matchType, allItemsFromCloud) {
+ console.log('========== 开始生成匹配数据 ==========');
+ console.log('匹配类型(匹配到的物品类型):', matchType);
+
+ // 过滤已忽略的匹配
+ const ignoredIds = this.globalData.ignoredMatches || [];
+
+ // 基于用户发布的物品生成匹配
+ const userItems = this.globalData.userPublishedItems || [];
+ console.log('========== 用户发布的物品 ==========');
+ console.log('用户发布的物品数量:', userItems.length);
+ userItems.forEach((item, idx) => {
+ console.log(` 用户物品 ${idx + 1}:`, {
+ title: item.title,
+ type: item.type,
+ id: item.id,
+ location: item.location,
+ time: item.time || item.date || item.publishTime,
+ hasImages: !!(item.images && item.images.length > 0)
+ });
+ });
+
+ // 获取所有可比较的物品(优先使用从云数据库获取的物品)
+ const allItems = allItemsFromCloud || this.getAllComparableItems(true); // true表示排除用户物品
+ console.log('========== 所有可比较物品 ==========');
+ console.log('所有可比较物品数量:', allItems.length);
+ allItems.forEach((item, idx) => {
+ console.log(` 可比较物品 ${idx + 1}:`, {
+ title: item.title,
+ type: item.type,
+ id: item.id,
+ location: item.location,
+ time: item.time || item.date || item.publishTime,
+ isUserPublished: item.isUserPublished,
+ hasImages: !!(item.images && item.images.length > 0)
+ });
+ });
+
+ const matches = [];
+
+ // 确定用户应该匹配的类型(与matchType相反)
+ // 如果matchType是'found'(匹配到的招领),那么用户发布的是'lost'(失物)
+ // 如果matchType是'lost'(匹配到的失物),那么用户发布的是'found'(招领)
+ const userItemType = matchType === 'found' ? 'lost' : 'found';
+
+ console.log('基于匹配类型推断:用户发布的物品类型应该是:', userItemType);
+
+ // 过滤出符合类型的用户物品(且必须有图片)
+ const relevantUserItems = userItems.filter(item => {
+ const isCorrectType = item.type === userItemType;
+ const hasImages = item.images && item.images.length > 0; // 必须有图片才能匹配
+ return isCorrectType && hasImages;
+ });
+ console.log('符合类型的用户物品数量(且有图片):', relevantUserItems.length);
+
+ if (relevantUserItems.length === 0) {
+ console.log('没有符合类型的用户物品,无法生成匹配');
+ }
+
+ // 遍历用户发布的物品,只处理与matchType相反类型的物品
+ relevantUserItems.forEach(userItem => {
+ console.log('--- 为用户物品查找匹配 ---');
+ console.log('用户物品:', userItem.title, '类型:', userItem.type, 'ID:', userItem.id);
+ console.log('查找匹配类型:', matchType);
+
+ // 从所有物品中筛选出与matchType相同类型的物品(排除用户自己发布的,且必须有图片)
+ const candidateItems = allItems.filter(item => {
+ const isCorrectType = item.type === matchType;
+ const isNotUserPublished = !item.isUserPublished;
+ const isNotSameItem = item.id !== userItem.id;
+ const hasImages = item.images && item.images.length > 0; // 必须有图片
+ const isNotUserItem = item.id !== userItem.id && !item.isUserPublished; // 确保不是用户自己的物品
+
+ console.log(' 候选物品:', item.title, '类型:', item.type,
+ 'matchType:', matchType,
+ 'userItem.type:', userItem.type,
+ 'isUserPublished:', item.isUserPublished,
+ '类型匹配:', isCorrectType,
+ '非用户发布:', isNotUserPublished,
+ '不是同一物品:', isNotSameItem,
+ '不是用户物品:', isNotUserItem,
+ '有图片:', hasImages);
+
+ // 确保:1) 类型匹配 2) 不是用户发布的 3) 不是同一物品 4) 有图片
+ const shouldInclude = isCorrectType && isNotUserPublished && isNotSameItem && hasImages && isNotUserItem;
+
+ if (!shouldInclude) {
+ console.log(` 跳过候选物品: ${item.title}, 原因: 类型匹配=${isCorrectType}, 非用户发布=${isNotUserPublished}, 不是同一物品=${isNotSameItem}, 有图片=${hasImages}`);
+ }
+
+ return shouldInclude;
+ });
+
+ console.log('候选匹配物品数量:', candidateItems.length);
+ candidateItems.forEach((item, idx) => {
+ console.log(` 候选 ${idx + 1}:`, {
+ title: item.title,
+ type: item.type,
+ id: item.id,
+ location: item.location,
+ time: item.time || item.date || item.publishTime,
+ hasImages: !!(item.images && item.images.length > 0),
+ isUserPublished: item.isUserPublished
+ });
+ });
+
+ if (candidateItems.length === 0) {
+ console.log('⚠️ 没有候选物品,可能的原因:');
+ console.log(' 1. allItems中没有类型为', matchType, '的物品');
+ console.log(' 2. 所有物品都是用户自己发布的');
+ console.log(' 3. 所有物品都没有图片');
+ console.log('allItems详情:');
+ allItems.forEach((item, idx) => {
+ console.log(` allItems[${idx}]:`, {
+ title: item.title,
+ type: item.type,
+ isUserPublished: item.isUserPublished,
+ hasImages: !!(item.images && item.images.length > 0)
+ });
+ });
+ }
+
+ if (candidateItems.length === 0) {
+ console.log('没有可用的候选匹配物品');
+ console.log('调试信息:');
+ console.log(' - matchType:', matchType);
+ console.log(' - userItem.type:', userItem.type);
+ console.log(' - userItem.id:', userItem.id);
+ console.log(' - allItems数量:', allItems.length);
+ console.log(' - allItems中类型为', matchType, '的物品:', allItems.filter(item => item.type === matchType).length);
+ console.log(' - allItems中非用户发布的物品:', allItems.filter(item => !item.isUserPublished).length);
+ console.log(' - allItems中有图片的物品:', allItems.filter(item => item.images && item.images.length > 0).length);
+ return; // 跳过这个用户物品
+ }
+
+ // 预先获取用户物品的AI标签(如果还没有)
+ const userImagePath = userItem.images && userItem.images.length > 0 ? userItem.images[0] : null;
+ if (userImagePath && !this.globalData.aiLabelsCache[userImagePath]) {
+ console.log('预先获取用户物品的AI标签:', userImagePath);
+ this.getAIImageLabels(userImagePath, (labels) => {
+ if (labels) {
+ console.log('✅ 用户物品AI标签获取成功,数量:', labels.length);
+ }
+ });
+ }
+
+ // 计算所有候选物品的匹配度,然后按匹配度排序
+ // 使用分层匹配策略:1) 文本匹配优先 2) 图片匹配 3) 综合匹配
+ const candidateScores = candidateItems.map(matchedItem => {
+ // 预先获取匹配物品的AI标签(如果还没有)
+ const matchedImagePath = matchedItem.images && matchedItem.images.length > 0 ? matchedItem.images[0] : null;
+ if (matchedImagePath && !this.globalData.aiLabelsCache[matchedImagePath]) {
+ console.log('预先获取匹配物品的AI标签:', matchedImagePath);
+ this.getAIImageLabels(matchedImagePath, (labels) => {
+ if (labels) {
+ console.log('✅ 匹配物品AI标签获取成功,数量:', labels.length);
+ }
+ });
+ }
+
+ // 第一层:计算综合文本相似度(标题、描述、地点、日期)
+ const comprehensiveTextSimilarity = this.calculateComprehensiveTextSimilarity(userItem, matchedItem);
+
+ // 如果文本相似度 >= 50%,直接匹配成功
+ if (comprehensiveTextSimilarity >= 50) {
+ console.log(`✅ 文本匹配成功: ${matchedItem.title}, 综合文本相似度=${comprehensiveTextSimilarity}%`);
+ return {
+ item: matchedItem,
+ matchDegree: comprehensiveTextSimilarity,
+ imageSimilarity: 0,
+ textSimilarity: comprehensiveTextSimilarity,
+ locationProximity: 0,
+ matchStrategy: 'text_first' // 标记匹配策略
+ };
+ }
+
+ // 第二层:检查图片是否完全相同
+ // 注意:即使图片完全相同,也要结合文本相似度,避免不同物品因为使用相同默认图片而错误匹配
+ const isImageIdentical = this.checkImageIdentical(userItem, matchedItem);
+ if (isImageIdentical) {
+ // 如果图片完全相同,但文本相似度很低,可能是使用了相同的默认图片,不应该直接匹配
+ // 只有当文本相似度 >= 30% 时,才认为图片匹配有效
+ if (comprehensiveTextSimilarity >= 30) {
+ console.log(`✅ 图片匹配成功: ${matchedItem.title}, 图片完全相同,文本相似度=${comprehensiveTextSimilarity}%`);
+ // 综合匹配度:图片100%,但结合文本相似度,避免误匹配
+ const matchDegree = Math.floor(
+ comprehensiveTextSimilarity * 0.3 + 100 * 0.7
+ );
+ return {
+ item: matchedItem,
+ matchDegree: matchDegree,
+ imageSimilarity: 100,
+ textSimilarity: comprehensiveTextSimilarity,
+ locationProximity: 0,
+ matchStrategy: 'image_first' // 标记匹配策略
+ };
+ } else {
+ console.log(`⚠️ 图片完全相同但文本相似度太低(${comprehensiveTextSimilarity}%),可能是使用了相同默认图片,跳过图片匹配`);
+ // 继续到第三层综合匹配
+ }
+ }
+
+ // 第三层:综合匹配(文本50% + 图片50%)
+ const imageSimilarity = this.calculateMockImageSimilarity(userItem, matchedItem);
+ const matchDegree = Math.floor(
+ comprehensiveTextSimilarity * 0.5 +
+ imageSimilarity * 0.5
+ );
+
+ console.log(`综合匹配: ${matchedItem.title}, 文本=${comprehensiveTextSimilarity}%, 图片=${imageSimilarity}%, 综合=${matchDegree}%`);
+
+ return {
+ item: matchedItem,
+ matchDegree: matchDegree,
+ imageSimilarity: imageSimilarity,
+ textSimilarity: comprehensiveTextSimilarity,
+ locationProximity: 0,
+ matchStrategy: 'comprehensive' // 标记匹配策略
+ };
+ });
+
+ // 按匹配度降序排序
+ candidateScores.sort((a, b) => b.matchDegree - a.matchDegree);
+
+ console.log('========== 所有候选物品匹配度排序结果 ==========');
+ if (candidateScores.length === 0) {
+ console.log('⚠️ 没有候选物品的匹配度计算结果!');
+ console.log('可能的原因:候选物品数量为0,或者匹配度计算过程中出错');
+ } else {
+ candidateScores.forEach((score, index) => {
+ const strategy = score.matchStrategy || 'unknown';
+ const strategyName = {
+ 'text_first': '文本优先匹配',
+ 'image_first': '图片优先匹配',
+ 'comprehensive': '综合匹配'
+ }[strategy] || strategy;
+ console.log(` ${index + 1}. ${score.item.title}: 匹配度=${score.matchDegree}% (策略=${strategyName}, 图片=${score.imageSimilarity}%, 文本=${score.textSimilarity}%)`);
+ });
+ }
+
+ // 选择匹配度最高的物品(至少匹配度>=20%,最多选择5个)
+ // 降低阈值,因为如果图片相似度低但文本和位置相似,也应该匹配
+ const selectedScores = candidateScores
+ .filter(score => {
+ // 三重确保:匹配的类型必须与matchType相同,且与用户物品类型相反,且不是用户物品
+ if (score.item.type !== matchType) {
+ return false;
+ }
+ if (score.item.type === userItem.type) {
+ return false;
+ }
+ if (score.item.isUserPublished || score.item.id === userItem.id) {
+ return false;
+ }
+
+ // 匹配度阈值:>=20%(因为分层匹配已经处理了优先级)
+ const shouldMatch = score.matchDegree >= 20;
+
+ if (!shouldMatch) {
+ const strategy = score.matchStrategy || 'unknown';
+ console.log(`跳过匹配项: ${score.item.title}, 匹配度=${score.matchDegree}%, 策略=${strategy}, 文本=${score.textSimilarity}%, 图片=${score.imageSimilarity}%`);
+ }
+
+ return shouldMatch;
+ })
+ .slice(0, 5); // 最多选择5个
+
+ console.log('========== 最终选择的匹配项 ==========');
+ console.log('最终选择的匹配项数量:', selectedScores.length);
+ if (selectedScores.length === 0) {
+ console.log('⚠️ 没有选择任何匹配项!');
+ console.log('可能的原因:');
+ console.log(' 1. 所有候选物品的匹配度都 < 20%');
+ console.log(' 2. 候选物品被过滤掉了(类型不匹配、是用户物品等)');
+ if (candidateScores.length > 0) {
+ console.log('候选物品匹配度详情:');
+ candidateScores.forEach((score, idx) => {
+ console.log(` 候选 ${idx + 1}: ${score.item.title}, 匹配度=${score.matchDegree}%, 类型=${score.item.type}, 用户物品类型=${userItem.type}, matchType=${matchType}`);
+ console.log(` 类型检查: score.item.type(${score.item.type}) === matchType(${matchType}) = ${score.item.type === matchType}`);
+ console.log(` 类型检查: score.item.type(${score.item.type}) !== userItem.type(${userItem.type}) = ${score.item.type !== userItem.type}`);
+ console.log(` 用户物品检查: isUserPublished=${score.item.isUserPublished}, id相同=${score.item.id === userItem.id}`);
+ });
+ }
+ } else {
+ selectedScores.forEach((score, idx) => {
+ console.log(` 匹配项 ${idx + 1}: ${score.item.title}, 匹配度=${score.matchDegree}%`);
+ });
+ }
+
+ selectedScores.forEach(score => {
+ const matchedItem = score.item;
+ const matchDegree = score.matchDegree;
+ const imageSimilarity = score.imageSimilarity;
+ const textSimilarity = score.textSimilarity;
+ const strategy = score.matchStrategy || 'unknown';
+ const strategyName = {
+ 'text_first': '文本优先匹配',
+ 'image_first': '图片优先匹配',
+ 'comprehensive': '综合匹配'
+ }[strategy] || strategy;
+
+ console.log('处理匹配项:', matchedItem.title, '类型:', matchedItem.type, 'ID:', matchedItem.id);
+ console.log('匹配度计算:', {
+ 策略: strategyName,
+ imageSimilarity: `${imageSimilarity}%`,
+ textSimilarity: `${textSimilarity}%`,
+ matchDegree: `${matchDegree}%`
+ });
+
+ // 创建匹配项ID
+ const matchId = `match_${userItem.id}_${matchedItem.id}_${Date.now()}`;
+
+ // 跳过已忽略的匹配
+ if (ignoredIds.includes(matchId)) {
+ console.log('匹配已被忽略,跳过');
+ return;
+ }
+
+ matches.push({
+ id: matchId,
+ matchDegree: matchDegree,
+ ...matchedItem,
+ userItemId: userItem.id, // 用户的物品ID,用于展示关系
+ matchedTime: new Date().toISOString() // 匹配时间
+ });
+
+ console.log('✅ 添加匹配项:', matchedItem.title, '类型:', matchedItem.type, '匹配度:', matchDegree);
+ });
+ });
+
+ // 如果没有真实匹配,不返回默认模拟数据
+ // 改为返回空数组,让用户知道没有找到匹配
+ if (matches.length === 0) {
+ console.log('⚠️ 没有找到真实匹配项,返回空结果(已移除默认模拟匹配数据)');
+ // 不再生成默认匹配数据,只返回真实匹配的结果
+ // const defaultMatches = this.getMockDefaultMatches(matchType);
+ // defaultMatches.forEach(match => {
+ // if (!ignoredIds.includes(match.id)) {
+ // matches.push(match);
+ // }
+ // });
+ }
+
+ // 按匹配度降序排序
+ matches.sort((a, b) => b.matchDegree - a.matchDegree);
+
+ console.log('========== 匹配数据生成完成 ==========');
+ console.log('最终匹配结果数量:', matches.length, '类型:', matchType);
+ matches.forEach((match, index) => {
+ console.log(`匹配项 ${index + 1}:`, match.title, '类型:', match.type, '匹配度:', match.matchDegree);
+ });
+
+ return matches;
+ },
+
+ // 获取默认模拟匹配数据
+ // matchType: 匹配到的物品类型(lost=失物,found=招领)
+ getMockDefaultMatches: function(matchType) {
+ // matchType 就是匹配到的物品类型,直接使用
+ const defaultItems = [
+ {
+ id: 'default_match_1',
+ matchDegree: 85,
+ type: matchType, // 匹配到的物品类型
+ title: matchType === 'found' ? '黑色钱包' : '丢失的黑色钱包',
+ description: matchType === 'found'
+ ? '在图书馆二楼发现的黑色钱包,内有学生证和少量现金。'
+ : '在图书馆二楼丢失的黑色钱包,内有学生证和少量现金。',
+ location: '学校图书馆',
+ time: new Date(Date.now() - 86400000).toISOString().split('T')[0],
+ images: [matchType === 'found' ? '/images/found.png' : '/images/lost.png'],
+ isUserPublished: false
+ },
+ {
+ id: 'default_match_2',
+ matchDegree: 72,
+ type: matchType,
+ title: matchType === 'found' ? '蓝牙耳机' : '丢失的蓝牙耳机',
+ description: matchType === 'found'
+ ? '在食堂捡到的白色蓝牙耳机,带有充电盒。'
+ : '在食堂附近丢失的白色蓝牙耳机,带有充电盒。',
+ location: '校园食堂',
+ time: new Date(Date.now() - 172800000).toISOString().split('T')[0],
+ images: ['/images/match.svg'],
+ isUserPublished: false
+ },
+ {
+ id: 'default_match_3',
+ matchDegree: 60,
+ type: matchType,
+ title: matchType === 'found' ? '笔记本电脑' : '丢失的笔记本电脑',
+ description: matchType === 'found'
+ ? '在教学楼教室发现的银色笔记本电脑,品牌未知。'
+ : '在教学楼教室丢失的银色笔记本电脑,品牌未知。',
+ location: '教学楼A区',
+ time: new Date(Date.now() - 259200000).toISOString().split('T')[0],
+ images: ['/images/search.png'],
+ isUserPublished: false
+ }
+ ];
+
+ return defaultItems;
+ },
+
+ // 获取图片的AI识别标签(使用腾讯AI)
+ getAIImageLabels: function(imagePath, callback) {
+ const cacheKey = imagePath;
+
+ // 检查缓存
+ if (this.globalData.aiLabelsCache[cacheKey]) {
+ console.log('使用缓存的AI识别标签:', cacheKey);
+ callback(this.globalData.aiLabelsCache[cacheKey]);
+ return;
+ }
+
+ console.log('开始调用腾讯AI识别图片标签:', imagePath);
+
+ // 优先使用腾讯云API
+ const config = this.AI_CONFIG.TENCENT_AI;
+ if (config.USE_TENCENT_CLOUD && config.SECRET_ID && config.SECRET_KEY &&
+ config.SECRET_ID !== 'YOUR_SECRET_ID') {
+
+ // 处理不同路径格式的图片
+ const readImageAsBase64 = (path, cb) => {
+ // 如果是 cloud:// 路径,需要先转换为临时URL
+ if (path && path.startsWith('cloud://')) {
+ console.log('检测到cloud://路径,需要先转换为临时URL');
+
+ if (!wx.cloud || typeof wx.cloud.getTempFileURL !== 'function') {
+ console.warn('不支持云开发API,无法转换cloud://路径');
+ cb(null);
+ return;
+ }
+
+ // 转换为临时URL
+ wx.cloud.getTempFileURL({
+ fileList: [path],
+ success: (res) => {
+ if (res.fileList && res.fileList.length > 0 && res.fileList[0].tempFileURL) {
+ const tempUrl = res.fileList[0].tempFileURL;
+ console.log('云存储路径转换成功,下载图片:', tempUrl);
+
+ // 下载图片到本地临时文件
+ wx.downloadFile({
+ url: tempUrl,
+ success: (downloadRes) => {
+ if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
+ // 读取下载的临时文件
+ wx.getFileSystemManager().readFile({
+ filePath: downloadRes.tempFilePath,
+ encoding: 'base64',
+ success: (readRes) => {
+ cb(readRes.data);
+ },
+ fail: (readErr) => {
+ console.error('❌ 读取下载的图片文件失败:', readErr);
+ cb(null);
+ }
+ });
+ } else {
+ console.error('❌ 下载图片失败:', downloadRes);
+ cb(null);
+ }
+ },
+ fail: (downloadErr) => {
+ console.error('❌ 下载图片失败:', downloadErr);
+ cb(null);
+ }
+ });
+ } else {
+ console.warn('云存储路径转换失败,未返回临时URL');
+ cb(null);
+ }
+ },
+ fail: (convertErr) => {
+ console.error('❌ 转换cloud://路径失败:', convertErr);
+ cb(null);
+ }
+ });
+ }
+ // 如果是 http/https URL,需要先下载
+ else if (path && (path.startsWith('http://') || path.startsWith('https://'))) {
+ console.log('检测到HTTP URL,下载图片:', path);
+ wx.downloadFile({
+ url: path,
+ success: (downloadRes) => {
+ if (downloadRes.statusCode === 200 && downloadRes.tempFilePath) {
+ wx.getFileSystemManager().readFile({
+ filePath: downloadRes.tempFilePath,
+ encoding: 'base64',
+ success: (readRes) => {
+ cb(readRes.data);
+ },
+ fail: (readErr) => {
+ console.error('❌ 读取下载的图片文件失败:', readErr);
+ cb(null);
+ }
+ });
+ } else {
+ console.error('❌ 下载图片失败:', downloadRes);
+ cb(null);
+ }
+ },
+ fail: (downloadErr) => {
+ console.error('❌ 下载图片失败:', downloadErr);
+ cb(null);
+ }
+ });
+ }
+ // 如果是本地路径(wxfile:// 或 / 开头),直接读取
+ else if (path && (path.startsWith('wxfile://') || path.startsWith('/'))) {
+ console.log('检测到本地路径,直接读取:', path);
+ wx.getFileSystemManager().readFile({
+ filePath: path,
+ encoding: 'base64',
+ success: (res) => {
+ cb(res.data);
+ },
+ fail: (err) => {
+ console.error('❌ 读取图片文件失败:', err);
+ cb(null);
+ }
+ });
+ } else {
+ console.error('❌ 不支持的图片路径格式:', path);
+ cb(null);
+ }
+ };
+
+ // 读取图片并转换为base64
+ readImageAsBase64(imagePath, (base64Image) => {
+ if (!base64Image) {
+ console.warn('⚠️ 无法读取图片,降级到本地特征');
+ callback(null);
+ return;
+ }
+
+ // 调用云函数进行AI识别
+ if (typeof wx.cloud.callFunction === 'function') {
+ wx.cloud.callFunction({
+ name: 'tencentAI',
+ data: {
+ image: base64Image,
+ secretId: config.SECRET_ID,
+ secretKey: config.SECRET_KEY,
+ region: config.REGION || 'ap-beijing'
+ },
+ success: (cloudRes) => {
+ if (cloudRes.result && cloudRes.result.apiResult &&
+ cloudRes.result.apiResult.Response &&
+ cloudRes.result.apiResult.Response.Labels) {
+ const labels = cloudRes.result.apiResult.Response.Labels.map(label => ({
+ name: label.Name,
+ confidence: label.Confidence || 0
+ }));
+
+ // 缓存结果
+ this.globalData.aiLabelsCache[cacheKey] = labels;
+ console.log('✅ 腾讯云AI识别成功,标签数量:', labels.length);
+ callback(labels);
+ } else {
+ console.warn('⚠️ 腾讯云AI返回格式错误,降级到本地特征');
+ callback(null);
+ }
+ },
+ fail: (err) => {
+ console.error('❌ 云函数调用失败:', err);
+ callback(null);
+ }
+ });
+ } else {
+ callback(null);
+ }
+ });
+ return;
+ }
+
+ // 降级到本地特征提取
+ console.warn('⚠️ 腾讯AI未配置,使用本地特征提取');
+ callback(null);
+ },
+
+ // 基于AI识别标签计算语义相似度
+ calculateAILabelSimilarity: function(labels1, labels2) {
+ if (!labels1 || !labels2 || labels1.length === 0 || labels2.length === 0) {
+ return 0;
+ }
+
+ // 提取标签名称(转换为小写,去除空格)
+ const normalizeLabel = (name) => (name || '').toLowerCase().trim();
+ const labels1Set = new Set(labels1.map(l => normalizeLabel(l.name || l.tag_name)));
+ const labels2Set = new Set(labels2.map(l => normalizeLabel(l.name || l.tag_name)));
+
+ // 计算交集和并集
+ const intersection = new Set([...labels1Set].filter(x => labels2Set.has(x)));
+ const union = new Set([...labels1Set, ...labels2Set]);
+
+ // Jaccard相似度
+ const jaccardSimilarity = intersection.size / union.size;
+
+ // 计算加权相似度(考虑置信度)
+ let weightedSimilarity = 0;
+ let totalWeight = 0;
+
+ labels1.forEach(label1 => {
+ const name1 = normalizeLabel(label1.name || label1.tag_name);
+ const conf1 = label1.confidence || label1.Confidence || 0;
+
+ labels2.forEach(label2 => {
+ const name2 = normalizeLabel(label2.name || label2.tag_name);
+ const conf2 = label2.confidence || label2.Confidence || 0;
+
+ if (name1 === name2) {
+ // 相同标签,使用平均置信度
+ const avgConf = (conf1 + conf2) / 2;
+ weightedSimilarity += avgConf;
+ totalWeight += 100; // 最大置信度
+ }
+ });
+ });
+
+ const weightedScore = totalWeight > 0 ? weightedSimilarity / totalWeight : 0;
+
+ // 综合相似度:Jaccard相似度(40%)+ 加权相似度(60%)
+ const finalSimilarity = jaccardSimilarity * 0.4 + weightedScore * 0.6;
+
+ console.log('AI标签相似度计算:', {
+ labels1: labels1.map(l => l.name || l.tag_name),
+ labels2: labels2.map(l => l.name || l.tag_name),
+ intersection: intersection.size,
+ union: union.size,
+ jaccardSimilarity: jaccardSimilarity.toFixed(3),
+ weightedScore: weightedScore.toFixed(3),
+ finalSimilarity: finalSimilarity.toFixed(3)
+ });
+
+ return Math.floor(finalSimilarity * 100);
+ },
+
+ // 计算颜色相似度(专门用于颜色匹配)
+ calculateColorSimilarity: function(item1, item2) {
+ const features1 = Array.isArray(item1.imageFeatures) ? item1.imageFeatures : [];
+ const features2 = Array.isArray(item2.imageFeatures) ? item2.imageFeatures : [];
+
+ if (features1.length === 0 || features2.length === 0) {
+ return 0;
+ }
+
+ // 提取颜色相关的特征维度
+ // 基础特征(230维):RGB直方图(0-191) + 亮度直方图(192-223) + 平均颜色(224-226) + 尺寸(227-229)
+ // 增强特征(564维):HSV(0-99) + 边缘(100-163) + 纹理(164-419) + 形状(420-451) + 多尺度(452-515) + 空间(516-563)
+
+ let colorFeatures1 = [];
+ let colorFeatures2 = [];
+
+ // 判断特征类型并提取颜色特征
+ if (features1.length >= 564) {
+ // 增强特征:提取HSV特征(前100维)和空间分布特征(最后48维)
+ colorFeatures1 = [
+ ...features1.slice(0, 100), // HSV特征
+ ...features1.slice(516, 564) // 空间分布特征(RGB网格)
+ ];
+ colorFeatures2 = [
+ ...features2.slice(0, 100),
+ ...features2.slice(516, 564)
+ ];
+ } else if (features1.length >= 230) {
+ // 基础特征:提取RGB直方图(前192维)和平均颜色(224-226维)
+ colorFeatures1 = [
+ ...features1.slice(0, 192), // RGB直方图
+ ...features1.slice(224, 227) // 平均颜色
+ ];
+ colorFeatures2 = [
+ ...features2.slice(0, 192),
+ ...features2.slice(224, 227)
+ ];
+ } else {
+ // 特征维度不足,使用全部特征
+ colorFeatures1 = features1;
+ colorFeatures2 = features2;
+ }
+
+ // 计算颜色特征的余弦相似度
+ const colorSimilarity = this.calculateCosineSimilarity(colorFeatures1, colorFeatures2);
+
+ // 提取主色调(从HSV特征中提取)
+ let dominantColor1 = this.extractDominantColor(features1);
+ let dominantColor2 = this.extractDominantColor(features2);
+
+ // 计算主色调相似度(如果都能提取到)
+ let dominantColorSimilarity = 0;
+ if (dominantColor1 && dominantColor2) {
+ // 计算HSV空间中的颜色距离
+ const hDiff = Math.min(Math.abs(dominantColor1.h - dominantColor2.h), 360 - Math.abs(dominantColor1.h - dominantColor2.h)) / 180; // 归一化到0-1
+ const sDiff = Math.abs(dominantColor1.s - dominantColor2.s);
+ const vDiff = Math.abs(dominantColor1.v - dominantColor2.v);
+
+ // 综合颜色距离(色调权重更高)
+ const colorDistance = (hDiff * 0.6 + sDiff * 0.2 + vDiff * 0.2);
+ dominantColorSimilarity = Math.max(0, 1 - colorDistance);
+ }
+
+ // 综合颜色相似度:直方图相似度(70%)+ 主色调相似度(30%)
+ const finalColorSimilarity = colorSimilarity * 0.7 + dominantColorSimilarity * 0.3;
+
+ console.log(`颜色相似度: ${item1.title} vs ${item2.title}`, {
+ colorSimilarity: (colorSimilarity * 100).toFixed(1) + '%',
+ dominantColor1: dominantColor1 ? `H:${dominantColor1.h.toFixed(0)} S:${dominantColor1.s.toFixed(2)} V:${dominantColor1.v.toFixed(2)}` : 'N/A',
+ dominantColor2: dominantColor2 ? `H:${dominantColor2.h.toFixed(0)} S:${dominantColor2.s.toFixed(2)} V:${dominantColor2.v.toFixed(2)}` : 'N/A',
+ dominantColorSimilarity: (dominantColorSimilarity * 100).toFixed(1) + '%',
+ finalColorSimilarity: (finalColorSimilarity * 100).toFixed(1) + '%'
+ });
+
+ return finalColorSimilarity;
+ },
+
+ // 从特征向量中提取主色调
+ extractDominantColor: function(features) {
+ if (!features || features.length === 0) {
+ return null;
+ }
+
+ // 如果是增强特征(564维),HSV特征在前100维
+ if (features.length >= 564) {
+ // HSV直方图:H(0-35, 36维) + S(36-67, 32维) + V(68-99, 32维)
+ const hHist = features.slice(0, 36);
+ const sHist = features.slice(36, 68);
+ const vHist = features.slice(68, 100);
+
+ // 找到最大值所在的bin
+ const maxHIdx = hHist.indexOf(Math.max(...hHist));
+ const maxSIdx = sHist.indexOf(Math.max(...sHist));
+ const maxVIdx = vHist.indexOf(Math.max(...vHist));
+
+ // 转换为HSV值
+ const h = (maxHIdx + 0.5) * 10; // 每个bin代表10度
+ const s = (maxSIdx + 0.5) / 31; // 归一化到0-1
+ const v = (maxVIdx + 0.5) / 31; // 归一化到0-1
+
+ return { h, s, v };
+ }
+
+ // 如果是基础特征(230维),从RGB直方图中提取
+ if (features.length >= 230) {
+ // RGB直方图:R(0-63) + G(64-127) + B(128-191)
+ const rHist = features.slice(0, 64);
+ const gHist = features.slice(64, 128);
+ const bHist = features.slice(128, 192);
+
+ const maxRIdx = rHist.indexOf(Math.max(...rHist));
+ const maxGIdx = gHist.indexOf(Math.max(...gHist));
+ const maxBIdx = bHist.indexOf(Math.max(...bHist));
+
+ // 转换为RGB值
+ const r = (maxRIdx + 0.5) * 4; // 每个bin代表4个值
+ const g = (maxGIdx + 0.5) * 4;
+ const b = (maxBIdx + 0.5) * 4;
+
+ // RGB转HSV
+ const rNorm = r / 255;
+ const gNorm = g / 255;
+ const bNorm = b / 255;
+
+ const max = Math.max(rNorm, gNorm, bNorm);
+ const min = Math.min(rNorm, gNorm, bNorm);
+ const delta = max - min;
+
+ let h = 0;
+ if (delta !== 0) {
+ if (max === rNorm) {
+ h = 60 * (((gNorm - bNorm) / delta) % 6);
+ } else if (max === gNorm) {
+ h = 60 * ((bNorm - rNorm) / delta + 2);
+ } else {
+ h = 60 * ((rNorm - gNorm) / delta + 4);
+ }
+ }
+ if (h < 0) h += 360;
+
+ const s = max === 0 ? 0 : delta / max;
+ const v = max;
+
+ return { h, s, v };
+ }
+
+ return null;
+ },
+
+ // 计算图像相似度(支持AI识别和降级方案,增强颜色识别)
+ calculateMockImageSimilarity: function(item1, item2) {
+ // 如果两个物品都有图片,优先使用AI识别
+ if (item1.images && item1.images.length > 0 &&
+ item2.images && item2.images.length > 0) {
+
+ const image1 = item1.images[0];
+ const image2 = item2.images[0];
+
+ // 检查是否已缓存AI标签
+ const labels1Cache = this.globalData.aiLabelsCache[image1];
+ const labels2Cache = this.globalData.aiLabelsCache[image2];
+
+ // 如果两个图片都有AI标签,使用AI标签相似度
+ if (labels1Cache && labels2Cache) {
+ const aiSimilarity = this.calculateAILabelSimilarity(labels1Cache, labels2Cache);
+ console.log(`✅ AI标签相似度: 物品1=${item1.title}, 物品2=${item2.title}, 相似度=${aiSimilarity}%`);
+ return aiSimilarity;
+ }
+
+ // 否则使用特征向量计算相似度(包含颜色识别)
+ const features1 = Array.isArray(item1.imageFeatures) ? item1.imageFeatures : [];
+ const features2 = Array.isArray(item2.imageFeatures) ? item2.imageFeatures : [];
+
+ if (features1.length === 0 || features2.length === 0) {
+ console.warn('缺少图片内容特征,无法计算相似度', {
+ item1: item1.title,
+ item2: item2.title
+ });
+ return 0;
+ }
+
+ // 计算整体特征相似度(余弦相似度)
+ const overallSimilarity = this.calculateCosineSimilarity(features1, features2);
+
+ // 计算颜色相似度(专门的颜色匹配)
+ const colorSimilarity = this.calculateColorSimilarity(item1, item2);
+
+ // 综合相似度:整体特征(60%)+ 颜色特征(40%)
+ // 这样颜色相似的物品会有更高的相似度
+ const finalSimilarity = overallSimilarity * 0.6 + colorSimilarity * 0.4;
+
+ const similarityPercent = Math.floor(finalSimilarity * 100);
+ console.log(`图片相似度计算: 物品1=${item1.title}, 物品2=${item2.title}`, {
+ overallSimilarity: (overallSimilarity * 100).toFixed(1) + '%',
+ colorSimilarity: (colorSimilarity * 100).toFixed(1) + '%',
+ finalSimilarity: similarityPercent + '%'
+ });
+
+ return similarityPercent;
+ }
+
+ // 没有图片时返回0,不进行匹配(因为只基于图片匹配)
+ console.log(`警告: 物品缺少图片,无法计算相似度。物品1=${item1.title}, 物品2=${item2.title}`);
+ return 0;
+ },
+
+ // 计算文本相似度(改进版:考虑标题和描述,排除联系人信息)
+ calculateTextSimilarity: function(item1, item2) {
+ // 简单的文本相似度计算
+ // 排除联系人姓名和电话号码,只使用标题和描述
+ let title1 = (item1.title || '').toLowerCase().trim();
+ let title2 = (item2.title || '').toLowerCase().trim();
+ let desc1 = (item1.description || '').toLowerCase().trim();
+ let desc2 = (item2.description || '').toLowerCase().trim();
+
+ // 从描述中移除联系人姓名和电话号码
+ const removeContactInfo = (text, contactName, contactPhone) => {
+ let cleaned = text;
+
+ // 移除联系人姓名(如果存在)
+ if (contactName) {
+ const name = contactName.toLowerCase().trim();
+ // 移除姓名及其周围的常见连接词
+ cleaned = cleaned.replace(new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), '');
+ cleaned = cleaned.replace(/联系\s*[::]\s*/g, '');
+ cleaned = cleaned.replace(/电话\s*[::]\s*/g, '');
+ }
+
+ // 移除电话号码(支持多种格式)
+ if (contactPhone) {
+ const phone = contactPhone.trim();
+ // 移除原始电话号码
+ cleaned = cleaned.replace(new RegExp(phone.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '');
+ // 移除带分隔符的电话号码(如 138-0013-8000, 138 0013 8000)
+ const phoneFormatted = phone.replace(/[-\s]/g, '');
+ if (phoneFormatted.length >= 7) {
+ // 匹配各种格式的电话号码
+ const phonePatterns = [
+ phoneFormatted, // 原始号码
+ phoneFormatted.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'), // 138-0013-8000
+ phoneFormatted.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3'), // 138 0013 8000
+ phoneFormatted.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'), // 138-0013-8000
+ ];
+ phonePatterns.forEach(pattern => {
+ cleaned = cleaned.replace(new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '');
+ });
+ }
+ }
+
+ // 移除常见的电话号码模式(11位数字、带分隔符等)
+ cleaned = cleaned.replace(/\d{3}[-.\s]?\d{4}[-.\s]?\d{4}/g, ''); // 手机号格式
+ cleaned = cleaned.replace(/\d{7,11}/g, ''); // 7-11位连续数字
+
+ // 清理多余的空格
+ cleaned = cleaned.replace(/\s+/g, ' ').trim();
+
+ return cleaned;
+ };
+
+ // 移除联系人信息
+ desc1 = removeContactInfo(desc1, item1.contactName, item1.contactPhone);
+ desc2 = removeContactInfo(desc2, item2.contactName, item2.contactPhone);
+
+ // 如果标题完全相同,返回高相似度
+ if (title1 === title2 && title1) {
+ console.log('文本相似度:标题完全相同,返回90%');
+ return 90;
+ }
+
+ // 提取标题和描述中的关键词(中文和英文)
+ const extractKeywords = (text) => {
+ // 移除标点符号和特殊字符
+ const cleaned = text.replace(/[,。!?、;:""''()【】《》\s]+/g, ' ');
+ // 分割成词(支持中文和英文)
+ const words = cleaned.split(/\s+/).filter(word => word.length > 0);
+ // 进一步处理中文:尝试提取2-4字的词组
+ const chineseWords = [];
+ for (let i = 0; i < text.length; i++) {
+ // 提取2-4字的中文词组
+ for (let len = 2; len <= 4 && i + len <= text.length; len++) {
+ const word = text.substring(i, i + len);
+ // 检查是否是中文字符
+ if (/[\u4e00-\u9fa5]/.test(word)) {
+ chineseWords.push(word);
+ }
+ }
+ }
+ // 合并所有词,过滤掉纯数字(可能是电话号码残留)
+ return [...words, ...chineseWords]
+ .filter(word => word.length > 0 && !/^\d+$/.test(word)) // 排除纯数字
+ .filter((word, index, arr) => arr.indexOf(word) === index); // 去重
+ };
+
+ const words1 = extractKeywords(title1 + ' ' + desc1);
+ const words2 = extractKeywords(title2 + ' ' + desc2);
+
+ console.log('文本相似度计算(已排除联系人信息):', {
+ title1: title1,
+ title2: title2,
+ desc1_cleaned: desc1.substring(0, 50),
+ desc2_cleaned: desc2.substring(0, 50),
+ words1: words1.slice(0, 10),
+ words2: words2.slice(0, 10)
+ });
+
+ // 检查共同关键词
+ const commonWords = [];
+ words1.forEach(word => {
+ if (word.length > 1 && words2.includes(word)) {
+ commonWords.push(word);
+ }
+ });
+
+ console.log('共同关键词:', commonWords);
+
+ // 基于共同词比例计算相似度
+ let similarity = 0;
+ const totalWords = words1.length + words2.length;
+
+ // 首先检查标题和描述是否完全相同(已排除联系人信息)
+ const titleMatch = title1 === title2 && title1;
+ const descMatch = desc1 === desc2 && desc1;
+
+ if (titleMatch && descMatch) {
+ // 标题和描述都完全相同,返回95%相似度
+ similarity = 95;
+ console.log('文本相似度:标题和描述完全相同(已排除联系人信息),返回95%');
+ } else if (titleMatch) {
+ // 标题相同,描述不同
+ if (totalWords > 0) {
+ similarity = (commonWords.length * 2) / totalWords * 100;
+ similarity = Math.min(similarity + 30, 100); // 标题相同,增加30%
+ } else {
+ similarity = 85; // 标题相同,即使没有提取到关键词
+ }
+ } else if (descMatch) {
+ // 描述相同,标题不同
+ if (totalWords > 0) {
+ similarity = (commonWords.length * 2) / totalWords * 100;
+ similarity = Math.min(similarity + 20, 100); // 描述相同,增加20%
+ } else {
+ similarity = 70; // 描述相同,即使没有提取到关键词
+ }
+ } else if (totalWords > 0) {
+ // 使用Jaccard相似度:共同词数 / 总词数
+ similarity = (commonWords.length * 2) / totalWords * 100;
+
+ // 如果标题有部分匹配,增加相似度
+ if (title1 && title2) {
+ // 检查标题是否包含对方的子串
+ if (title1.includes(title2) || title2.includes(title1)) {
+ similarity = Math.min(similarity + 20, 100);
+ }
+ }
+ } else {
+ // 没有提取到关键词,但标题或描述有部分相似
+ if (title1 && title2 && (title1.includes(title2) || title2.includes(title1))) {
+ similarity = 60; // 标题部分匹配
+ } else if (desc1 && desc2 && (desc1.includes(desc2) || desc2.includes(desc1))) {
+ similarity = 50; // 描述部分匹配
+ }
+ }
+
+ const finalSimilarity = Math.floor(Math.min(similarity, 100));
+ console.log(`文本相似度: ${finalSimilarity}% (共同词: ${commonWords.length}, 总词: ${totalWords}, 标题相同: ${titleMatch}, 描述相同: ${descMatch})`);
+
+ return finalSimilarity;
+ },
+
+ // 计算位置接近度
+ calculateLocationProximity: function(item1, item2) {
+ // 简化的位置接近度计算
+ const loc1 = (item1.location || '').toLowerCase();
+ const loc2 = (item2.location || '').toLowerCase();
+
+ // 如果位置完全相同,相似度高
+ if (loc1 === loc2 && loc1) {
+ return 90;
+ }
+
+ // 检查是否包含共同的位置关键词
+ const locationKeywords = ['图书馆', '食堂', '教学楼', '宿舍', '操场', '校门'];
+ let commonLocations = 0;
+
+ locationKeywords.forEach(keyword => {
+ if (loc1.includes(keyword) && loc2.includes(keyword)) {
+ commonLocations++;
+ }
+ });
+
+ // 有共同位置关键词,给中等相似度
+ if (commonLocations > 0) {
+ return 60;
+ }
+
+ // 没有位置信息或没有共同关键词,给低相似度
+ return 30;
+ },
+
+ // 计算日期相似度
+ calculateDateSimilarity: function(item1, item2) {
+ // 支持多种日期字段名:time, date, publishTime, createTime, foundTime, lostTime
+ const date1 = item1.time || item1.date || item1.publishTime || item1.createTime || item1.foundTime || item1.lostTime || '';
+ const date2 = item2.time || item2.date || item2.publishTime || item2.createTime || item2.foundTime || item2.lostTime || '';
+
+ // 如果日期是ISO格式(如 "2025-11-06T13:08:57.307Z"),提取日期部分
+ const normalizeDate = (dateStr) => {
+ if (!dateStr) return '';
+ // 如果是ISO格式,提取日期部分(YYYY-MM-DD)
+ if (dateStr.includes('T')) {
+ return dateStr.split('T')[0];
+ }
+ // 如果已经是YYYY-MM-DD格式,直接返回
+ if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) {
+ return dateStr.substring(0, 10);
+ }
+ return dateStr;
+ };
+
+ const normalizedDate1 = normalizeDate(date1);
+ const normalizedDate2 = normalizeDate(date2);
+
+ console.log('日期相似度计算:', {
+ date1原始: date1,
+ date2原始: date2,
+ date1标准化: normalizedDate1,
+ date2标准化: normalizedDate2
+ });
+
+ if (!normalizedDate1 || !normalizedDate2) {
+ console.log('缺少日期信息,返回50%相似度');
+ return 50; // 如果缺少日期信息,给中等相似度
+ }
+
+ // 如果日期完全相同
+ if (normalizedDate1 === normalizedDate2) {
+ console.log('日期完全相同,返回100%相似度');
+ return 100;
+ }
+
+ // 计算日期差异(天数)
+ try {
+ const d1 = new Date(normalizedDate1);
+ const d2 = new Date(normalizedDate2);
+
+ // 检查日期是否有效
+ if (isNaN(d1.getTime()) || isNaN(d2.getTime())) {
+ console.warn('日期解析失败,返回50%相似度');
+ return 50;
+ }
+
+ const diffDays = Math.abs((d1 - d2) / (1000 * 60 * 60 * 24));
+ console.log('日期差异:', diffDays, '天');
+
+ // 如果日期差异在3天内,相似度较高
+ if (diffDays <= 3) {
+ return 80;
+ }
+ // 如果日期差异在7天内,相似度中等
+ else if (diffDays <= 7) {
+ return 60;
+ }
+ // 如果日期差异在30天内,相似度较低
+ else if (diffDays <= 30) {
+ return 40;
+ }
+ // 日期差异较大,相似度很低
+ else {
+ return 20;
+ }
+ } catch (e) {
+ console.warn('日期解析失败:', e);
+ return 50;
+ }
+ },
+
+ // 计算综合文本相似度(包括标题、描述、地点、日期)
+ calculateComprehensiveTextSimilarity: function(item1, item2) {
+ console.log('========== 开始计算综合文本相似度 ==========');
+ console.log('物品1:', {
+ title: item1.title,
+ description: item1.description?.substring(0, 50),
+ location: item1.location,
+ time: item1.time,
+ date: item1.date,
+ publishTime: item1.publishTime,
+ createTime: item1.createTime
+ });
+ console.log('物品2:', {
+ title: item2.title,
+ description: item2.description?.substring(0, 50),
+ location: item2.location,
+ time: item2.time,
+ date: item2.date,
+ publishTime: item2.publishTime,
+ createTime: item2.createTime
+ });
+
+ // 文本相似度(标题+描述,已排除联系人信息)
+ const textSimilarity = this.calculateTextSimilarity(item1, item2);
+
+ // 位置接近度
+ const locationProximity = this.calculateLocationProximity(item1, item2);
+
+ // 日期相似度
+ const dateSimilarity = this.calculateDateSimilarity(item1, item2);
+
+ // 综合文本相似度:文本40%,位置30%,日期30%
+ const comprehensiveTextSimilarity = Math.floor(
+ textSimilarity * 0.4 +
+ locationProximity * 0.3 +
+ dateSimilarity * 0.3
+ );
+
+ console.log('========== 综合文本相似度计算结果 ==========');
+ const isMatch = comprehensiveTextSimilarity >= 50;
+ console.log('综合文本相似度计算:', {
+ textSimilarity: `${textSimilarity}% (权重40%)`,
+ locationProximity: `${locationProximity}% (权重30%)`,
+ dateSimilarity: `${dateSimilarity}% (权重30%)`,
+ comprehensiveTextSimilarity: `${comprehensiveTextSimilarity}%`,
+ isMatch: isMatch ? 'YES' : 'NO'
+ });
+
+ return comprehensiveTextSimilarity;
+ },
+
+ // 检查图片是否完全相同
+ checkImageIdentical: function(item1, item2) {
+ // 如果两个物品都有图片
+ if (item1.images && item1.images.length > 0 &&
+ item2.images && item2.images.length > 0) {
+ const image1 = item1.images[0];
+ const image2 = item2.images[0];
+
+ // 如果图片路径完全相同,认为是同一张图片
+ // 但要注意:如果都是默认图片(/images/lost.png 或 /images/found.png),
+ // 可能是不同物品使用了相同的默认图片,需要结合文本相似度判断
+ if (image1 === image2) {
+ // 检查是否是默认图片
+ const isDefaultImage = image1.includes('/images/lost.png') ||
+ image1.includes('/images/found.png') ||
+ image1.includes('/images/match.png');
+ if (isDefaultImage) {
+ console.log('⚠️ 检测到默认图片,需要结合文本相似度判断:', image1);
+ // 对于默认图片,不直接返回true,让调用方结合文本相似度判断
+ // 但如果是云存储路径相同,则认为是同一张图片
+ if (image1.startsWith('cloud://')) {
+ console.log('✅ 云存储图片路径完全相同,认为是同一张图片');
+ return true;
+ }
+ // 默认图片路径相同,但可能是不同物品,返回false,让调用方结合文本判断
+ return false;
+ }
+ console.log('✅ 图片路径完全相同:', image1);
+ return true;
+ }
+
+ // 如果图片相似度达到100%(特征向量完全相同)
+ // 但也要注意:如果都是默认图片,特征向量可能相同,需要结合文本判断
+ const imageSimilarity = this.calculateMockImageSimilarity(item1, item2);
+ if (imageSimilarity >= 100) {
+ // 检查是否是默认图片
+ const isDefaultImage1 = image1.includes('/images/lost.png') ||
+ image1.includes('/images/found.png') ||
+ image1.includes('/images/match.png');
+ const isDefaultImage2 = image2.includes('/images/lost.png') ||
+ image2.includes('/images/found.png') ||
+ image2.includes('/images/match.png');
+ if (isDefaultImage1 && isDefaultImage2) {
+ console.log('⚠️ 默认图片相似度100%,需要结合文本相似度判断');
+ return false; // 返回false,让调用方结合文本判断
+ }
+ console.log('✅ 图片相似度100%,认为是同一张图片');
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ // 为单个物品查找匹配(不遍历所有用户物品)
+ findMatchesForSingleItem: function(userItem, targetType, callback) {
+ console.log('为单个物品查找匹配:', userItem.title, 'ID:', userItem.id, '目标类型:', targetType);
+
+ // 从云数据库获取所有可比较的物品
+ this.getAllComparableItemsFromCloud((allItems) => {
+ try {
+ // 筛选出与targetType相同类型的候选物品
+ const candidateItems = allItems.filter(item => {
+ const isCorrectType = item.type === targetType;
+ const isNotUserPublished = !item.isUserPublished;
+ const isNotSameItem = item.id !== userItem.id;
+ const hasImages = item.images && item.images.length > 0;
+
+ return isCorrectType && isNotUserPublished && isNotSameItem && hasImages;
+ });
+
+ console.log('候选匹配物品数量:', candidateItems.length);
+
+ if (candidateItems.length === 0) {
+ callback({
+ code: 200,
+ message: 'success',
+ data: { items: [], total: 0 },
+ items: [],
+ hasMore: false
+ });
+ return;
+ }
+
+ // 计算所有候选物品的匹配度
+ const candidateScores = candidateItems.map(matchedItem => {
+ // 计算综合文本相似度
+ const comprehensiveTextSimilarity = this.calculateComprehensiveTextSimilarity(userItem, matchedItem);
+
+ // 如果文本相似度 >= 50%,直接匹配成功
+ if (comprehensiveTextSimilarity >= 50) {
+ return {
+ item: matchedItem,
+ matchDegree: comprehensiveTextSimilarity,
+ imageSimilarity: 0,
+ textSimilarity: comprehensiveTextSimilarity,
+ matchStrategy: 'text_first'
+ };
+ }
+
+ // 检查图片是否完全相同
+ const isImageIdentical = this.checkImageIdentical(userItem, matchedItem);
+ if (isImageIdentical && comprehensiveTextSimilarity >= 30) {
+ const matchDegree = Math.floor(comprehensiveTextSimilarity * 0.3 + 100 * 0.7);
+ return {
+ item: matchedItem,
+ matchDegree: matchDegree,
+ imageSimilarity: 100,
+ textSimilarity: comprehensiveTextSimilarity,
+ matchStrategy: 'image_first'
+ };
+ }
+
+ // 综合匹配
+ const imageSimilarity = this.calculateMockImageSimilarity(userItem, matchedItem);
+ const matchDegree = Math.floor(comprehensiveTextSimilarity * 0.5 + imageSimilarity * 0.5);
+
+ return {
+ item: matchedItem,
+ matchDegree: matchDegree,
+ imageSimilarity: imageSimilarity,
+ textSimilarity: comprehensiveTextSimilarity,
+ matchStrategy: 'comprehensive'
+ };
+ });
+
+ // 按匹配度降序排序
+ candidateScores.sort((a, b) => b.matchDegree - a.matchDegree);
+
+ // 选择匹配度 >= 20% 的物品(最多5个)
+ const selectedScores = candidateScores
+ .filter(score => score.matchDegree >= 20)
+ .slice(0, 5);
+
+ // 转换为匹配结果格式
+ const matchedItems = selectedScores.map(score => ({
+ ...score.item,
+ matchDegree: score.matchDegree,
+ imageSimilarity: score.imageSimilarity,
+ textSimilarity: score.textSimilarity
+ }));
+
+ callback({
+ code: 200,
+ message: 'success',
+ data: { items: matchedItems, total: matchedItems.length },
+ items: matchedItems,
+ hasMore: false
+ });
+ } catch (error) {
+ console.error('为单个物品查找匹配时出错:', error);
+ callback({
+ code: 500,
+ message: '匹配失败',
+ data: { items: [], total: 0 },
+ items: [],
+ hasMore: false
+ });
+ }
+ });
+ },
+
+ // 为新发布的物品查找匹配(只匹配新物品,不遍历所有用户物品)
+ findMatchesForNewItem: function(newItem) {
+ console.log('为新发布的物品查找匹配:', newItem.title, '类型:', newItem.type, 'ID:', newItem.id);
+
+ // 确保新物品已添加到用户发布列表中(如果还没有添加)
+ if (!this.globalData.userPublishedItems ||
+ !this.globalData.userPublishedItems.find(item => item.id === newItem.id)) {
+ console.log('新物品尚未添加到用户发布列表,现在添加');
+ if (!this.globalData.userPublishedItems) {
+ this.globalData.userPublishedItems = [];
+ }
+ this.globalData.userPublishedItems.push(newItem);
+ }
+
+ // 检查是否已经为这个物品创建过匹配消息(避免重复匹配)
+ const existingMessages = this.globalData.pendingClaimMessages || [];
+ const hasExistingMatch = existingMessages.some(msg =>
+ msg.itemId === newItem.id &&
+ (msg.type === 'match' || (msg.type === 'system' && msg.content && msg.content.includes('找到')))
+ );
+
+ if (hasExistingMatch) {
+ console.log('⚠️ 该物品已经匹配过,跳过重复匹配:', newItem.id);
+ return;
+ }
+
+ // 获取相反类型的物品(失物匹配招领,招领匹配失物)
+ const targetType = newItem.type === 'lost' ? 'found' : 'lost';
+
+ console.log('开始查找匹配,目标类型:', targetType, '仅针对新物品:', newItem.id);
+
+ // 只针对新物品查找匹配,而不是调用getSmartMatches(它会匹配所有用户物品)
+ this.findMatchesForSingleItem(newItem, targetType, (res) => {
+ console.log('匹配查找结果:', res);
+
+ if (res.items && res.items.length > 0) {
+ console.log(`找到 ${res.items.length} 个匹配项`);
+
+ // 创建匹配通知
+ const matchCount = res.items.length;
+
+ // 构建匹配项详细信息列表(最多显示前5个)
+ const matchItems = res.items.slice(0, 5).map(item => {
+ // 处理图片路径,确保图片能正确显示
+ const processedItem = this.processItemImages(item);
+ return {
+ id: processedItem.id,
+ title: processedItem.title,
+ description: processedItem.description || '',
+ images: processedItem.images || [],
+ location: processedItem.location || '',
+ matchDegree: processedItem.matchDegree || 0,
+ type: processedItem.type
+ };
+ });
+
+ // 创建匹配通知消息
+ this.createSystemMessage(
+ 'match',
+ '发现匹配项',
+ `系统为您发布的"${newItem.title}"找到了 ${matchCount} 个${targetType === 'found' ? '招领' : '失物'}匹配项,快去查看吧!`,
+ newItem.id,
+ newItem.type,
+ {
+ matchCount: matchCount,
+ matchItems: matchItems, // 包含所有匹配项的详细信息
+ matchItemId: matchItems[0]?.id, // 第一个匹配项ID(用于兼容性)
+ matchItemTitle: matchItems[0]?.title // 第一个匹配项标题(用于兼容性)
+ }
+ );
+
+ // 显示Toast提示
+ this.showMatchNotification(matchCount);
+ } else {
+ console.log('未找到匹配项,但仍创建提示消息');
+ // 即使没有找到匹配,也创建一个提示消息,告知用户匹配已完成
+ this.createSystemMessage(
+ 'system',
+ '匹配完成',
+ `系统已为您发布的"${newItem.title}"完成匹配搜索,暂未发现相关${targetType === 'found' ? '招领' : '失物'}信息。`,
+ newItem.id,
+ newItem.type
+ );
+ }
+ });
+ },
+
+ // 忽略匹配
+ ignoreMatch: function(matchId, callback) {
+ // 保存到已忽略列表
+ if (!this.globalData.ignoredMatches) {
+ this.globalData.ignoredMatches = [];
+ }
+
+ if (!this.globalData.ignoredMatches.includes(matchId)) {
+ this.globalData.ignoredMatches.push(matchId);
+
+ // 持久化存储
+ wx.setStorageSync('ignoredMatches', this.globalData.ignoredMatches);
+ }
+
+ // 模拟API响应
+ setTimeout(() => {
+ callback({
+ code: 200,
+ message: '忽略成功',
+ success: true
+ });
+ }, 300);
+ },
+
+ // 更新用户设置
+ updateSettings: function(newSettings, callback) {
+ this.globalData.settings = { ...this.globalData.settings, ...newSettings };
+
+ // 持久化存储
+ wx.setStorageSync('userSettings', this.globalData.settings);
+
+ if (callback) {
+ callback({
+ code: 200,
+ message: '设置更新成功',
+ success: true
+ });
+ }
+ },
+
+ // 显示匹配通知
+ showMatchNotification: function(count) {
+ // 实际应用中可以使用订阅消息
+ wx.showToast({
+ title: `发现 ${count} 个新匹配`,
+ icon: 'none',
+ duration: 2000
+ });
+ },
+
+ // 创建系统消息
+ createSystemMessage: function(type, title, content, itemId, itemType, extraData) {
+ console.log('创建系统消息:', type, title, content);
+
+ // 初始化消息列表(如果不存在,尝试从本地存储加载)
+ if (!this.globalData.pendingClaimMessages) {
+ try {
+ const storedMessages = wx.getStorageSync('pendingClaimMessages');
+ if (storedMessages && Array.isArray(storedMessages)) {
+ this.globalData.pendingClaimMessages = storedMessages;
+ console.log('从本地存储加载已有消息:', storedMessages.length, '条');
+ } else {
+ this.globalData.pendingClaimMessages = [];
+ console.log('初始化空消息列表');
+ }
+ } catch (e) {
+ console.error('加载消息失败:', e);
+ this.globalData.pendingClaimMessages = [];
+ }
+ }
+
+ // 格式化时间
+ const now = new Date();
+ const timeStr = this.formatMessageTime(now);
+
+ // 创建消息对象
+ const message = {
+ id: 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 10000),
+ type: type, // 'system', 'match', 'claim' 等
+ title: title,
+ content: content,
+ time: timeStr,
+ timestamp: now.getTime(),
+ itemId: itemId || null,
+ itemType: itemType || null,
+ status: 'unread', // 'unread', 'read', 'pending'
+ isRead: false,
+ ...extraData // 允许传入额外数据
+ };
+
+ console.log('创建的消息对象:', message);
+
+ // 添加到消息列表的开头,并按时间倒序排序
+ this.globalData.pendingClaimMessages.unshift(message);
+
+ // 按时间戳倒序排序(最新的在前)
+ this.globalData.pendingClaimMessages.sort((a, b) => {
+ const timeA = a.timestamp || 0;
+ const timeB = b.timestamp || 0;
+ return timeB - timeA; // 倒序
+ });
+
+ // 限制消息数量,最多保留100条
+ if (this.globalData.pendingClaimMessages.length > 100) {
+ this.globalData.pendingClaimMessages = this.globalData.pendingClaimMessages.slice(0, 100);
+ }
+
+ console.log('消息列表当前总数:', this.globalData.pendingClaimMessages.length);
+
+ // 持久化存储
+ try {
+ wx.setStorageSync('pendingClaimMessages', this.globalData.pendingClaimMessages);
+ console.log('消息已保存到本地存储');
+ } catch (e) {
+ console.error('保存消息到本地存储失败:', e);
+ }
+
+ // 更新tabBar角标
+ this.updateTabBarBadge();
+
+ console.log('系统消息创建成功,当前消息总数:', this.globalData.pendingClaimMessages.length);
+ return message;
+ },
+
+ // 格式化消息时间
+ formatMessageTime: function(date) {
+ if (!date || !(date instanceof Date)) {
+ return '刚刚';
+ }
+
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const minutes = Math.floor(diff / 60000);
+ const hours = Math.floor(diff / 3600000);
+ const days = Math.floor(diff / 86400000);
+
+ if (minutes < 1) {
+ return '刚刚';
+ } else if (minutes < 60) {
+ return `${minutes}分钟前`;
+ } else if (hours < 24) {
+ return `${hours}小时前`;
+ } else if (days < 7) {
+ return `${days}天前`;
+ } else {
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ const year = date.getFullYear();
+ const currentYear = now.getFullYear();
+
+ // 如果是今年的,不显示年份
+ if (year === currentYear) {
+ return `${month}月${day}日`;
+ } else {
+ return `${year}年${month}月${day}日`;
+ }
+ }
+ },
+
+ // 更新tabBar角标
+ updateTabBarBadge: function() {
+ if (!this.globalData.pendingClaimMessages) {
+ wx.removeTabBarBadge({ index: 3 });
+ return;
+ }
+
+ // 计算未读消息数量
+ const unreadCount = this.globalData.pendingClaimMessages.filter(msg =>
+ msg.status === 'unread' || msg.status === 'pending' || !msg.isRead
+ ).length;
+
+ if (unreadCount > 0) {
+ wx.setTabBarBadge({
+ index: 3,
+ text: unreadCount > 99 ? '99+' : unreadCount.toString()
+ });
+ } else {
+ wx.removeTabBarBadge({ index: 3 });
+ }
+ },
+
+ // 全局错误处理
+ onError: function(error) {
+ console.error('小程序全局错误:', error);
+ // 可以在这里上报错误到服务器
+ },
+
+ // 页面不存在处理
+ onPageNotFound: function(res) {
+ console.error('页面不存在:', res);
+ wx.redirectTo({
+ url: '/pages/index/index'
+ });
+ },
+
+ // 云存储路径转临时URL的缓存
+ cloudUrlCache: {},
+
+ // 将云存储路径转换为临时URL(异步)
+ convertCloudPathToUrl: function(cloudPath, callback) {
+ if (!cloudPath || !cloudPath.startsWith('cloud://')) {
+ callback(cloudPath);
+ return;
+ }
+
+ // 检查缓存
+ if (this.cloudUrlCache[cloudPath]) {
+ console.log('使用缓存的云存储URL:', this.cloudUrlCache[cloudPath]);
+ callback(this.cloudUrlCache[cloudPath]);
+ return;
+ }
+
+ // 检查是否支持云开发
+ if (!wx.cloud || typeof wx.cloud.getTempFileURL !== 'function') {
+ console.warn('不支持云开发API,无法转换cloud://路径');
+ callback(cloudPath); // 返回原路径,让错误处理机制处理
+ return;
+ }
+
+ // 转换为临时URL
+ wx.cloud.getTempFileURL({
+ fileList: [cloudPath],
+ success: (res) => {
+ if (res.fileList && res.fileList.length > 0 && res.fileList[0].tempFileURL) {
+ const tempUrl = res.fileList[0].tempFileURL;
+ // 缓存结果
+ this.cloudUrlCache[cloudPath] = tempUrl;
+ console.log('云存储路径转换成功:', cloudPath, '->', tempUrl);
+ callback(tempUrl);
+ } else {
+ console.warn('云存储路径转换失败,未返回临时URL:', res);
+ callback(cloudPath); // 返回原路径
+ }
+ },
+ fail: (err) => {
+ console.error('云存储路径转换失败:', err, '路径:', cloudPath);
+ callback(cloudPath); // 返回原路径,让错误处理机制处理
+ }
+ });
+ },
+
+ // 批量转换云存储路径(同步返回,但内部异步处理)
+ processItemImages: function(item) {
+ try {
+ const processedItem = { ...item };
+
+ // 确保images字段存在
+ if (!processedItem.images || !Array.isArray(processedItem.images)) {
+ processedItem.images = [];
+ return processedItem;
+ }
+
+ console.log('app.js - 处理图片前:', JSON.stringify(processedItem.images));
+
+ // 处理图片路径 - 仅做基本清理,保留所有原始路径
+ // 注意:对于cloud://路径,这里先标记,实际转换在detail.js中异步进行
+ processedItem.images = processedItem.images.map(imgPath => {
+ try {
+ console.log('app.js - 处理单张图片:', imgPath, '类型:', typeof imgPath);
+
+ // 确保imgPath是字符串
+ if (!imgPath || typeof imgPath !== 'string') {
+ console.warn('app.js - 图片路径不是字符串类型:', imgPath);
+ return '/images/empty.png';
+ }
+
+ // 最小化清理:只移除首尾的引号和空白字符
+ let cleanPath = imgPath.trim();
+ cleanPath = cleanPath.replace(/^["'`]+|["'`]+$/g, '');
+
+ console.log('app.js - 清理后路径:', cleanPath);
+
+ // 如果清理后为空,使用默认图片
+ if (!cleanPath) {
+ console.warn('app.js - 图片路径为空,使用默认图片');
+ return '/images/empty.png';
+ }
+
+ // 对于cloud://路径,保留原路径(将在detail.js中异步转换)
+ if (cleanPath.startsWith('cloud://')) {
+ console.log('app.js - 检测到cloud://路径,保留原路径用于后续转换:', cleanPath);
+ return cleanPath;
+ }
+
+ // 对于local_image_类型的模拟图片,优化处理逻辑
+ if (cleanPath.includes('local_image_')) {
+ console.log('app.js - 处理local_image_类型图片,原始路径:', cleanPath);
+
+ // 提取实际的图片ID部分,无论路径如何,直接提取local_image_开头的部分
+ let imageId = cleanPath.match(/local_image_[0-9]+_[0-9]+/);
+ imageId = imageId ? imageId[0] : cleanPath.split('/').pop().replace(/[`'"\s]/g, '');
+ console.log('app.js - 提取的图片ID:', imageId);
+
+ // 尝试多种可能的键名来查找图片
+ const possibleKeys = [
+ imageId, // 直接使用提取的ID
+ cleanPath, // 使用完整路径作为键
+ cleanPath.replace(/^\/pages\/publish\//, ''), // 移除/pages/publish/前缀
+ cleanPath.replace(/^[\/\\]?/, '') // 移除开头的斜杠
+ ];
+
+ // 遍历所有可能的键名
+ let actualPath = null;
+ if (this.globalData.localImages) {
+ for (const key of possibleKeys) {
+ if (this.globalData.localImages[key]) {
+ actualPath = this.globalData.localImages[key];
+ console.log('app.js - 找到匹配的图片路径,键名:', key, '->', actualPath);
+ break;
+ }
+ }
+ }
+
+ // 如果找到实际路径,返回它
+ if (actualPath && typeof actualPath === 'string') {
+ // 验证路径是否有效(临时文件路径应该以 wxfile:// 开头或者是http/https)
+ if (actualPath.startsWith('wxfile://') || actualPath.startsWith('http://') || actualPath.startsWith('https://') || actualPath.startsWith('/')) {
+ return actualPath;
+ } else {
+ console.warn('app.js - 找到的路径格式无效,使用默认图片:', actualPath);
+ return '/images/empty.png';
+ }
+ } else {
+ // 如果找不到映射,说明这是旧的无效路径,直接使用默认图片
+ console.warn('app.js - 未找到local_image_路径映射,使用默认图片替代。路径:', cleanPath);
+ return '/images/empty.png';
+ }
+ }
+
+ // 对于所有其他路径,无论格式如何,都直接返回原始清理后的路径
+ // 不再进行任何类型的路径替换或验证
+ console.log('app.js - 直接返回用户上传的图片路径:', cleanPath);
+ return cleanPath;
+ } catch (error) {
+ console.error('app.js - 处理单张图片路径时出错:', error, '图片路径:', imgPath);
+ // 出错时仍然尽量保留原始路径
+ return typeof imgPath === 'string' ? imgPath : '/images/empty.png';
+ }
+ });
+
+ console.log('app.js - 处理图片后:', JSON.stringify(processedItem.images));
+
+ return processedItem;
+ } catch (error) {
+ console.error('app.js - 处理图片路径失败:', error);
+ // 发生严重错误时,尝试保留原始数据结构
+ return item || {};
+ }
+ },
+
+ // 清除缓存数据
+ clearCache: function() {
+ this.globalData.similarityCache = {};
+ this.globalData.itemFeaturesCache = {};
+ console.log('缓存已清除');
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/app.json b/shiwuzhaol22/shiwuzhaol/app.json
new file mode 100644
index 0000000..e262972
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/app.json
@@ -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"
+ ]
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/app.wxss b/shiwuzhaol22/shiwuzhaol/app.wxss
new file mode 100644
index 0000000..06f2f53
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/app.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/cloud_development_guide.md b/shiwuzhaol22/shiwuzhaol/cloud_development_guide.md
new file mode 100644
index 0000000..1666163
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/cloud_development_guide.md
@@ -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. 使用云函数进行复杂的数据处理,减轻前端负担
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/config.json b/shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/config.json
new file mode 100644
index 0000000..a88007d
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/config.json
@@ -0,0 +1,8 @@
+{
+ "permissions": {
+ "openapi": []
+ },
+ "timeout": 60,
+ "memorySize": 512
+}
+
diff --git a/shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/index.js b/shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/index.js
new file mode 100644
index 0000000..85b3f46
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/index.js
@@ -0,0 +1,547 @@
+// 云函数:图片搜索(以图搜图)
+// 功能:接收图片,提取特征,与云数据库中的图片进行相似度匹配
+
+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提取图片特征(通过调用tencentAI云函数)
+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) {
+ console.log('使用腾讯AI提取特征成功,维度:', aiResult.result.features.length);
+ return aiResult.result.features;
+ }
+ } catch (aiError) {
+ console.warn('调用tencentAI云函数失败,使用降级方法:', aiError);
+ }
+
+ // 如果AI调用失败,使用降级方法
+ return extractImageFeaturesFromBuffer(imageBuffer);
+
+ } catch (error) {
+ console.error('腾讯AI特征提取失败,使用降级方法:', error);
+ return extractImageFeaturesFromBuffer(imageBuffer);
+ }
+}
+
+// 从图片Buffer提取特征(改进版)
+function extractImageFeaturesFromBuffer(imageBuffer) {
+ try {
+ // 使用更复杂的特征提取方法
+ // 1. 文件大小特征
+ const fileSize = imageBuffer.length;
+ const normalizedSize = Math.min(fileSize / 2000000, 1); // 假设最大2MB
+
+ // 2. 基于文件内容的哈希特征(使用多个哈希算法)
+ const md5Hash = crypto.createHash('md5').update(imageBuffer).digest('hex');
+ const sha1Hash = crypto.createHash('sha1').update(imageBuffer).digest('hex');
+
+ // 3. 提取文件头信息(图片格式特征)
+ const header = imageBuffer.slice(0, 16);
+ const headerFeatures = [];
+ for (let i = 0; i < 16; i++) {
+ headerFeatures.push(header[i] / 255);
+ }
+
+ // 4. 从哈希生成特征向量(256维)
+ 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);
+ }
+
+ // 5. 文件大小分布特征(32维)
+ 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;
+ }
+
+ // 如果零值超过80%,可能是无效特征向量
+ if (zeroCountA / minLength > 0.8 || zeroCountB / minLength > 0.8) {
+ console.warn(`特征向量零值过多: A=${(zeroCountA/minLength*100).toFixed(1)}%, B=${(zeroCountB/minLength*100).toFixed(1)}%`);
+ return 0;
+ }
+
+ 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);
+
+ // 动态相似度阈值(更严格的阈值,避免误匹配):
+ // - 如果特征维度 >= 512(使用腾讯AI特征),阈值设为0.5(50%),因为AI特征更准确,应该更严格
+ // - 如果特征维度 >= 176(使用改进的特征提取),阈值设为0.6(60%)
+ // - 否则(降级方法),阈值设为0.85(85%),因为降级方法不够准确,需要更高的阈值
+ let threshold = 0.85;
+ if (queryFeatures.length >= 512 && itemFeatures.length >= 512) {
+ threshold = 0.5; // 腾讯AI特征,更准确,但也要严格
+ } else if (queryFeatures.length >= 176 && itemFeatures.length >= 176) {
+ threshold = 0.6; // 改进的特征提取
+ }
+
+ // 如果相似度异常高(>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. 过滤和返回结果
+ // 根据特征类型设置不同的最终过滤阈值:
+ // - 如果使用腾讯AI特征(512维),最终阈值设为50%
+ // - 如果使用改进特征(176维),最终阈值设为60%
+ // - 否则(降级方法),最终阈值设为85%
+ let finalThreshold = 0.85;
+ if (queryFeatures.length >= 512) {
+ finalThreshold = 0.5; // 50%
+ } else if (queryFeatures.length >= 176) {
+ finalThreshold = 0.6; // 60%
+ }
+
+ // 只返回相似度 >= 最终阈值 的结果,最多返回前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);
+ }
+}
+
diff --git a/shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/package.json b/shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/package.json
new file mode 100644
index 0000000..a7f5e2d
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "imageSearch",
+ "version": "1.0.0",
+ "description": "图片搜索云函数",
+ "main": "index.js",
+ "dependencies": {
+ "wx-server-sdk": "~2.6.3"
+ }
+}
+
diff --git a/shiwuzhaol22/shiwuzhaol/cloudfunctions/login/index.js b/shiwuzhaol22/shiwuzhaol/cloudfunctions/login/index.js
new file mode 100644
index 0000000..8b785ac
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/cloudfunctions/login/index.js
@@ -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,
+ };
+};
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/cloudfunctions/login/package.json b/shiwuzhaol22/shiwuzhaol/cloudfunctions/login/package.json
new file mode 100644
index 0000000..052badf
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/cloudfunctions/login/package.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/config.json b/shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/config.json
new file mode 100644
index 0000000..eaa6dae
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/config.json
@@ -0,0 +1,6 @@
+{
+ "permissions": {
+ "openapi": []
+ }
+}
+
diff --git a/shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/index.js b/shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/index.js
new file mode 100644
index 0000000..e08dff7
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/index.js
@@ -0,0 +1,432 @@
+// 腾讯云图像识别云函数
+// 用于调用腾讯云图像识别API(API 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 // 标记为降级方案
+ };
+ }
+};
+
diff --git a/shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/package.json b/shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/package.json
new file mode 100644
index 0000000..1f7f76e
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/package.json
@@ -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"
+ }
+}
+
diff --git a/shiwuzhaol22/shiwuzhaol/images/app_logo.png b/shiwuzhaol22/shiwuzhaol/images/app_logo.png
new file mode 100644
index 0000000..3b663d3
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/app_logo.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/copy.svg b/shiwuzhaol22/shiwuzhaol/images/copy.svg
new file mode 100644
index 0000000..8ecbf85
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/copy.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/default_avatar.svg b/shiwuzhaol22/shiwuzhaol/images/default_avatar.svg
new file mode 100644
index 0000000..6a3489e
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/default_avatar.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/developer.png b/shiwuzhaol22/shiwuzhaol/images/developer.png
new file mode 100644
index 0000000..0a59191
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/developer.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/edit.svg b/shiwuzhaol22/shiwuzhaol/images/edit.svg
new file mode 100644
index 0000000..1621d77
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/edit.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/email.png b/shiwuzhaol22/shiwuzhaol/images/email.png
new file mode 100644
index 0000000..e47c5a7
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/email.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/empty.png b/shiwuzhaol22/shiwuzhaol/images/empty.png
new file mode 100644
index 0000000..4adce87
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/empty.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/found.png b/shiwuzhaol22/shiwuzhaol/images/found.png
new file mode 100644
index 0000000..132a764
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/found.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/home.png b/shiwuzhaol22/shiwuzhaol/images/home.png
new file mode 100644
index 0000000..7d1b8b5
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/home.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/home_new.png b/shiwuzhaol22/shiwuzhaol/images/home_new.png
new file mode 100644
index 0000000..9c3bd3f
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/home_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/home_selected.png b/shiwuzhaol22/shiwuzhaol/images/home_selected.png
new file mode 100644
index 0000000..a3128c5
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/home_selected.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/home_selected_new.png b/shiwuzhaol22/shiwuzhaol/images/home_selected_new.png
new file mode 100644
index 0000000..9c3bd3f
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/home_selected_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/lost.png b/shiwuzhaol22/shiwuzhaol/images/lost.png
new file mode 100644
index 0000000..3593f14
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/lost.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/match.png b/shiwuzhaol22/shiwuzhaol/images/match.png
new file mode 100644
index 0000000..658b17f
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/match.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/match.svg b/shiwuzhaol22/shiwuzhaol/images/match.svg
new file mode 100644
index 0000000..a6704e4
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/match.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/message.png b/shiwuzhaol22/shiwuzhaol/images/message.png
new file mode 100644
index 0000000..d4e2c42
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/message.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/message.svg b/shiwuzhaol22/shiwuzhaol/images/message.svg
new file mode 100644
index 0000000..3681fcf
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/message.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/message_new.png b/shiwuzhaol22/shiwuzhaol/images/message_new.png
new file mode 100644
index 0000000..d4c1d4f
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/message_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/message_selected.png b/shiwuzhaol22/shiwuzhaol/images/message_selected.png
new file mode 100644
index 0000000..184ae6e
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/message_selected.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/message_selected_new.png b/shiwuzhaol22/shiwuzhaol/images/message_selected_new.png
new file mode 100644
index 0000000..d4c1d4f
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/message_selected_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/no_match.svg b/shiwuzhaol22/shiwuzhaol/images/no_match.svg
new file mode 100644
index 0000000..91ed40a
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/no_match.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/no_message.svg b/shiwuzhaol22/shiwuzhaol/images/no_message.svg
new file mode 100644
index 0000000..df67e88
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/no_message.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/phone.svg b/shiwuzhaol22/shiwuzhaol/images/phone.svg
new file mode 100644
index 0000000..919db83
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/phone.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/publish.png b/shiwuzhaol22/shiwuzhaol/images/publish.png
new file mode 100644
index 0000000..60e3da4
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/publish.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/publish_new.png b/shiwuzhaol22/shiwuzhaol/images/publish_new.png
new file mode 100644
index 0000000..067c3ce
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/publish_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/publish_selected.png b/shiwuzhaol22/shiwuzhaol/images/publish_selected.png
new file mode 100644
index 0000000..ea31c85
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/publish_selected.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/publish_selected_new.png b/shiwuzhaol22/shiwuzhaol/images/publish_selected_new.png
new file mode 100644
index 0000000..067c3ce
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/publish_selected_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/search.png b/shiwuzhaol22/shiwuzhaol/images/search.png
new file mode 100644
index 0000000..913cda8
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/search.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/search_new.png b/shiwuzhaol22/shiwuzhaol/images/search_new.png
new file mode 100644
index 0000000..99a58fa
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/search_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/search_selected.png b/shiwuzhaol22/shiwuzhaol/images/search_selected.png
new file mode 100644
index 0000000..3bbdaa5
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/search_selected.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/search_selected_new.png b/shiwuzhaol22/shiwuzhaol/images/search_selected_new.png
new file mode 100644
index 0000000..99a58fa
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/search_selected_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/settings.svg b/shiwuzhaol22/shiwuzhaol/images/settings.svg
new file mode 100644
index 0000000..053588d
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/settings.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/system.svg b/shiwuzhaol22/shiwuzhaol/images/system.svg
new file mode 100644
index 0000000..a470ffa
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/system.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/user.png b/shiwuzhaol22/shiwuzhaol/images/user.png
new file mode 100644
index 0000000..d42c790
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/user.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/user_new.png b/shiwuzhaol22/shiwuzhaol/images/user_new.png
new file mode 100644
index 0000000..55f1c3b
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/user_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/user_selected.png b/shiwuzhaol22/shiwuzhaol/images/user_selected.png
new file mode 100644
index 0000000..e8a4a82
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/user_selected.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/images/user_selected_new.png b/shiwuzhaol22/shiwuzhaol/images/user_selected_new.png
new file mode 100644
index 0000000..55f1c3b
Binary files /dev/null and b/shiwuzhaol22/shiwuzhaol/images/user_selected_new.png differ
diff --git a/shiwuzhaol22/shiwuzhaol/images/website.png b/shiwuzhaol22/shiwuzhaol/images/website.png
new file mode 100644
index 0000000..94a62ff
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/images/website.png
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/about/about.js b/shiwuzhaol22/shiwuzhaol/pages/about/about.js
new file mode 100644
index 0000000..7c21d3a
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/about/about.js
@@ -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'
+ };
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/about/about.wxml b/shiwuzhaol22/shiwuzhaol/pages/about/about.wxml
new file mode 100644
index 0000000..b6fd7b9
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/about/about.wxml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+ {{appInfo.name}}
+ 版本 {{appInfo.version}}
+ {{appInfo.description}}
+
+
+
+
+ 功能介绍
+
+
+
+ 发布失物信息
+
+
+
+ 发布招领信息
+
+
+
+ 搜索物品信息
+
+
+
+ 智能匹配物品
+
+
+
+ 消息通知提醒
+
+
+
+
+
+
+ 联系我们
+
+
+
+ 官方网站
+ →
+
+
+
+ {{appInfo.contactEmail}}
+ →
+
+
+
+ {{appInfo.developer}}
+
+
+
+
+
+
+ 更新日志
+
+
+ {{item}}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/about/about.wxss b/shiwuzhaol22/shiwuzhaol/pages/about/about.wxss
new file mode 100644
index 0000000..293bea8
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/about/about.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.js b/shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.js
new file mode 100644
index 0000000..6ceebcc
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.js
@@ -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
+ });
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.wxml b/shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.wxml
new file mode 100644
index 0000000..1c2e0b9
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.wxml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+ {{agreementContent}}
+
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.wxss b/shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.wxss
new file mode 100644
index 0000000..3c2e000
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/claim/claim.js b/shiwuzhaol22/shiwuzhaol/pages/claim/claim.js
new file mode 100644
index 0000000..4c51348
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/claim/claim.js
@@ -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();
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/claim/claim.wxml b/shiwuzhaol22/shiwuzhaol/pages/claim/claim.wxml
new file mode 100644
index 0000000..58070b6
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/claim/claim.wxml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+ 全部
+
+
+ 失物
+
+
+ 招领
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.itemTitle}}
+ {{item.itemDesc}}
+ {{item.itemType === 'lost' ? '失物' : '招领'}}
+
+
+
+
+
+ 认领理由:
+ {{item.reason}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无认领请求
+ 用户提交认领请求后,将显示在此处
+
+
+
+
+ 加载中...
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/claim/claim.wxss b/shiwuzhaol22/shiwuzhaol/pages/claim/claim.wxss
new file mode 100644
index 0000000..078961d
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/claim/claim.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/detail/detail.js b/shiwuzhaol22/shiwuzhaol/pages/detail/detail.js
new file mode 100644
index 0000000..9d07eea
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/detail/detail.js
@@ -0,0 +1,924 @@
+// 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() {
+ if (this.data.claimStatus === 'claimed') {
+ wx.showToast({
+ title: '该物品已被认领',
+ icon: 'none'
+ });
+ return;
+ }
+
+ const { itemType } = this.data;
+ const isLostItem = itemType === 'lost';
+
+ wx.showModal({
+ title: isLostItem ? '确认捡到物品' : '确认认领',
+ content: isLostItem ? '请确认您捡到了该物品,我们将安排您与失主联系。' : '请确认您是物品的主人或知道物品的相关信息,认领后我们将安排您与发布者联系。',
+ success: (res) => {
+ if (res.confirm) {
+ this.submitClaim();
+ }
+ }
+ });
+ },
+
+ // 提交认领请求
+ 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();
+ }
+ }
+ });
+ },
+
+ // 提交删除请求
+ 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;
+ }
+
+ wx.showToast({
+ title: '删除成功',
+ icon: 'success',
+ complete: () => {
+ // 返回到首页
+ wx.switchTab({
+ url: '/pages/index/index'
+ });
+ }
+ });
+ } catch (err) {
+ console.error('删除物品失败', err);
+ wx.showToast({
+ title: '删除失败,请重试',
+ icon: 'none'
+ });
+ } finally {
+ wx.hideLoading();
+ }
+ }, 1000);
+ },
+
+ // 复制联系电话
+ copyPhone: function() {
+ const phoneNumber = this.data.itemDetail.contactPhone;
+
+ wx.setClipboardData({
+ data: phoneNumber,
+ success: () => {
+ wx.showToast({
+ title: '电话号码已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/detail/detail.wxml b/shiwuzhaol22/shiwuzhaol/pages/detail/detail.wxml
new file mode 100644
index 0000000..9181430
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/detail/detail.wxml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{currentImageIndex + 1}}/{{itemDetail.images.length}}
+
+
+
+
+ {{itemType === 'lost' ? '失物信息' : '招领信息'}}
+
+
+
+
+ 已被认领
+
+
+ 认领审核中
+
+
+
+
+
+
+ {{itemDetail.title}}
+
+
+
+
+ 详细描述
+ {{itemDetail.description}}
+
+
+
+
+ {{itemType === 'lost' ? '丢失' : '捡到'}}信息
+
+
+ {{itemType === 'lost' ? '丢失时间:' : '捡到时间:'}}{{itemDetail.time}}
+
+
+
+ {{itemType === 'lost' ? '丢失地点:' : '捡到地点:'}}{{itemDetail.location}}
+
+
+
+
+
+ 联系方式
+
+
+ {{itemDetail.contactName}}
+
+
+
+ {{itemDetail.contactPhone}}
+
+
+
+
+
+
+ 发布时间:{{itemDetail.publishTime}}
+
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/detail/detail.wxss b/shiwuzhaol22/shiwuzhaol/pages/detail/detail.wxss
new file mode 100644
index 0000000..2647f59
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/detail/detail.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/index/index.js b/shiwuzhaol22/shiwuzhaol/pages/index/index.js
new file mode 100644
index 0000000..d91ce64
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/index/index.js
@@ -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('无法确定物品类型,默认使用 lost,itemId:', 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的延迟,模拟网络请求时间
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/index/index.wxml b/shiwuzhaol22/shiwuzhaol/pages/index/index.wxml
new file mode 100644
index 0000000..61a88e9
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/index/index.wxml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ 失物
+
+
+
+ 招领
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.description}}
+
+ {{item.lostTime}}
+ {{item.location}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.description}}
+
+ {{item.foundTime}}
+ {{item.location}}
+
+
+
+
+
+
+
+
+
+ {{activeTab === 'lost' ? '暂无失物信息' : '暂无招领信息'}}
+
+
+
+
+ 加载中...
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/index/index.wxss b/shiwuzhaol22/shiwuzhaol/pages/index/index.wxss
new file mode 100644
index 0000000..3a27a37
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/index/index.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/login/login.js b/shiwuzhaol22/shiwuzhaol/pages/login/login.js
new file mode 100644
index 0000000..cede740
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/login/login.js
@@ -0,0 +1,141 @@
+// pages/login/login.js
+Page({
+ data: {
+ canIUseGetUserProfile: false,
+ userInfo: null,
+ hasUserInfo: false
+ },
+
+ 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.canIUseGetUserProfile) {
+ // 使用新版接口
+ this.getUserProfile();
+ } else {
+ // 使用旧版接口
+ this.onGetUserInfo();
+ }
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/login/login.wxml b/shiwuzhaol22/shiwuzhaol/pages/login/login.wxml
new file mode 100644
index 0000000..807f0a6
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/login/login.wxml
@@ -0,0 +1,33 @@
+
+
+
+
+ 失物招领
+ 让每一件失物都能找到回家的路
+
+
+
+
+
+
+
+
+
+ 登录即表示您同意
+ 《隐私政策》
+ 和
+ 《用户协议》
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/login/login.wxss b/shiwuzhaol22/shiwuzhaol/pages/login/login.wxss
new file mode 100644
index 0000000..f64632c
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/login/login.wxss
@@ -0,0 +1,71 @@
+/* pages/login/login.wxss */
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ background-color: #f5f5f5;
+ padding: 40rpx;
+ box-sizing: border-box;
+}
+
+.logo-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ margin-bottom: 60rpx;
+}
+
+.logo {
+ width: 240rpx;
+ height: 240rpx;
+ margin-bottom: 30rpx;
+}
+
+.app-name {
+ font-size: 48rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 16rpx;
+}
+
+.app-desc {
+ font-size: 28rpx;
+ color: #666;
+}
+
+.login-container {
+ width: 100%;
+ margin-bottom: 60rpx;
+}
+
+.login-btn {
+ width: 100%;
+ height: 96rpx;
+ background-color: #07C160;
+ color: #fff;
+ border-radius: 48rpx;
+ font-size: 32rpx;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ line-height: 96rpx;
+}
+
+.wechat-icon {
+ width: 48rpx;
+ height: 48rpx;
+ margin-right: 16rpx;
+}
+
+.agreement {
+ font-size: 24rpx;
+ color: #999;
+ text-align: center;
+ margin-bottom: 40rpx;
+}
+
+.link {
+ color: #2196F3;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/match/match.js b/shiwuzhaol22/shiwuzhaol/pages/match/match.js
new file mode 100644
index 0000000..1efcc38
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/match/match.js
@@ -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'
+ });
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/match/match.wxml b/shiwuzhaol22/shiwuzhaol/pages/match/match.wxml
new file mode 100644
index 0000000..a8ec5d0
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/match/match.wxml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+ 匹配到的失物
+
+
+ 匹配到的招领
+
+
+
+
+
+
+
+
+ 匹配度:{{item.matchDegree}}%
+
+
+
+
+
+
+ {{item.title}}
+ {{item.description}}
+
+ {{item.time}}
+ {{item.location}}
+
+ {{item.type === 'lost' ? '失物' : '招领'}}
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无匹配的物品
+ 系统会根据您发布的信息自动匹配相关物品
+
+
+
+
+ 加载中...
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/match/match.wxss b/shiwuzhaol22/shiwuzhaol/pages/match/match.wxss
new file mode 100644
index 0000000..23df437
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/match/match.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/message/message.js b/shiwuzhaol22/shiwuzhaol/pages/message/message.js
new file mode 100644
index 0000000..197f9a8
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/message/message.js
@@ -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
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/message/message.wxml b/shiwuzhaol22/shiwuzhaol/pages/message/message.wxml
new file mode 100644
index 0000000..24027c2
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/message/message.wxml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+ 全部消息
+ {{unreadCount}}
+
+
+ 未读消息
+ {{unreadCount}}
+
+
+ 系统消息
+
+
+ 匹配通知
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.content}}
+
+
+
+
+
+
+ {{matchItem.title}}
+ {{matchItem.description}}
+
+ {{matchItem.location}}
+ 匹配度: {{matchItem.matchDegree}}%
+
+
+
+
+ 还有 {{item.matchCount - item.matchItems.length}} 个匹配项,点击查看全部
+
+
+
+
+ {{item.itemType === 'lost' ? '失物' : '招领'}}
+ 匹配{{item.matchCount}}项
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+ 暂无消息
+
+
+
+
+
+ 加载中...
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/message/message.wxss b/shiwuzhaol22/shiwuzhaol/pages/message/message.wxss
new file mode 100644
index 0000000..02ab3d6
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/message/message.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.js b/shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.js
new file mode 100644
index 0000000..3c246db
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.js
@@ -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
+ });
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.wxml b/shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.wxml
new file mode 100644
index 0000000..9c3cadf
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.wxml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+ {{privacyContent}}
+
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.wxss b/shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.wxss
new file mode 100644
index 0000000..9dc7b2d
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/publish/publish.js b/shiwuzhaol22/shiwuzhaol/pages/publish/publish.js
new file mode 100644
index 0000000..19f1ac5
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/publish/publish.js
@@ -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;
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxml b/shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxml
new file mode 100644
index 0000000..c0993b0
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+ 发布失物
+
+
+ 发布招领
+
+
+
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxs b/shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxs
new file mode 100644
index 0000000..92de515
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxs
@@ -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
+};
+
diff --git a/shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxss b/shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxss
new file mode 100644
index 0000000..87ea640
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/search/search.js b/shiwuzhaol22/shiwuzhaol/pages/search/search.js
new file mode 100644
index 0000000..6a700b3
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/search/search.js
@@ -0,0 +1,632 @@
+// 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 sortedResults = results.sort((a, b) => {
+ return new Date(b.publishTime || b.createTime) - new Date(a.publishTime || a.createTime);
+ });
+
+ // 处理每个搜索结果项,确保有正确的图片和发布者标识
+ const processedResults = sortedResults.map(item => {
+ // 确保每个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,
+ // 保留相似度字段(如果存在)
+ similarity: item.similarity !== undefined ? item.similarity : (item.similarityValue !== undefined ? (item.similarityValue * 100).toFixed(1) + '%' : undefined)
+ };
+ });
+
+ console.log('处理后的搜索结果,包含其他用户物品:', processedResults.some(item => !item.isUserPublished));
+
+ // 转换所有cloud://路径为临时URL
+ this.convertAllCloudImages(processedResults, (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
+ });
+ }
+ }
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/search/search.wxml b/shiwuzhaol22/shiwuzhaol/pages/search/search.wxml
new file mode 100644
index 0000000..433d29c
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/search/search.wxml
@@ -0,0 +1,91 @@
+
+
+
+
+
+ 文字搜索
+
+
+ AI图片搜索
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 点击上传图片
+ 支持相册选择或拍照
+
+
+ ×
+
+
+
+
+
+
+
+ 💡 提示:上传清晰的物品图片,系统将自动匹配相似度高的失物/招领信息
+
+
+ {{searchProgress}}
+
+
+
+
+
+
+
+
+
+
+ {{item.title}}
+ {{item.description}}
+
+ {{item.time}}
+ {{item.location}}
+
+
+ {{item.type === 'lost' ? '失物' : '招领'}}
+
+
+ 相似度: {{item.similarity}}
+
+
+
+
+
+
+
+
+
+
+ {{searchType === 'text' ? '请输入关键词进行搜索' : '请上传图片进行搜索'}}
+
+
+
+
+ 搜索中...
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/search/search.wxss b/shiwuzhaol22/shiwuzhaol/pages/search/search.wxss
new file mode 100644
index 0000000..66b610d
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/search/search.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/settings/settings.js b/shiwuzhaol22/shiwuzhaol/pages/settings/settings.js
new file mode 100644
index 0000000..949edbb
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/settings/settings.js
@@ -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'
+ });
+ }
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/settings/settings.wxml b/shiwuzhaol22/shiwuzhaol/pages/settings/settings.wxml
new file mode 100644
index 0000000..c6e78e0
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/settings/settings.wxml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+ 个人设置
+
+ 编辑个人资料
+
+ →
+
+
+
+
+
+
+ 功能设置
+
+
+ 接收通知
+
+
+
+
+ 智能匹配
+
+
+
+
+ 位置权限
+
+
+
+
+
+
+ 云开发环境
+
+
+ 环境状态
+
+ {{cloudStatusText}}
+
+
+
+
+ 环境ID
+ {{cloudEnvId}}
+
+
+
+
+
+
+
+ {{cloudTestResult}}
+
+
+
+
+
+ 隐私设置
+
+ 隐私政策
+
+ →
+
+
+
+ 用户协议
+
+ →
+
+
+
+
+
+
+ 其他
+
+ 清除缓存
+
+ →
+
+
+
+ 关于我们
+
+ →
+
+
+
+
+
+
+ 当前版本:v{{appVersion}}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/settings/settings.wxss b/shiwuzhaol22/shiwuzhaol/pages/settings/settings.wxss
new file mode 100644
index 0000000..0ba441d
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/settings/settings.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/user/user.js b/shiwuzhaol22/shiwuzhaol/pages/user/user.js
new file mode 100644
index 0000000..5ea7166
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/user/user.js
@@ -0,0 +1,478 @@
+// 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 });
+
+ // 使用本地模拟数据代替网络请求
+ setTimeout(() => {
+ // 从全局数据中获取用户发布的物品
+ let userItems = [];
+ const app = getApp();
+
+ if (app.globalData && app.globalData.userPublishedItems) {
+ userItems = app.globalData.userPublishedItems;
+ } else {
+ // 如果没有用户物品,生成一些模拟数据
+ userItems = this.generateMockUserItems();
+ }
+
+ // 分页处理
+ const pageSize = 10;
+ const startIndex = (this.data.pageNum - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const newItems = userItems.slice(startIndex, endIndex);
+
+ if (this.data.pageNum === 1) {
+ this.setData({
+ myItems: newItems,
+ pageNum: 2,
+ hasMore: newItems.length === 10
+ });
+ } else {
+ this.setData({
+ myItems: [...this.data.myItems, ...newItems],
+ pageNum: this.data.pageNum + 1,
+ hasMore: newItems.length === 10
+ });
+ }
+
+ this.setData({ loading: false });
+ }, 600);
+ },
+
+ // 生成模拟的用户发布物品数据
+ generateMockUserItems: function() {
+ const mockItems = [
+ {
+ id: 'mock_user_1',
+ type: 'lost',
+ title: '丢失的钱包',
+ description: '黑色钱包,内有身份证和银行卡',
+ location: '图书馆',
+ time: new Date(Date.now() - 86400000).toISOString().split('T')[0],
+ images: ['/images/lost.png'],
+ isUserPublished: true,
+ status: 'pending'
+ },
+ {
+ id: 'mock_user_2',
+ type: 'found',
+ title: '捡到的钥匙',
+ description: '一串钥匙,带有学校标志的钥匙扣',
+ location: '教学楼A区',
+ time: new Date(Date.now() - 172800000).toISOString().split('T')[0],
+ images: ['/images/found.png'],
+ isUserPublished: true,
+ status: 'pending'
+ },
+ {
+ id: 'mock_user_3',
+ type: 'lost',
+ title: '蓝牙耳机',
+ description: '白色AirPods,在操场丢失',
+ location: '操场',
+ time: new Date(Date.now() - 259200000).toISOString().split('T')[0],
+ images: ['/images/search.png'],
+ isUserPublished: true,
+ status: 'pending'
+ }
+ ];
+
+ // 将模拟数据保存到全局,以便智能匹配功能使用
+ const app = getApp();
+ if (!app.globalData) app.globalData = {};
+ app.globalData.userPublishedItems = mockItems;
+
+ return mockItems;
+ },
+
+ // 加载我的收藏
+ 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();
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/user/user.wxml b/shiwuzhaol22/shiwuzhaol/pages/user/user.wxml
new file mode 100644
index 0000000..c86c8bf
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/user/user.wxml
@@ -0,0 +1,112 @@
+
+
+
+
+
+ {{userInfo.nickName || '游客用户'}}
+ ID: {{userInfo.openId || '未登录'}}
+
+
+
+
+
+
+
+
+
+ 我发布的
+
+
+ 我的收藏
+
+
+
+
+
+
+
+
+
+
+
+ {{item.type === 'lost' ? '丢失' : '捡到'}}
+ {{item.title}}
+
+ {{item.location}}
+ {{item.time}}
+
+ ✓ 已匹配
+
+
+
+
+
+ 删除
+
+
+
+
+
+
+ 您还没有发布任何物品
+
+
+
+ 加载中...
+
+
+ 没有更多了
+
+
+
+
+
+
+
+
+
+
+
+ {{item.type === 'lost' ? '丢失' : '捡到'}}
+ {{item.title}}
+
+ {{item.location}}
+ {{item.time}}
+
+
+
+
+ 取消收藏
+
+
+
+
+
+
+ 您还没有收藏任何物品
+
+
+
+ 加载中...
+
+
+ 没有更多了
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/user/user.wxss b/shiwuzhaol22/shiwuzhaol/pages/user/user.wxss
new file mode 100644
index 0000000..5429cff
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/user/user.wxss
@@ -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;
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/webview/webview.js b/shiwuzhaol22/shiwuzhaol/pages/webview/webview.js
new file mode 100644
index 0000000..120ff2e
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/webview/webview.js
@@ -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: ''
+ };
+ }
+});
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/webview/webview.wxml b/shiwuzhaol22/shiwuzhaol/pages/webview/webview.wxml
new file mode 100644
index 0000000..461809c
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/webview/webview.wxml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/pages/webview/webview.wxss b/shiwuzhaol22/shiwuzhaol/pages/webview/webview.wxss
new file mode 100644
index 0000000..f93f10f
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/pages/webview/webview.wxss
@@ -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);
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/project.config.json b/shiwuzhaol22/shiwuzhaol/project.config.json
new file mode 100644
index 0000000..b454326
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/project.config.json
@@ -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": {}
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/project.private.config.json b/shiwuzhaol22/shiwuzhaol/project.private.config.json
new file mode 100644
index 0000000..e3755e1
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/project.private.config.json
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/shiwuzhaol22/shiwuzhaol/utils/md5.js b/shiwuzhaol22/shiwuzhaol/utils/md5.js
new file mode 100644
index 0000000..e8386e0
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/utils/md5.js
@@ -0,0 +1,223 @@
+/**
+ * MD5加密工具(适用于微信小程序)
+ * 基于:https://github.com/blueimp/JavaScript-MD5
+ */
+
+(function (factory) {
+ if (typeof module === 'object' && typeof module.exports === 'object') {
+ module.exports = factory();
+ } else {
+ window.md5 = factory();
+ }
+}(function () {
+ 'use strict';
+
+ /**
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+ function safeAdd(x, y) {
+ var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+ return (msw << 16) | (lsw & 0xFFFF);
+ }
+
+ /**
+ * Bitwise rotate a 32-bit number to the left.
+ */
+ function bitRotateLeft(num, cnt) {
+ return (num << cnt) | (num >>> (32 - cnt));
+ }
+
+ /**
+ * These functions implement the four basic operations the algorithm uses.
+ */
+ 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);
+ }
+
+ /**
+ * Calculate the MD5 of an array of little-endian words, and a bit length.
+ */
+ 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];
+ }
+
+ /**
+ * Convert an array of little-endian words to a string
+ */
+ 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;
+ }
+
+ /**
+ * Convert a raw string to an array of little-endian words
+ * Characters >255 have their high-byte silently ignored.
+ */
+ 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;
+ }
+
+ /**
+ * Calculate the MD5 of a raw string
+ */
+ function rstrMD5(s) {
+ return binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
+ }
+
+ /**
+ * Convert a raw string to a hex string
+ */
+ 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;
+ }
+
+ /**
+ * Encode a string as UTF-8
+ */
+ function str2rstrUTF8(input) {
+ return unescape(encodeURIComponent(input));
+ }
+
+ /**
+ * Take string arguments and return either raw or hex encoded string
+ */
+ function rawMD5(s) {
+ return rstrMD5(str2rstrUTF8(s));
+ }
+ function hexMD5(s) {
+ return rstr2hex(rawMD5(s));
+ }
+
+ return hexMD5;
+}));
+
diff --git a/shiwuzhaol22/shiwuzhaol/utils/mobilenetFeatureExtractor.js b/shiwuzhaol22/shiwuzhaol/utils/mobilenetFeatureExtractor.js
new file mode 100644
index 0000000..3166ac5
--- /dev/null
+++ b/shiwuzhaol22/shiwuzhaol/utils/mobilenetFeatureExtractor.js
@@ -0,0 +1,205 @@
+/**
+ * MobileNet特征提取器(开源模型)
+ * 使用TensorFlow.js + MobileNet模型提取图片特征
+ *
+ * 注意:小程序需要使用@tensorflow/tfjs-platform-miniprogram
+ */
+
+// 模型缓存
+let model = null;
+let modelLoading = false;
+let modelLoadPromise = null;
+
+/**
+ * 加载MobileNet模型
+ * 模型可以从CDN加载,避免增加小程序包体积
+ */
+async function loadModel() {
+ // 如果模型已加载,直接返回
+ if (model) {
+ return model;
+ }
+
+ // 如果正在加载,等待加载完成
+ if (modelLoading && modelLoadPromise) {
+ return modelLoadPromise;
+ }
+
+ // 开始加载模型
+ modelLoading = true;
+ modelLoadPromise = new Promise(async (resolve, reject) => {
+ try {
+ console.log('开始加载MobileNet模型...');
+
+ // 方案1:使用CDN加载(推荐,不增加包体积)
+ // 注意:需要在小程序后台配置downloadFile合法域名
+ const modelUrl = 'https://storage.googleapis.com/tfjs-models/tfjs/mobilenet_v2_1.0_224/model.json';
+
+ // 方案2:如果CDN不可用,可以使用本地模型文件
+ // const modelUrl = '/models/mobilenet/model.json';
+
+ // 检查是否支持TensorFlow.js
+ if (typeof tf === 'undefined') {
+ console.warn('TensorFlow.js未加载,请先引入tf.min.js');
+ console.warn('下载地址: https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js');
+ reject(new Error('TensorFlow.js未加载'));
+ return;
+ }
+
+ // 加载模型
+ model = await tf.loadLayersModel(modelUrl);
+ console.log('MobileNet模型加载成功');
+
+ modelLoading = false;
+ resolve(model);
+ } catch (error) {
+ console.error('MobileNet模型加载失败:', error);
+ modelLoading = false;
+ modelLoadPromise = null;
+ reject(error);
+ }
+ });
+
+ return modelLoadPromise;
+}
+
+/**
+ * 从图片路径提取特征向量
+ * @param {string} imagePath - 图片路径
+ * @returns {Promise} 特征向量(1000维)
+ */
+async function extractFeatures(imagePath) {
+ try {
+ // 加载模型
+ const mobilenetModel = await loadModel();
+
+ // 加载并预处理图片
+ const imgTensor = await loadAndPreprocessImage(imagePath);
+
+ // 提取特征(使用中间层输出,而不是分类结果)
+ // MobileNet的倒数第二层输出是1000维特征向量
+ const prediction = mobilenetModel.predict(imgTensor);
+
+ // 获取特征向量
+ const features = await prediction.data();
+
+ // 清理Tensor(释放内存)
+ imgTensor.dispose();
+ prediction.dispose();
+
+ // 转换为数组并归一化
+ const featuresArray = Array.from(features);
+ const normalizedFeatures = normalizeFeatures(featuresArray);
+
+ console.log('MobileNet特征提取成功,维度:', normalizedFeatures.length);
+ return normalizedFeatures;
+ } catch (error) {
+ console.error('MobileNet特征提取失败:', error);
+ throw error;
+ }
+}
+
+/**
+ * 加载并预处理图片
+ * @param {string} imagePath - 图片路径
+ * @returns {Promise} 预处理后的图片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} features - 原始特征向量
+ * @returns {Array} 归一化后的特征向量
+ */
+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
+};
+