From cb5346d1c895b0319aed6e737a11559582aecc52 Mon Sep 17 00:00:00 2001 From: 12 <2749900757@qq.com> Date: Fri, 28 Nov 2025 18:57:05 +0800 Subject: [PATCH] 1 --- shiwuzhaol22/README.md | 136 + shiwuzhaol22/project.config.json | 39 + shiwuzhaol22/project.private.config.json | 22 + shiwuzhaol22/shiwuzhaol/app.js | 5619 +++++++++++++++++ shiwuzhaol22/shiwuzhaol/app.json | 77 + shiwuzhaol22/shiwuzhaol/app.wxss | 216 + .../shiwuzhaol/cloud_development_guide.md | 127 + .../cloudfunctions/imageSearch/config.json | 8 + .../cloudfunctions/imageSearch/index.js | 547 ++ .../cloudfunctions/imageSearch/package.json | 10 + .../shiwuzhaol/cloudfunctions/login/index.js | 25 + .../cloudfunctions/login/package.json | 14 + .../cloudfunctions/tencentAI/config.json | 6 + .../cloudfunctions/tencentAI/index.js | 432 ++ .../cloudfunctions/tencentAI/package.json | 12 + shiwuzhaol22/shiwuzhaol/images/app_logo.png | 1 + shiwuzhaol22/shiwuzhaol/images/copy.svg | 4 + .../shiwuzhaol/images/default_avatar.svg | 4 + shiwuzhaol22/shiwuzhaol/images/developer.png | 1 + shiwuzhaol22/shiwuzhaol/images/edit.svg | 4 + shiwuzhaol22/shiwuzhaol/images/email.png | 1 + shiwuzhaol22/shiwuzhaol/images/empty.png | 1 + shiwuzhaol22/shiwuzhaol/images/found.png | 1 + shiwuzhaol22/shiwuzhaol/images/home.png | 1 + shiwuzhaol22/shiwuzhaol/images/home_new.png | Bin 0 -> 2705 bytes .../shiwuzhaol/images/home_selected.png | 1 + .../shiwuzhaol/images/home_selected_new.png | Bin 0 -> 2705 bytes shiwuzhaol22/shiwuzhaol/images/lost.png | 1 + shiwuzhaol22/shiwuzhaol/images/match.png | 1 + shiwuzhaol22/shiwuzhaol/images/match.svg | 4 + shiwuzhaol22/shiwuzhaol/images/message.png | 1 + shiwuzhaol22/shiwuzhaol/images/message.svg | 3 + .../shiwuzhaol/images/message_new.png | Bin 0 -> 2507 bytes .../shiwuzhaol/images/message_selected.png | 1 + .../images/message_selected_new.png | Bin 0 -> 2507 bytes shiwuzhaol22/shiwuzhaol/images/no_match.svg | 12 + shiwuzhaol22/shiwuzhaol/images/no_message.svg | 11 + shiwuzhaol22/shiwuzhaol/images/phone.svg | 3 + shiwuzhaol22/shiwuzhaol/images/publish.png | 1 + .../shiwuzhaol/images/publish_new.png | Bin 0 -> 2589 bytes .../shiwuzhaol/images/publish_selected.png | 1 + .../images/publish_selected_new.png | Bin 0 -> 2589 bytes shiwuzhaol22/shiwuzhaol/images/search.png | 1 + shiwuzhaol22/shiwuzhaol/images/search_new.png | Bin 0 -> 2895 bytes .../shiwuzhaol/images/search_selected.png | 1 + .../shiwuzhaol/images/search_selected_new.png | Bin 0 -> 2895 bytes shiwuzhaol22/shiwuzhaol/images/settings.svg | 3 + shiwuzhaol22/shiwuzhaol/images/system.svg | 12 + shiwuzhaol22/shiwuzhaol/images/user.png | 1 + shiwuzhaol22/shiwuzhaol/images/user_new.png | Bin 0 -> 2160 bytes .../shiwuzhaol/images/user_selected.png | 1 + .../shiwuzhaol/images/user_selected_new.png | Bin 0 -> 2160 bytes shiwuzhaol22/shiwuzhaol/images/website.png | 1 + shiwuzhaol22/shiwuzhaol/pages/about/about.js | 75 + .../shiwuzhaol/pages/about/about.wxml | 78 + .../shiwuzhaol/pages/about/about.wxss | 174 + .../shiwuzhaol/pages/agreement/agreement.js | 87 + .../shiwuzhaol/pages/agreement/agreement.wxml | 16 + .../shiwuzhaol/pages/agreement/agreement.wxss | 46 + shiwuzhaol22/shiwuzhaol/pages/claim/claim.js | 225 + .../shiwuzhaol/pages/claim/claim.wxml | 71 + .../shiwuzhaol/pages/claim/claim.wxss | 254 + .../shiwuzhaol/pages/detail/detail.js | 924 +++ .../shiwuzhaol/pages/detail/detail.wxml | 94 + .../shiwuzhaol/pages/detail/detail.wxss | 210 + shiwuzhaol22/shiwuzhaol/pages/index/index.js | 495 ++ .../shiwuzhaol/pages/index/index.wxml | 66 + .../shiwuzhaol/pages/index/index.wxss | 159 + shiwuzhaol22/shiwuzhaol/pages/login/login.js | 141 + .../shiwuzhaol/pages/login/login.wxml | 33 + .../shiwuzhaol/pages/login/login.wxss | 71 + shiwuzhaol22/shiwuzhaol/pages/match/match.js | 166 + .../shiwuzhaol/pages/match/match.wxml | 66 + .../shiwuzhaol/pages/match/match.wxss | 219 + .../shiwuzhaol/pages/message/message.js | 403 ++ .../shiwuzhaol/pages/message/message.wxml | 106 + .../shiwuzhaol/pages/message/message.wxss | 311 + .../shiwuzhaol/pages/privacy/privacy.js | 92 + .../shiwuzhaol/pages/privacy/privacy.wxml | 16 + .../shiwuzhaol/pages/privacy/privacy.wxss | 46 + .../shiwuzhaol/pages/publish/publish.js | 786 +++ .../shiwuzhaol/pages/publish/publish.wxml | 99 + .../shiwuzhaol/pages/publish/publish.wxs | 67 + .../shiwuzhaol/pages/publish/publish.wxss | 166 + .../shiwuzhaol/pages/search/search.js | 632 ++ .../shiwuzhaol/pages/search/search.wxml | 91 + .../shiwuzhaol/pages/search/search.wxss | 312 + .../shiwuzhaol/pages/settings/settings.js | 393 ++ .../shiwuzhaol/pages/settings/settings.wxml | 107 + .../shiwuzhaol/pages/settings/settings.wxss | 133 + shiwuzhaol22/shiwuzhaol/pages/user/user.js | 478 ++ shiwuzhaol22/shiwuzhaol/pages/user/user.wxml | 112 + shiwuzhaol22/shiwuzhaol/pages/user/user.wxss | 247 + .../shiwuzhaol/pages/webview/webview.js | 71 + .../shiwuzhaol/pages/webview/webview.wxml | 19 + .../shiwuzhaol/pages/webview/webview.wxss | 54 + shiwuzhaol22/shiwuzhaol/project.config.json | 96 + .../shiwuzhaol/project.private.config.json | 24 + shiwuzhaol22/shiwuzhaol/utils/md5.js | 223 + .../utils/mobilenetFeatureExtractor.js | 205 + 100 files changed, 16327 insertions(+) create mode 100644 shiwuzhaol22/README.md create mode 100644 shiwuzhaol22/project.config.json create mode 100644 shiwuzhaol22/project.private.config.json create mode 100644 shiwuzhaol22/shiwuzhaol/app.js create mode 100644 shiwuzhaol22/shiwuzhaol/app.json create mode 100644 shiwuzhaol22/shiwuzhaol/app.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/cloud_development_guide.md create mode 100644 shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/config.json create mode 100644 shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/index.js create mode 100644 shiwuzhaol22/shiwuzhaol/cloudfunctions/imageSearch/package.json create mode 100644 shiwuzhaol22/shiwuzhaol/cloudfunctions/login/index.js create mode 100644 shiwuzhaol22/shiwuzhaol/cloudfunctions/login/package.json create mode 100644 shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/config.json create mode 100644 shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/index.js create mode 100644 shiwuzhaol22/shiwuzhaol/cloudfunctions/tencentAI/package.json create mode 100644 shiwuzhaol22/shiwuzhaol/images/app_logo.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/copy.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/default_avatar.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/developer.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/edit.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/email.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/empty.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/found.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/home.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/home_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/home_selected.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/home_selected_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/lost.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/match.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/match.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/message.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/message.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/message_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/message_selected.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/message_selected_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/no_match.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/no_message.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/phone.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/publish.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/publish_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/publish_selected.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/publish_selected_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/search.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/search_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/search_selected.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/search_selected_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/settings.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/system.svg create mode 100644 shiwuzhaol22/shiwuzhaol/images/user.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/user_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/user_selected.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/user_selected_new.png create mode 100644 shiwuzhaol22/shiwuzhaol/images/website.png create mode 100644 shiwuzhaol22/shiwuzhaol/pages/about/about.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/about/about.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/about/about.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/agreement/agreement.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/claim/claim.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/claim/claim.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/claim/claim.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/detail/detail.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/detail/detail.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/detail/detail.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/index/index.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/index/index.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/index/index.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/login/login.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/login/login.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/login/login.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/match/match.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/match/match.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/match/match.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/message/message.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/message/message.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/message/message.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/privacy/privacy.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/publish/publish.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxs create mode 100644 shiwuzhaol22/shiwuzhaol/pages/publish/publish.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/search/search.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/search/search.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/search/search.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/settings/settings.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/settings/settings.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/settings/settings.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/user/user.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/user/user.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/user/user.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/pages/webview/webview.js create mode 100644 shiwuzhaol22/shiwuzhaol/pages/webview/webview.wxml create mode 100644 shiwuzhaol22/shiwuzhaol/pages/webview/webview.wxss create mode 100644 shiwuzhaol22/shiwuzhaol/project.config.json create mode 100644 shiwuzhaol22/shiwuzhaol/project.private.config.json create mode 100644 shiwuzhaol22/shiwuzhaol/utils/md5.js create mode 100644 shiwuzhaol22/shiwuzhaol/utils/mobilenetFeatureExtractor.js 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 0000000000000000000000000000000000000000..9c3bd3f9a380a5efbc49171b2e61cb17c004d4ae GIT binary patch literal 2705 zcmV;C3U2j@P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D3MNTJK~z{rZCHDZ zRmB7wIwJabjhFUZ+_qJd(1au zA|pc#FaQX^0LA|XAgO-=W27ID-!g+RFkmjBu^D3k0zA$)z~udfY(+t!Vl9l1fgw1U z5;`i0Bc1B5+GrWtk}4$C~1rV3}8(Kw!API zLB>KTqJ9&Qga9K7aPpbLp-p%R7+J^|3nBtB2)KkJDQ?6HvOp-k%VUiaEy(o?38ha# zSXm6v{$!T)-N4u;7=2G46Ci6U5cKJj0Zke!7BI%Dz?@DHgiS)yUt=tct;oV@urbQ6 zFlS69pCj`O0R7G+t8sp^M6ML9%93#=nO5vn;PKHj1`je8{xgUPqpUYJ6%>=t$an*T zh$xEY)>6eNBXS9OU&(#ca@JT_TY)tYhcwoz-6bBAUdIC`(A3E#;2^Il)QZSWQL5zT zeMAASXzI6Wb_mbNS4qN-*9yS`LH-#SQ_)~39-#2Q9KHq@k<3?sPfgrQA7wf>u}Uch z>Hvk4za^pRdm8`N`uDJ6)f&K9Tr+oT9{tc1MkR@bBql%Bz%s@r1|`Lm zwM|XjR^CMWSOP5I{`BxuSa9oI=-Gb|pLX})`d@V6FMod;)>dF(vGL=rcz@GoY~HpD z72BkeQz|PrRfMtNH$>0O)mX3hv!3-cb5%NDqxW@spXHD9bvo^HzRt|mn7NF1{c#b; zo->2bzVH%5^bFDSndevVl*!Ziy9fTv=U-mMF>M_j(>9H*?Hye6&N^nk&dk^7LLt-p zj4ou%+yLuYl`drT!4p1u?G%9Z>{x&WxCAg`f3=@C-ToV%Hf0u9zwr(uBX@W2<*q&5 zBqLv4^A^WX>Ez^#ui(U)^Vr+>CBJ>%EUs9+hAt`TYji>6xu5`jjhP!zfR07>OeB|5 z&EQ6e;~YLx#q~eG6aU??2`@ag1amK$g~y&=jtl4AfNAp=;E89S$NV{+c;U&#s0|Dt ztJiVDkRh;E9beZF?~{NjZveWOeD}dMFqpZ4M%tCVq|yF^pL6CF3wYk_A8_lAU5qjE z!NreqOj`#RKmIiT_QX=QOzPkRe|?l8dT!mZljmN1IXkbqnd8p8sDZ&`R$;JC7wTeu zHz4bai3bC$(kD=AYUgY8!Ewi~-JCk-Do(%b2KF5Ij6MW*-Mfh6&b^4wz4$UibPU1q zxffURl*u#MbBsto{L z@&jL^b9MTVal?lnbHemXIq$}w^29{FK>T{l;2ZRONV|@wGSC^7N^l zTzJQCSsfVQS6_e4c?*8V2_19!{s$lBATwW6OVtX1%3w%Aay)>^ygoR-_V#~x>Xc62 zcxxArR{Qz&;cs~5!rOS(cjs{Zh7U?nJ;2O~Ea&QcZ{vrY&~YiRTzDJ5IediGfdMY~ zWfxDII-9S*y^bz8W@)fTLld51b ze~^hA0J;G>rFZe;|KynV4&L|B!;CRj`T>)yAY z9rLc^`QN*SyY}p5h@SU9w1lmbrg6!W&oD$!=Y&Z~HAPS=z*+-)K4e_B;uW^Gckr>L z%Se)&KH18NGw1S>Yk$n%zCJo%XRV}E?h=EdKXU_guEyTJK3;m=Eo_@HmzzG>Ml$m8 zrOVjbK8?#(tStFqifnN&sb)&s4k?xIz5@p_V)$@;vSTN@?tKtf|Ktu#o;U$7Eqw%| zM-2y~RNGQ4SjmyPacgD+$-JT-J?bR<>mN&SPTNFWb;}*N^PWZ6ynQD|j~IdOo&%-Q zK?~+oO@$a3LMCSlBYP_uF;MeZwD>XX`|Kc&_E+)lhD~_))#W&2{8%aBHJMjz6L#<2 zkAuBmlyfY`wYK1_GfvIxJE=D@%=^-70KZ`!cqyU5V}MS7Xw5&cb^eKLQCv>akYqzLFjhA>k7zNDK}llCcC?ho{ib zDN+$EP8P;kQ0n*sSbkC?2!oj9=p>OQiY1|GE(KZAA}6op69ka_GXWEyjJjxnSN$rb zF$l|PKmsX0RVfmYLC(}rB^Z+caR*USa$sT61g;1YIg1!rTS2ALgi2)ytZB*_HO6Rv z1=ecf*27PwG6WUdq~{S?sIWu9OKyvtAn4_h&vK^3`5R4s_(&B~=3I@|mXmQ(b8`_G z2Kb`yD;(|b$C;;}TA0fKpYGj<=3zt8+H$ghQdW)}t)lPHA#7apZ;TqztZT|VK!bz@ z$YLkaR3nR5^6}Dduyx07ta@WDA_<#Pj}bG8Jmo}#NMG~C%S_LoGYgZ>J`>a%h!l9K z$!`lQ*Kdx&yPW7eVG4c_MAVz9#GH^pxez)=xL$i0gQz(zfe`snMx(G~D(ZWs)T026 zt)R4%BrR1?VKB|~X*OUJ{pp^M5o9RME7DPFCf#NvA<31rFOiauSGOo)8+mflo}#{h z3J`&ss7g \ 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 0000000000000000000000000000000000000000..9c3bd3f9a380a5efbc49171b2e61cb17c004d4ae GIT binary patch literal 2705 zcmV;C3U2j@P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D3MNTJK~z{rZCHDZ zRmB7wIwJabjhFUZ+_qJd(1au zA|pc#FaQX^0LA|XAgO-=W27ID-!g+RFkmjBu^D3k0zA$)z~udfY(+t!Vl9l1fgw1U z5;`i0Bc1B5+GrWtk}4$C~1rV3}8(Kw!API zLB>KTqJ9&Qga9K7aPpbLp-p%R7+J^|3nBtB2)KkJDQ?6HvOp-k%VUiaEy(o?38ha# zSXm6v{$!T)-N4u;7=2G46Ci6U5cKJj0Zke!7BI%Dz?@DHgiS)yUt=tct;oV@urbQ6 zFlS69pCj`O0R7G+t8sp^M6ML9%93#=nO5vn;PKHj1`je8{xgUPqpUYJ6%>=t$an*T zh$xEY)>6eNBXS9OU&(#ca@JT_TY)tYhcwoz-6bBAUdIC`(A3E#;2^Il)QZSWQL5zT zeMAASXzI6Wb_mbNS4qN-*9yS`LH-#SQ_)~39-#2Q9KHq@k<3?sPfgrQA7wf>u}Uch z>Hvk4za^pRdm8`N`uDJ6)f&K9Tr+oT9{tc1MkR@bBql%Bz%s@r1|`Lm zwM|XjR^CMWSOP5I{`BxuSa9oI=-Gb|pLX})`d@V6FMod;)>dF(vGL=rcz@GoY~HpD z72BkeQz|PrRfMtNH$>0O)mX3hv!3-cb5%NDqxW@spXHD9bvo^HzRt|mn7NF1{c#b; zo->2bzVH%5^bFDSndevVl*!Ziy9fTv=U-mMF>M_j(>9H*?Hye6&N^nk&dk^7LLt-p zj4ou%+yLuYl`drT!4p1u?G%9Z>{x&WxCAg`f3=@C-ToV%Hf0u9zwr(uBX@W2<*q&5 zBqLv4^A^WX>Ez^#ui(U)^Vr+>CBJ>%EUs9+hAt`TYji>6xu5`jjhP!zfR07>OeB|5 z&EQ6e;~YLx#q~eG6aU??2`@ag1amK$g~y&=jtl4AfNAp=;E89S$NV{+c;U&#s0|Dt ztJiVDkRh;E9beZF?~{NjZveWOeD}dMFqpZ4M%tCVq|yF^pL6CF3wYk_A8_lAU5qjE z!NreqOj`#RKmIiT_QX=QOzPkRe|?l8dT!mZljmN1IXkbqnd8p8sDZ&`R$;JC7wTeu zHz4bai3bC$(kD=AYUgY8!Ewi~-JCk-Do(%b2KF5Ij6MW*-Mfh6&b^4wz4$UibPU1q zxffURl*u#MbBsto{L z@&jL^b9MTVal?lnbHemXIq$}w^29{FK>T{l;2ZRONV|@wGSC^7N^l zTzJQCSsfVQS6_e4c?*8V2_19!{s$lBATwW6OVtX1%3w%Aay)>^ygoR-_V#~x>Xc62 zcxxArR{Qz&;cs~5!rOS(cjs{Zh7U?nJ;2O~Ea&QcZ{vrY&~YiRTzDJ5IediGfdMY~ zWfxDII-9S*y^bz8W@)fTLld51b ze~^hA0J;G>rFZe;|KynV4&L|B!;CRj`T>)yAY z9rLc^`QN*SyY}p5h@SU9w1lmbrg6!W&oD$!=Y&Z~HAPS=z*+-)K4e_B;uW^Gckr>L z%Se)&KH18NGw1S>Yk$n%zCJo%XRV}E?h=EdKXU_guEyTJK3;m=Eo_@HmzzG>Ml$m8 zrOVjbK8?#(tStFqifnN&sb)&s4k?xIz5@p_V)$@;vSTN@?tKtf|Ktu#o;U$7Eqw%| zM-2y~RNGQ4SjmyPacgD+$-JT-J?bR<>mN&SPTNFWb;}*N^PWZ6ynQD|j~IdOo&%-Q zK?~+oO@$a3LMCSlBYP_uF;MeZwD>XX`|Kc&_E+)lhD~_))#W&2{8%aBHJMjz6L#<2 zkAuBmlyfY`wYK1_GfvIxJE=D@%=^-70KZ`!cqyU5V}MS7Xw5&cb^eKLQCv>akYqzLFjhA>k7zNDK}llCcC?ho{ib zDN+$EP8P;kQ0n*sSbkC?2!oj9=p>OQiY1|GE(KZAA}6op69ka_GXWEyjJjxnSN$rb zF$l|PKmsX0RVfmYLC(}rB^Z+caR*USa$sT61g;1YIg1!rTS2ALgi2)ytZB*_HO6Rv z1=ecf*27PwG6WUdq~{S?sIWu9OKyvtAn4_h&vK^3`5R4s_(&B~=3I@|mXmQ(b8`_G z2Kb`yD;(|b$C;;}TA0fKpYGj<=3zt8+H$ghQdW)}t)lPHA#7apZ;TqztZT|VK!bz@ z$YLkaR3nR5^6}Dduyx07ta@WDA_<#Pj}bG8Jmo}#NMG~C%S_LoGYgZ>J`>a%h!l9K z$!`lQ*Kdx&yPW7eVG4c_MAVz9#GH^pxez)=xL$i0gQz(zfe`snMx(G~D(ZWs)T026 zt)R4%BrR1?VKB|~X*OUJ{pp^M5o9RME7DPFCf#NvA<31rFOiauSGOo)8+mflo}#{h z3J`&ss7g \ 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 0000000000000000000000000000000000000000..d4c1d4fadafd143da9f87b184c37504a15a79226 GIT binary patch literal 2507 zcmV;+2{iVJP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D31CS?K~z{rjhS7n z9aR;_e{0Vw~Edr^tdUSIH0Z3u}iMcPo)N-)BkBoq>8%8Q^SvGswE1_=npKuyYn zLLxkZ2^5&$(;^G#4slFFY7LB**L4!#=6Q}sNi=xD|DJrG#SW%DoK;0 z)}H7UNI)d`v`!@w9B2{7#)%jJ2?3-+q!UV{>RiTV`M1r_wpY~-rmBe{`15X#kFYO? z<^EBDOB|#!9}3ce%zv)A@*T3~^NL!uXs+ZIaq|}6Zkn;SZHtJy!`ea?NkRw%PMFcC-!zeozg$^?DpW|**4nlWB5EP+ z{-K(vsv(a*zMFgQxrYm;E?~)$B`)WuRc&`C?@w}NXS(rt%-q~rcHMtJJ9g|KrHE<- zr!p|J2MN%$wJt1ttfSG#Iv&sKXk@)^M*8B5FY1ydOLXtvz1p;m*3DQ=x2tlk%$czI5 zDJ3qu>@v=rIqhN91xrVJmJEtu*_ksG$|Tc}V2+-tK&Xl$GpQTFCzRy0;`~Ub-5{9* z9dK{`b8~d)OxsN;cT@?%HRWS+bXQ@3gn;I-5upl0LKuKls2cZ$F!a=Ad}!cpqhb>W zYyi#$nAPS8irMlA+ueTJ%G7ns_1Ax!wQJTgJNq|+RGd9K&&G`#xn})(KKkfi1PT28 zAOB?C)mO85^L5P6FMxI&P}cWm%5h$FJZc)ukF9kk>KWcw%_Mv3)G1!xzn?eX{38c` zcK{LL_1E9v;K73&`NJC=K6Dt7z-zC*#?d23c=grabNu-6-kKeq*zZZL*-b+=DE_2Y zCroP2G-^h-cv4kXuU^fa-@B93r%!Xk4c{T9#D)#)x$U;wi7B%2n;VI#<+|&>#mzTw zVQOlMwQJUZmr=PDJLP?}+c$$QL=9>Y1BJymZQ9JHO`F-i{VtM5go>omecF68rGyfD z1%;U?r>rUr>^ga_7b`@Oz@9x%u=j=MIdI?=Vr&q>vxx=Gpc)c#q-AX8lKl$UB~gGx z2$p{#8ECPfkpe6NsZdJN*0Y2K97suNzEv@NADrP*t7fFbVUw~0c%5ivqg5uxQhi^* zXv66vgsN!LD8)E4NCnuKOf8zso>@2%D`5N2JifE9P5R)>su@n6JV^)^>{`k)j01bM zU9PTik~UDe)bjF-t_ZHG0dJjni&d*u(;H~ zfd?Mo)?063>C&YvS~MgjGa*=cUe^tET|0^AolI!ASns~Du&}^0&pgAu_ub2(gNInX z`YZ0^y~C9IAZ=`{Kp0!GFdl#6*YjG}BmK?3eL6EUqax@<7cJAeu5~z%4hnm|^+FvXrH_HflzYtdwd0cs`MLk~T~ zgAe}LDdoR6Rb~B#4eZ;uj})5{NR*w{PXx@nfuA z^L0`(!Hi~XTpROSigN3`O+<)oEL!fyTjmmhatg_+GpM1Q6%-|=mi_x*=Iyut#Lk^R zq^dxPMOvKyvI27J$o~AzT-M7{yHk{++AkJ-F?CRRhPh&7>9Q$4|G8;%=(3FIm`to{ zv;W`L6$y5BG2gfSlO{l{-0FroEX{|V06Ljt#TOC#uvyw|CK9|N@<;Ct7wk+vQLtuQ_958=TpCMj#+CViZDRmGgIGBh zQ$tKOF*dZ`Q)+12nyt6r#^FPU`Px@kGyCDJf$NcUmMMd^Owu%9A?=b|h`TFzCNLj# zG_Fz`uMQZndNGSta&w;K{_Rn(YNiHz-(&Ttdf6pZmGfX$O4su12z67s(|a$!g`!Q1 zIx9wVH!iboWtLe$QBrD5821CSOb(X(zDJA)P0CK{?5rIc^S|=m&h22Y-VAM)eqz9c zL?$r3`Vn=m2<~a9yH#hAplEpoY9M^mUO?oH)Pd4}xoP?!xwdU=F7G||iS3_wa|g$`(M;Z7;NXT33{P0lmz7z9CRnwGY;1d!AyXD}GD zc=2Zl>h>2;H(A;&kA%sS7 \ 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 0000000000000000000000000000000000000000..d4c1d4fadafd143da9f87b184c37504a15a79226 GIT binary patch literal 2507 zcmV;+2{iVJP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D31CS?K~z{rjhS7n z9aR;_e{0Vw~Edr^tdUSIH0Z3u}iMcPo)N-)BkBoq>8%8Q^SvGswE1_=npKuyYn zLLxkZ2^5&$(;^G#4slFFY7LB**L4!#=6Q}sNi=xD|DJrG#SW%DoK;0 z)}H7UNI)d`v`!@w9B2{7#)%jJ2?3-+q!UV{>RiTV`M1r_wpY~-rmBe{`15X#kFYO? z<^EBDOB|#!9}3ce%zv)A@*T3~^NL!uXs+ZIaq|}6Zkn;SZHtJy!`ea?NkRw%PMFcC-!zeozg$^?DpW|**4nlWB5EP+ z{-K(vsv(a*zMFgQxrYm;E?~)$B`)WuRc&`C?@w}NXS(rt%-q~rcHMtJJ9g|KrHE<- zr!p|J2MN%$wJt1ttfSG#Iv&sKXk@)^M*8B5FY1ydOLXtvz1p;m*3DQ=x2tlk%$czI5 zDJ3qu>@v=rIqhN91xrVJmJEtu*_ksG$|Tc}V2+-tK&Xl$GpQTFCzRy0;`~Ub-5{9* z9dK{`b8~d)OxsN;cT@?%HRWS+bXQ@3gn;I-5upl0LKuKls2cZ$F!a=Ad}!cpqhb>W zYyi#$nAPS8irMlA+ueTJ%G7ns_1Ax!wQJTgJNq|+RGd9K&&G`#xn})(KKkfi1PT28 zAOB?C)mO85^L5P6FMxI&P}cWm%5h$FJZc)ukF9kk>KWcw%_Mv3)G1!xzn?eX{38c` zcK{LL_1E9v;K73&`NJC=K6Dt7z-zC*#?d23c=grabNu-6-kKeq*zZZL*-b+=DE_2Y zCroP2G-^h-cv4kXuU^fa-@B93r%!Xk4c{T9#D)#)x$U;wi7B%2n;VI#<+|&>#mzTw zVQOlMwQJUZmr=PDJLP?}+c$$QL=9>Y1BJymZQ9JHO`F-i{VtM5go>omecF68rGyfD z1%;U?r>rUr>^ga_7b`@Oz@9x%u=j=MIdI?=Vr&q>vxx=Gpc)c#q-AX8lKl$UB~gGx z2$p{#8ECPfkpe6NsZdJN*0Y2K97suNzEv@NADrP*t7fFbVUw~0c%5ivqg5uxQhi^* zXv66vgsN!LD8)E4NCnuKOf8zso>@2%D`5N2JifE9P5R)>su@n6JV^)^>{`k)j01bM zU9PTik~UDe)bjF-t_ZHG0dJjni&d*u(;H~ zfd?Mo)?063>C&YvS~MgjGa*=cUe^tET|0^AolI!ASns~Du&}^0&pgAu_ub2(gNInX z`YZ0^y~C9IAZ=`{Kp0!GFdl#6*YjG}BmK?3eL6EUqax@<7cJAeu5~z%4hnm|^+FvXrH_HflzYtdwd0cs`MLk~T~ zgAe}LDdoR6Rb~B#4eZ;uj})5{NR*w{PXx@nfuA z^L0`(!Hi~XTpROSigN3`O+<)oEL!fyTjmmhatg_+GpM1Q6%-|=mi_x*=Iyut#Lk^R zq^dxPMOvKyvI27J$o~AzT-M7{yHk{++AkJ-F?CRRhPh&7>9Q$4|G8;%=(3FIm`to{ zv;W`L6$y5BG2gfSlO{l{-0FroEX{|V06Ljt#TOC#uvyw|CK9|N@<;Ct7wk+vQLtuQ_958=TpCMj#+CViZDRmGgIGBh zQ$tKOF*dZ`Q)+12nyt6r#^FPU`Px@kGyCDJf$NcUmMMd^Owu%9A?=b|h`TFzCNLj# zG_Fz`uMQZndNGSta&w;K{_Rn(YNiHz-(&Ttdf6pZmGfX$O4su12z67s(|a$!g`!Q1 zIx9wVH!iboWtLe$QBrD5821CSOb(X(zDJA)P0CK{?5rIc^S|=m&h22Y-VAM)eqz9c zL?$r3`Vn=m2<~a9yH#hAplEpoY9M^mUO?oH)Pd4}xoP?!xwdU=F7G||iS3_wa|g$`(M;Z7;NXT33{P0lmz7z9CRnwGY;1d!AyXD}GD zc=2Zl>h>2;H(A;&kA%sS7 + + + + + + + + + + + \ 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 0000000000000000000000000000000000000000..067c3ce4c1afb7b163aba8d9e1dc2832e942a16a GIT binary patch literal 2589 zcmV+&3gY#NP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D39?Bz5U#!*+%xJ5HUQA%LRK*f7fVD4G&__qCW1zy= z3p4H5hYCo7bVeNYrGT^@I}HbVaY%>KX&+>O-bqf9^FQampAWnHza{u(_MAPt-+jOR z_S=oB)$3y7IwA&8fPhjUg18X?tpk+SpcHYf&daa7%<*H#ICbh2XV0GH;-!m>jgBG$ z&CShRd+lsGJGFuSzzn`wIF04}| zM`QJ~eL)C=2B=1pGCn>g(Is5Lr(%aiBL8v7RwS*-trIMCXNx5$= zr3NLX20PZ!;OG6E{SF;{Pfw4GyfY$`)kzs2uVmzn$;4#Zk7`vRimEa(QB-V9CaaTD ztJP%J6T76T=_&~VC1J@akyAirT2X1+8VWRFsHLfCrVQ*HkZQFm6BFa9e5cFA#F#`; zRa8`qB(6ueFaSUY0a3Na`t?8L#TO5wb#5IYjn;hMg>Mo3Ts9m;z!+iGs@44Q`RAG0 zI0Iu$jxG#KsA@DRalM`l>L6geGQmCfe4E!^`!`yvj8LnJjU6Y=2CAJdDB&R+nHfI- zU}9m}t;;!b zG_lUlnCZTiRr2KU>-=c_dUOz4Gn^Z3nu+TKVTtFSeU`0Tx1u#DZSC|PNWlBwfe76G z)vwXhvxY!x>v0hV4<6*iiGNKSzte(?$$_1_*t~f&Ca$A{09B2uA|~d-g-a}6vWSWC ziq$?FR}=!ARLQ31R^EE^ZMJROMy(zJPzp=@=JCf_vUm}d%2=LoI%Qo<3qs?Jt0cLxYTryo1&OT5HYIPd$Z*NrfgM=C*Lc#7-t6r%1Ap3lTfwKJ#m={6``$ zjqRtOehRHMIB)0D~)CB=qwnXNyPl;-CX5^u3voxy_# zsYNyPE3drF#~*!^u+Ld8{v)Fij-f+SN+D9T#Aku*xPuHEuT+ZV5+?BJr=M`*#0m7# zqc2T~sg%MkXO?L?mGHAmSD1EHOwPsx6jJbXD${n9g5$@Jqlbr2CM8ZD(j?s4`UMs) z?4qNila7U5bS&(ov-8VzbapX+{`?f*G55WB^XJpq(M3n6YaO;NT-Zr#ODo2t%U!Sl zP?XCjQ41CT3wv0%Xhde`()E)SC)TK;^uX!GXIN42yx%fy6B&Nu}fM-kY*{g;f6jJb0q3NdGp|BdDwX_XA>g#u7-LhAtN5oj z?Ui$&te1Tyl;F6&){1()o~e}7w5E(zD(GqB&&zVDAqB@Mai#aRpL0K4h~&nTzVmQ0 zpMiw0`-wugwzg&vd?%%hO-vYu7!izdEq2Wa#@IvNQl!fG@hpySaV&&k31dtuNjb3@ zrCVEDP>U8Xl94n2Nfq~qfS_2icqxYuzsP^jpLaJuYndgmHRSN{FpvK9ktCKxVAm77 zS-$)>$J9dNV-e(9mESs9saAq`OA8wOwQB zl3i@QcHOnt@%!C-xNZ5eoPw{vUS{i0wsP^ph2*RjZ@<8$OBWd%8_TS!lk3F`JaB&x z3=9lN5Rgmn7rPU1fQdLqChDyq(PDy+F4HAY|;*lLMT)Y}=RWmR!AbRD> zl`iXB%@auCp)jFxv7QWNzr`&hsKncL=>X;VWwEnN-8n6=V#Nydk|j%MZ=dUsQxXfA zT~$!ZgG{#>3%?U$!4@1@eKNb&D=R4G&Yj27?ryXZVckRDPdr!@mVloqIyB**SIa+V zz?7+Ub$nC4V<$y~2iL7b#MoQi#=bsE4Ku8g|Bo>wOOW*On})}%-T(B}^C#M2hUe`KxnhBzcgl#S)U9k97dF|)dPLp|8v8#iuZ z_BFE+W9+R?Bg)or}__S>|!G^3S5O-_!B5ksIu4*lgY4}R~v1VQ=)mT;O* zW5@K4JHAfunzd+`2*nr%2M0NJ?5M3&vKuW!Yd=yMv4MU24shRnt1)p*5QI>zPRjWB zs8lMKC5o!D>BpOFPP=0Mnm(6>p^~tqC3Jbv(iieu$-jUV>FrIhzrSCqQRE)8MkR_S zMNL*G#Kf_E`p^nRzysfTfPenu@2=XI#&ZQwxuPQPmAu=gf{k0@8{fQ(p`jtPa{B-X z!w?r89$8RIIQZv-+&`vsIu8#YFqlAc4JlUMGpyYA+|{{3i$JMS%? z2=>|ujxP!$hOnd=8al-0EnAbfmMemAdT;%Z86Nys>d68#irY z`}SYZ+R{X9V(PUTahyJ03PPX*8XFsJ9yF%r*m(;)Q`=DrbP)34haa(b?_U1&!V7%x z!3TC}bIUl@l|Fg5&u!HzzJP#(K@gt5TZNr%!S8rK1cF4>K}yhV$po^ZDnW12AjWEauFaLuY3v%Wl1u z6)RS \ 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 0000000000000000000000000000000000000000..067c3ce4c1afb7b163aba8d9e1dc2832e942a16a GIT binary patch literal 2589 zcmV+&3gY#NP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D39?Bz5U#!*+%xJ5HUQA%LRK*f7fVD4G&__qCW1zy= z3p4H5hYCo7bVeNYrGT^@I}HbVaY%>KX&+>O-bqf9^FQampAWnHza{u(_MAPt-+jOR z_S=oB)$3y7IwA&8fPhjUg18X?tpk+SpcHYf&daa7%<*H#ICbh2XV0GH;-!m>jgBG$ z&CShRd+lsGJGFuSzzn`wIF04}| zM`QJ~eL)C=2B=1pGCn>g(Is5Lr(%aiBL8v7RwS*-trIMCXNx5$= zr3NLX20PZ!;OG6E{SF;{Pfw4GyfY$`)kzs2uVmzn$;4#Zk7`vRimEa(QB-V9CaaTD ztJP%J6T76T=_&~VC1J@akyAirT2X1+8VWRFsHLfCrVQ*HkZQFm6BFa9e5cFA#F#`; zRa8`qB(6ueFaSUY0a3Na`t?8L#TO5wb#5IYjn;hMg>Mo3Ts9m;z!+iGs@44Q`RAG0 zI0Iu$jxG#KsA@DRalM`l>L6geGQmCfe4E!^`!`yvj8LnJjU6Y=2CAJdDB&R+nHfI- zU}9m}t;;!b zG_lUlnCZTiRr2KU>-=c_dUOz4Gn^Z3nu+TKVTtFSeU`0Tx1u#DZSC|PNWlBwfe76G z)vwXhvxY!x>v0hV4<6*iiGNKSzte(?$$_1_*t~f&Ca$A{09B2uA|~d-g-a}6vWSWC ziq$?FR}=!ARLQ31R^EE^ZMJROMy(zJPzp=@=JCf_vUm}d%2=LoI%Qo<3qs?Jt0cLxYTryo1&OT5HYIPd$Z*NrfgM=C*Lc#7-t6r%1Ap3lTfwKJ#m={6``$ zjqRtOehRHMIB)0D~)CB=qwnXNyPl;-CX5^u3voxy_# zsYNyPE3drF#~*!^u+Ld8{v)Fij-f+SN+D9T#Aku*xPuHEuT+ZV5+?BJr=M`*#0m7# zqc2T~sg%MkXO?L?mGHAmSD1EHOwPsx6jJbXD${n9g5$@Jqlbr2CM8ZD(j?s4`UMs) z?4qNila7U5bS&(ov-8VzbapX+{`?f*G55WB^XJpq(M3n6YaO;NT-Zr#ODo2t%U!Sl zP?XCjQ41CT3wv0%Xhde`()E)SC)TK;^uX!GXIN42yx%fy6B&Nu}fM-kY*{g;f6jJb0q3NdGp|BdDwX_XA>g#u7-LhAtN5oj z?Ui$&te1Tyl;F6&){1()o~e}7w5E(zD(GqB&&zVDAqB@Mai#aRpL0K4h~&nTzVmQ0 zpMiw0`-wugwzg&vd?%%hO-vYu7!izdEq2Wa#@IvNQl!fG@hpySaV&&k31dtuNjb3@ zrCVEDP>U8Xl94n2Nfq~qfS_2icqxYuzsP^jpLaJuYndgmHRSN{FpvK9ktCKxVAm77 zS-$)>$J9dNV-e(9mESs9saAq`OA8wOwQB zl3i@QcHOnt@%!C-xNZ5eoPw{vUS{i0wsP^ph2*RjZ@<8$OBWd%8_TS!lk3F`JaB&x z3=9lN5Rgmn7rPU1fQdLqChDyq(PDy+F4HAY|;*lLMT)Y}=RWmR!AbRD> zl`iXB%@auCp)jFxv7QWNzr`&hsKncL=>X;VWwEnN-8n6=V#Nydk|j%MZ=dUsQxXfA zT~$!ZgG{#>3%?U$!4@1@eKNb&D=R4G&Yj27?ryXZVckRDPdr!@mVloqIyB**SIa+V zz?7+Ub$nC4V<$y~2iL7b#MoQi#=bsE4Ku8g|Bo>wOOW*On})}%-T(B}^C#M2hUe`KxnhBzcgl#S)U9k97dF|)dPLp|8v8#iuZ z_BFE+W9+R?Bg)or}__S>|!G^3S5O-_!B5ksIu4*lgY4}R~v1VQ=)mT;O* zW5@K4JHAfunzd+`2*nr%2M0NJ?5M3&vKuW!Yd=yMv4MU24shRnt1)p*5QI>zPRjWB zs8lMKC5o!D>BpOFPP=0Mnm(6>p^~tqC3Jbv(iieu$-jUV>FrIhzrSCqQRE)8MkR_S zMNL*G#Kf_E`p^nRzysfTfPenu@2=XI#&ZQwxuPQPmAu=gf{k0@8{fQ(p`jtPa{B-X z!w?r89$8RIIQZv-+&`vsIu8#YFqlAc4JlUMGpyYA+|{{3i$JMS%? z2=>|ujxP!$hOnd=8al-0EnAbfmMemAdT;%Z86Nys>d68#irY z`}SYZ+R{X9V(PUTahyJ03PPX*8XFsJ9yF%r*m(;)Q`=DrbP)34haa(b?_U1&!V7%x z!3TC}bIUl@l|Fg5&u!HzzJP#(K@gt5TZNr%!S8rK1cF4>K}yhV$po^ZDnW12AjWEauFaLuY3v%Wl1u z6)RS \ 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 0000000000000000000000000000000000000000..99a58fa5ca084b7f101a6cd8cc28b9a3a5964300 GIT binary patch literal 2895 zcmV-V3$XNwP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D3gk&dK~z{rjah4S zRmBzl=A4_GXdXNT1p^@jE5(2TBnU=cMOiBaL|`e^xGb*LceTEXMo}KhLJ&o$kD_!@ z!9uYZAtGo*ognGudYa30LC0R zP6V!-0@sP6sks%6XPZHi!F8i>9T$u_fkL)o7bs~NAeoYsVUi>X(HfpbpcMmy^y%uGkRG_M6FU~bJ1B@Z!I_Q;~gV8q*!CklAg8uovA*E0OY{8h}L$T<^Qe=1O3?cY|6iVatcq_-0 z`yNA8&9_)EV;XK6J{0e-DaWUqx8va9qeyrhojP_vVL?7_pHPg;J6(o9PM?FVJ9lIC zlGiYzs1Q=}kl&O6tjC8mK>r)s_5|07fC<4#$b@fu2EcJ55Q0#A=e?+JID_)#Z)5e^ zPcZ%E*O7f?S4=Ga74myug-g=XaO}h>Y}r|bwHv=c(VzkN%lw&GSyql0<}JpCmG9z) z!U9Ms{KI`;GH8=oKKB7?9Bvell2gKK(VrN3UYw+q^vp{OsK>w&bn;X^-SNQV)bZ*; zw5sf5;@l(8i<9TYDUpbi$6HAVkN$n)Bu$+95OwZzJ=J`(k7m8Hm^xoGgz6d^i1SuT z=slhnBag@Y?}U-##r=7OBSu0Bf|N2e$wh){e>_a7-STPc&fQe{(ks-pUlAQRbi_I+ z;P=NoPC{^cq;wW_=~qOj>KdqM+!VU^iRVZN9tIfN8xPjFl4WE|DF~8;b%>h0|EcF` zT*-a3Z+|VNbnQdicJ3z56O>3;z_`|!E32FRoO2pJ;ZC~e(Nfw`Sxu?k@~N(&LGxT3 zpz(<3#X@IDDacA(>cNnb@Q=0YanEliV&Rg%Vcg9lFl0~xBuSliOf=ddr&9_sfC~^KqNvkE*zVxLI4p4UV9CC<#xk{OlUpTTtneRa~+v1PIY=`jVu82|!-NMc~WeC++M7FXx>#G#`< zL1kC1kutYAS~(?; zj-Q*Fkd={+mRL)WX9HY{w1}{)&5{#X&`?JDrD$quMpkA9z!)0Oo&${bwr~T|BwRg5 zP@ZTw-H7&WvT&}s8R_Zix}UT)2{ba6w}}7{w2OVy6VNj!8^?~HKs1_yD>`+;p0D@m zl4q=@NIgf!wAPR@FgU3EYA^crxf9$1OHvBIQkXqKg4bo! zs!$29#i}Gp097^NG;Q`g${jR{PSrKg?;bCu_PqzwUstXnk9#D9M_eRGhy)4Yk(7eI z-Cs*L-f|b^3>-N@r0vNAI<{-)vR-Kz&uqfz{H{3O2EQH3qL_F&2_#dzg~r||OP zC3xq()yT@sz>q=x@!lKru>Zi1xT(Rq8a`JDH5^+&`=qQE`EI|44w~(2c76wTG^ee7;5Sibwk|fUKDyV%EDwGTij76|) z)rWX??(2v|BA8TsGj1sCj~?BwL~1kzbq#0muYLRR(ZxZsgcrO+XbT$C z6RFgZ@C4R;T!D`^ZbJ3f-{9DZQxJlqZB`b3c~u@p3@t>-y}o1_W#$&p&OJ4XSGQ>Kc$_9qeTZ_d8%4E;j;QDl zogHrdR0dW}h$2#&FefNY{t^+S6cCbwBnL=93J;Q0OCJOop(PeWG$jS8(G=|&b$t?v z0Dpgb0frS0z_9Tp_`dcC7<2udmIy^E&FI`?X%S8ZN_(zzp5I&=9zD-_D|tFc3c=~I z=`T^+UIS=HRkg~YdY|$Hrw6CappLx<)9#wRBqS$~D;&y#twA2N%^JAWa678rys4|xG7YP42bD-7t-bVL#U;tg*>m-w_*}N?c!uhSpd0T4U#@K zID5YybCIAW%U4lax4yKpY`t=){~i;kL?S_#=JchlJ9m@EZGc<{382!7pN^Q-;hHAZ zzs*y>nGPaGWy$AC^HUqFQ%7=l?b!?KGRjO8|3Rlz+MfQ|@~LPd+deci%n% zC+iyU=yP+>pwza9tOT zXMaXoS}LN^sIJYXs$2k2SrFPFf!qAqkW1m)M$LqIRSmaQP~%{S-4v*=WQ7&EkrXf? zJrY6$0BjzsE*fHRG@t-T;7s#v0wt*a!Fo_{&4TB7;6jA_;kpje(k?Z3)RqS7Y)eA8 z6(>l4(cH|NWdq#ssLKcS&#VGUWA)0Qx%DlDkP<>jl`-@&%>Qo)ivrMLlLwT%mSlEl tv)k9D7wid02mztH8pFXH2N5@d{{TIVS@aI4dzJtI002ovPDHLkV1n#uWpMxi literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..99a58fa5ca084b7f101a6cd8cc28b9a3a5964300 GIT binary patch literal 2895 zcmV-V3$XNwP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D3gk&dK~z{rjah4S zRmBzl=A4_GXdXNT1p^@jE5(2TBnU=cMOiBaL|`e^xGb*LceTEXMo}KhLJ&o$kD_!@ z!9uYZAtGo*ognGudYa30LC0R zP6V!-0@sP6sks%6XPZHi!F8i>9T$u_fkL)o7bs~NAeoYsVUi>X(HfpbpcMmy^y%uGkRG_M6FU~bJ1B@Z!I_Q;~gV8q*!CklAg8uovA*E0OY{8h}L$T<^Qe=1O3?cY|6iVatcq_-0 z`yNA8&9_)EV;XK6J{0e-DaWUqx8va9qeyrhojP_vVL?7_pHPg;J6(o9PM?FVJ9lIC zlGiYzs1Q=}kl&O6tjC8mK>r)s_5|07fC<4#$b@fu2EcJ55Q0#A=e?+JID_)#Z)5e^ zPcZ%E*O7f?S4=Ga74myug-g=XaO}h>Y}r|bwHv=c(VzkN%lw&GSyql0<}JpCmG9z) z!U9Ms{KI`;GH8=oKKB7?9Bvell2gKK(VrN3UYw+q^vp{OsK>w&bn;X^-SNQV)bZ*; zw5sf5;@l(8i<9TYDUpbi$6HAVkN$n)Bu$+95OwZzJ=J`(k7m8Hm^xoGgz6d^i1SuT z=slhnBag@Y?}U-##r=7OBSu0Bf|N2e$wh){e>_a7-STPc&fQe{(ks-pUlAQRbi_I+ z;P=NoPC{^cq;wW_=~qOj>KdqM+!VU^iRVZN9tIfN8xPjFl4WE|DF~8;b%>h0|EcF` zT*-a3Z+|VNbnQdicJ3z56O>3;z_`|!E32FRoO2pJ;ZC~e(Nfw`Sxu?k@~N(&LGxT3 zpz(<3#X@IDDacA(>cNnb@Q=0YanEliV&Rg%Vcg9lFl0~xBuSliOf=ddr&9_sfC~^KqNvkE*zVxLI4p4UV9CC<#xk{OlUpTTtneRa~+v1PIY=`jVu82|!-NMc~WeC++M7FXx>#G#`< zL1kC1kutYAS~(?; zj-Q*Fkd={+mRL)WX9HY{w1}{)&5{#X&`?JDrD$quMpkA9z!)0Oo&${bwr~T|BwRg5 zP@ZTw-H7&WvT&}s8R_Zix}UT)2{ba6w}}7{w2OVy6VNj!8^?~HKs1_yD>`+;p0D@m zl4q=@NIgf!wAPR@FgU3EYA^crxf9$1OHvBIQkXqKg4bo! zs!$29#i}Gp097^NG;Q`g${jR{PSrKg?;bCu_PqzwUstXnk9#D9M_eRGhy)4Yk(7eI z-Cs*L-f|b^3>-N@r0vNAI<{-)vR-Kz&uqfz{H{3O2EQH3qL_F&2_#dzg~r||OP zC3xq()yT@sz>q=x@!lKru>Zi1xT(Rq8a`JDH5^+&`=qQE`EI|44w~(2c76wTG^ee7;5Sibwk|fUKDyV%EDwGTij76|) z)rWX??(2v|BA8TsGj1sCj~?BwL~1kzbq#0muYLRR(ZxZsgcrO+XbT$C z6RFgZ@C4R;T!D`^ZbJ3f-{9DZQxJlqZB`b3c~u@p3@t>-y}o1_W#$&p&OJ4XSGQ>Kc$_9qeTZ_d8%4E;j;QDl zogHrdR0dW}h$2#&FefNY{t^+S6cCbwBnL=93J;Q0OCJOop(PeWG$jS8(G=|&b$t?v z0Dpgb0frS0z_9Tp_`dcC7<2udmIy^E&FI`?X%S8ZN_(zzp5I&=9zD-_D|tFc3c=~I z=`T^+UIS=HRkg~YdY|$Hrw6CappLx<)9#wRBqS$~D;&y#twA2N%^JAWa678rys4|xG7YP42bD-7t-bVL#U;tg*>m-w_*}N?c!uhSpd0T4U#@K zID5YybCIAW%U4lax4yKpY`t=){~i;kL?S_#=JchlJ9m@EZGc<{382!7pN^Q-;hHAZ zzs*y>nGPaGWy$AC^HUqFQ%7=l?b!?KGRjO8|3Rlz+MfQ|@~LPd+deci%n% zC+iyU=yP+>pwza9tOT zXMaXoS}LN^sIJYXs$2k2SrFPFf!qAqkW1m)M$LqIRSmaQP~%{S-4v*=WQ7&EkrXf? zJrY6$0BjzsE*fHRG@t-T;7s#v0wt*a!Fo_{&4TB7;6jA_;kpje(k?Z3)RqS7Y)eA8 z6(>l4(cH|NWdq#ssLKcS&#VGUWA)0Qx%DlDkP<>jl`-@&%>Qo)ivrMLlLwT%mSlEl tv)k9D7wid02mztH8pFXH2N5@d{{TIVS@aI4dzJtI002ovPDHLkV1n#uWpMxi literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..55f1c3b26ff154405e8e2f1697f082b317bb57c6 GIT binary patch literal 2160 zcmV-$2#@!PP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2n9(*K~z{rg;>vT zRmT}%m?~8rj8$b(ym4@fE64B7d|jORdEa$APxswBGiT2E&d<4X6{R6I z&gS93l!3Fq%C8@*V~B=>jW#PU{wZ_02E3; z>Unv_elCp>3L1(e9jq|+tD~{|<*R6-qzrq*IMT;jxKiw;SaGzJpnd73ANp8JF&ARZn2O`$ z-{+;@UE;>Z25Vb${mXS8IB^eMi3>0Lf}@PO9VtZkRwdYj#Po3*pNeEBks(R}g6@6kC)N|6yVH^@v0 z5)6_o*t00z14Bp0SAX&Zk%NN9&rhGm=H@1D|M@nYb09+9llK*@eROnm6i=S4z*+xt5!Zwgn8HGO z@)o3&0v)lsx{A@gqgg0qbrUYFjl3W(Zf0%iuH^DZe|yA%fdNiVPV&PK|C9p0C&(SG zcd?`6ty{M^eQcTo0|Q)KTx__w>Dr#XIFy;R*9uv=P=Hw8zki?8)6<-qnv4MR7%Y@? z!e?@7l2cPt{P^ShS>`ul-uH949$>Oy01>7O%etD@my~<=?olbln>TOLd4aVPnqZw^ zfg3k&P$|W`ckfDlwB6RXCf_S8>PQbTW^p3UHHp^oMc6vanVA{x+BL|JKKhH$W^X$I z-29z+4h{}-=2tUOkrH6$rIt`>xHxlFGo~`Pi;!(@ZEf-OpS@0FDxN?8CRbKg{PBid zd9uQD=g!fXil{V#HDj0cJC?{Nk!ZZOZ-=qtx<>UEWV=Rn!`V8N`@HXqZ zTNo%{VE~xoFU8tBzkP>Qr3+yFnXp`N+tqY^&*kZisd(|?MY`Z}ff#WpN0O9pGw>zm zi+_HBiHUL0IVkNjDnC+YB}vA!%p>K=(-!Mm8cAqf;j_>Fj~A0c2vAK3z)hc`YFLzl16| z@Q11Rj@hbc^1#y4r%}6{O2er3kSP(u zM(4}pq1yL_Hq%~A=NXjXc2q=#gLtr}JPSPv&lHXFrvVA-Kqf$f85-JW@tV7 \ 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 0000000000000000000000000000000000000000..55f1c3b26ff154405e8e2f1697f082b317bb57c6 GIT binary patch literal 2160 zcmV-$2#@!PP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2n9(*K~z{rg;>vT zRmT}%m?~8rj8$b(ym4@fE64B7d|jORdEa$APxswBGiT2E&d<4X6{R6I z&gS93l!3Fq%C8@*V~B=>jW#PU{wZ_02E3; z>Unv_elCp>3L1(e9jq|+tD~{|<*R6-qzrq*IMT;jxKiw;SaGzJpnd73ANp8JF&ARZn2O`$ z-{+;@UE;>Z25Vb${mXS8IB^eMi3>0Lf}@PO9VtZkRwdYj#Po3*pNeEBks(R}g6@6kC)N|6yVH^@v0 z5)6_o*t00z14Bp0SAX&Zk%NN9&rhGm=H@1D|M@nYb09+9llK*@eROnm6i=S4z*+xt5!Zwgn8HGO z@)o3&0v)lsx{A@gqgg0qbrUYFjl3W(Zf0%iuH^DZe|yA%fdNiVPV&PK|C9p0C&(SG zcd?`6ty{M^eQcTo0|Q)KTx__w>Dr#XIFy;R*9uv=P=Hw8zki?8)6<-qnv4MR7%Y@? z!e?@7l2cPt{P^ShS>`ul-uH949$>Oy01>7O%etD@my~<=?olbln>TOLd4aVPnqZw^ zfg3k&P$|W`ckfDlwB6RXCf_S8>PQbTW^p3UHHp^oMc6vanVA{x+BL|JKKhH$W^X$I z-29z+4h{}-=2tUOkrH6$rIt`>xHxlFGo~`Pi;!(@ZEf-OpS@0FDxN?8CRbKg{PBid zd9uQD=g!fXil{V#HDj0cJC?{Nk!ZZOZ-=qtx<>UEWV=Rn!`V8N`@HXqZ zTNo%{VE~xoFU8tBzkP>Qr3+yFnXp`N+tqY^&*kZisd(|?MY`Z}ff#WpN0O9pGw>zm zi+_HBiHUL0IVkNjDnC+YB}vA!%p>K=(-!Mm8cAqf;j_>Fj~A0c2vAK3z)hc`YFLzl16| z@Q11Rj@hbc^1#y4r%}6{O2er3kSP(u zM(4}pq1yL_Hq%~A=NXjXc2q=#gLtr}JPSPv&lHXFrvVA-Kqf$f85-JW@tV7 \ 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.claimerName}} 的认领请求 + {{item.createTime}} + + 待处理 + 已同意 + 已拒绝 + + + + + + + {{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' ? '丢失时间:' : '捡到时间:'}}{{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.title}} + 寻找中 + + + + + {{item.description}} + + {{item.lostTime}} + {{item.location}} + + + + + + + + + + {{item.title}} + 待认领 + + + + + {{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.title}} + {{item.time || '刚刚'}} + + {{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 @@ + + + + + + + 发布失物 + + + 发布招领 + + + + +
+ + + 标题 * + + + + + + 详细描述 * + + + + + + 地点 * + + {{location}} + 请选择丢失/捡到地点 + + + + + + + 时间 * + + {{time}} + 请选择丢失/捡到时间 + + + + + + + 联系人 * + + + + + 联系电话 * + + + + + + 上传图片 + + + + + × + + + + + + + + 最多上传{{maxImages}}张图片 + + + + +
+
\ 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}} + + + + + + + 搜索结果 ({{searchResults.length}}) + + + + + + {{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 @@ + + + + + + + + + 设置 + + + + + 智能匹配 + + + + + 关于我们 + + + + + + + + 我发布的 + + + 我的收藏 + + + + + + + + + + + + {{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 +}; +